diff options
277 files changed, 30298 insertions, 0 deletions
diff --git a/odl-aaa-moon/aaa/.gitignore b/odl-aaa-moon/aaa/.gitignore new file mode 100644 index 00000000..b8938691 --- /dev/null +++ b/odl-aaa-moon/aaa/.gitignore @@ -0,0 +1,26 @@ +*.class + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.ear + +# IDE Files +.classpath +.project +.settings/ +.idea + +# Generated stuff +target/ +META-INF/ +*.iml +.DS_Store +yang-gen-sal/ +yang-gen-config/ + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* diff --git a/odl-aaa-moon/aaa/README.md b/odl-aaa-moon/aaa/README.md new file mode 100644 index 00000000..dc748ef1 --- /dev/null +++ b/odl-aaa-moon/aaa/README.md @@ -0,0 +1,62 @@ +## Welcome to the OPNFV/Opendaylight AAA Project! + +This project is aimed at providing a flexible, pluggable framework with out-of-the-box capabilities for: + +* *Authentication*: Means to authenticate the identity of both human and machine users (direct or federated). +* *Authorization*: Means to authorize human or machine user access to resources including RPCs, notification subscriptions, and subsets of the datatree. +* *Accounting*: Means to record and access the records of human or machine user access to resources including RPCs, notifications, and subsets of the datatree + + + +### Building + +*Prerequisite:* The followings are required for building AAA: + +- Maven 3 +- Java 7 + +Get the code: + + clone the project with git + +Build it: + + cd aaa && mvn clean install -DskipTests + +### Export Moon information + +export MOON_SERVER_ADDR=192.168.56.101 +export MOON_SERVER_PORT=5000 + + +### Installing + +AAA installs into an existing Opendaylight controller Karaf installation. If you don't have an Opendaylight installation, please refer to this [page](https://wiki.opendaylight.org/view/OpenDaylight_Controller:Installation). + +Start the controller Karaf container: + cd distribution-karaf/target/assembly/ + bin/karaf + +Install AAA AuthN features: + + feature:install odl-aaa-shiro + +### Running + +Once the installation finishes, one can authenticates with the Opendaylight controller by presenting a username/password and a domain name (scope) to be logged into: + + curl -s -d 'grant_type=password&username=admin&password=admin' http://<controller>:<port>/moon/token + + curl -s -d 'grant_type=password&username=admin&password=password' http://localhost:8080/moon/token + +Upon successful authentication, the controller returns an access token with a configurable expiration in seconds, something similar to the followings: + + {"expires_in":3600,"token_type":"Bearer","access_token":"d772d85e-34c7-3099-bea5-cfafd3c747cb"} + +The access token can then be used to access protected resources on the controller by passing it along in the standard HTTP Authorization header with the resource request. Example: + + curl -s -H 'Authorization: Bearer d772d85e-34c7-3099-bea5-cfafd3c747cb' http://<controller>:<port>/restconf/operational/opendaylight-inventory:nodes + +Test HTTP Basic Authentication + + curl -u admin:password http://localhost:8080/auth/v1/domains
\ No newline at end of file diff --git a/odl-aaa-moon/aaa/aaa-authn-api/pom.xml b/odl-aaa-moon/aaa/aaa-authn-api/pom.xml new file mode 100644 index 00000000..97249ace --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn-api/pom.xml @@ -0,0 +1,38 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + <parent> + <groupId>org.opendaylight.aaa</groupId> + <artifactId>aaa-parent</artifactId> + <version>0.3.2-Beryllium-SR2</version> + <relativePath>../parent</relativePath> + </parent> + + <artifactId>aaa-authn-api</artifactId> + <packaging>bundle</packaging> + + <dependencies> + <dependency> + <groupId>org.slf4j</groupId> + <artifactId>slf4j-api</artifactId> + </dependency> + <dependency> + <groupId>org.slf4j</groupId> + <artifactId>slf4j-simple</artifactId> + </dependency> + <dependency> + <groupId>com.sun.jersey</groupId> + <artifactId>jersey-server</artifactId> + <scope>provided</scope> + </dependency> + </dependencies> + <build> + <plugins> + <plugin> + <groupId>org.apache.felix</groupId> + <artifactId>maven-bundle-plugin</artifactId> + </plugin> + </plugins> + </build> + +</project> diff --git a/odl-aaa-moon/aaa/aaa-authn-api/src/main/docs/Makefile b/odl-aaa-moon/aaa/aaa-authn-api/src/main/docs/Makefile new file mode 100644 index 00000000..446795b4 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn-api/src/main/docs/Makefile @@ -0,0 +1,29 @@ +all: sssd_configuration.html sssd_configuration.pdf mapping.html + + +images = sssd_01.png sssd_02.png sssd_03.png sssd_04.png sssd_05.png + +sssd_configuration.html: $(images) + +sssd_configuration.pdf: $(images) + +%.html: %.rst + rst2html $< $@ + +%.pdf: %.rst + rst2pdf --footer='-###Page###-' $< -o $@ + +%.png: %.svg + inkscape -z -e $@ -w 800 $< + +sssd_01.svg: sssd_01.diag + blockdiag -Tsvg $< + +sssd_02.svg: sssd_02.diag + blockdiag -Tsvg $< + +sssd_03.svg: sssd_03.diag + seqdiag -Tsvg $< + +sssd_04.svg: sssd_04.diag + blockdiag -Tsvg $< diff --git a/odl-aaa-moon/aaa/aaa-authn-api/src/main/docs/class_diagram.png b/odl-aaa-moon/aaa/aaa-authn-api/src/main/docs/class_diagram.png Binary files differnew file mode 100644 index 00000000..999a41f9 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn-api/src/main/docs/class_diagram.png diff --git a/odl-aaa-moon/aaa/aaa-authn-api/src/main/docs/class_diagram.ucls b/odl-aaa-moon/aaa/aaa-authn-api/src/main/docs/class_diagram.ucls new file mode 100644 index 00000000..68345256 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn-api/src/main/docs/class_diagram.ucls @@ -0,0 +1,127 @@ +<?xml version="1.0" encoding="UTF-8"?> +<class-diagram version="1.1.6" icons="true" automaticImage="PNG" always-add-relationships="false" generalizations="true" + realizations="true" associations="true" dependencies="true" nesting-relationships="true"> + <interface id="1" language="java" name="org.opendaylight.aaa.api.TokenStore" project="aaa-authn-api" + file="/aaa-authn-api/src/main/java/org/opendaylight/aaa/api/TokenStore.java" binary="false" corner="BOTTOM_RIGHT"> + <position height="-1" width="-1" x="637" y="568"/> + <display autosize="true" stereotype="true" package="true" initial-value="false" signature="true" accessors="true" + visibility="true"> + <attributes public="true" package="true" protected="true" private="false" static="true"/> + <operations public="true" package="true" protected="true" private="false" static="true"/> + </display> + </interface> + <interface id="2" language="java" name="org.opendaylight.aaa.api.AuthenticationService" project="aaa-authn-api" + file="/aaa-authn-api/src/main/java/org/opendaylight/aaa/api/AuthenticationService.java" binary="false" + corner="BOTTOM_RIGHT"> + <position height="-1" width="-1" x="385" y="727"/> + <display autosize="true" stereotype="true" package="true" initial-value="false" signature="true" accessors="true" + visibility="true"> + <attributes public="true" package="true" protected="true" private="false" static="true"/> + <operations public="true" package="true" protected="true" private="false" static="true"/> + </display> + </interface> + <interface id="3" language="java" name="org.opendaylight.aaa.api.CredentialAuth" project="aaa-authn-api" + file="/aaa-authn-api/src/main/java/org/opendaylight/aaa/api/CredentialAuth.java" binary="false" + corner="BOTTOM_RIGHT"> + <position height="-1" width="-1" x="148" y="94"/> + <display autosize="true" stereotype="true" package="true" initial-value="false" signature="true" accessors="true" + visibility="true"> + <attributes public="true" package="true" protected="true" private="false" static="true"/> + <operations public="true" package="true" protected="true" private="false" static="true"/> + </display> + </interface> + <interface id="4" language="java" name="org.opendaylight.aaa.api.TokenAuth" project="aaa-authn-api" + file="/aaa-authn-api/src/main/java/org/opendaylight/aaa/api/TokenAuth.java" binary="false" corner="BOTTOM_RIGHT"> + <position height="-1" width="-1" x="139" y="568"/> + <display autosize="true" stereotype="true" package="true" initial-value="false" signature="true" accessors="true" + visibility="true"> + <attributes public="true" package="true" protected="true" private="false" static="true"/> + <operations public="true" package="true" protected="true" private="false" static="true"/> + </display> + </interface> + <interface id="5" language="java" name="org.opendaylight.aaa.api.PasswordCredentials" project="aaa-authn-api" + file="/aaa-authn-api/src/main/java/org/opendaylight/aaa/api/PasswordCredentials.java" binary="false" + corner="BOTTOM_RIGHT"> + <position height="-1" width="-1" x="383" y="218"/> + <display autosize="true" stereotype="true" package="true" initial-value="false" signature="true" accessors="true" + visibility="true"> + <attributes public="true" package="true" protected="true" private="false" static="true"/> + <operations public="true" package="true" protected="true" private="false" static="true"/> + </display> + </interface> + <interface id="6" language="java" name="org.opendaylight.aaa.api.Credentials" project="aaa-authn-api" + file="/aaa-authn-api/src/main/java/org/opendaylight/aaa/api/Credentials.java" binary="false" corner="BOTTOM_RIGHT"> + <position height="-1" width="-1" x="385" y="93"/> + <display autosize="true" stereotype="true" package="true" initial-value="false" signature="true" accessors="true" + visibility="true"> + <attributes public="true" package="true" protected="true" private="false" static="true"/> + <operations public="true" package="true" protected="true" private="false" static="true"/> + </display> + </interface> + <interface id="7" language="java" name="org.opendaylight.aaa.api.Authentication" project="aaa-authn-api" + file="/aaa-authn-api/src/main/java/org/opendaylight/aaa/api/Authentication.java" binary="false" + corner="BOTTOM_RIGHT"> + <position height="-1" width="-1" x="386" y="567"/> + <display autosize="true" stereotype="true" package="true" initial-value="false" signature="true" accessors="true" + visibility="true"> + <attributes public="true" package="true" protected="true" private="false" static="true"/> + <operations public="true" package="true" protected="true" private="false" static="true"/> + </display> + </interface> + <interface id="8" language="java" name="org.opendaylight.aaa.api.ClaimAuth" project="aaa-authn-api" + file="/aaa-authn-api/src/main/java/org/opendaylight/aaa/api/ClaimAuth.java" binary="false" corner="BOTTOM_RIGHT"> + <position height="-1" width="-1" x="138" y="386"/> + <display autosize="true" stereotype="true" package="true" initial-value="false" signature="true" accessors="true" + visibility="true"> + <attributes public="true" package="true" protected="true" private="false" static="true"/> + <operations public="true" package="true" protected="true" private="false" static="true"/> + </display> + </interface> + <interface id="9" language="java" name="org.opendaylight.aaa.api.Claim" project="aaa-authn-api" + file="/aaa-authn-api/src/main/java/org/opendaylight/aaa/api/Claim.java" binary="false" corner="BOTTOM_RIGHT"> + <position height="-1" width="-1" x="386" y="386"/> + <display autosize="true" stereotype="true" package="true" initial-value="false" signature="true" accessors="true" + visibility="true"> + <attributes public="true" package="true" protected="true" private="false" static="true"/> + <operations public="true" package="true" protected="true" private="false" static="true"/> + </display> + </interface> + <dependency id="10"> + <end type="SOURCE" refId="3"/> + <end type="TARGET" refId="6"/> + </dependency> + <dependency id="11"> + <end type="SOURCE" refId="2"/> + <end type="TARGET" refId="7"/> + </dependency> + <generalization id="12"> + <end type="SOURCE" refId="5"/> + <end type="TARGET" refId="6"/> + </generalization> + <dependency id="13"> + <end type="SOURCE" refId="3"/> + <end type="TARGET" refId="9"/> + </dependency> + <generalization id="14"> + <end type="SOURCE" refId="7"/> + <end type="TARGET" refId="9"/> + </generalization> + <dependency id="15"> + <end type="SOURCE" refId="1"/> + <end type="TARGET" refId="7"/> + </dependency> + <dependency id="16"> + <end type="SOURCE" refId="8"/> + <end type="TARGET" refId="9"/> + </dependency> + <dependency id="17"> + <end type="SOURCE" refId="4"/> + <end type="TARGET" refId="7"/> + </dependency> + <classifier-display autosize="true" stereotype="true" package="true" initial-value="false" signature="true" + accessors="true" visibility="true"> + <attributes public="true" package="true" protected="true" private="false" static="true"/> + <operations public="true" package="true" protected="true" private="false" static="true"/> + </classifier-display> + <association-display labels="true" multiplicity="true"/> +</class-diagram>
\ No newline at end of file diff --git a/odl-aaa-moon/aaa/aaa-authn-api/src/main/docs/credential_auth_sequence.png b/odl-aaa-moon/aaa/aaa-authn-api/src/main/docs/credential_auth_sequence.png Binary files differnew file mode 100644 index 00000000..52d63650 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn-api/src/main/docs/credential_auth_sequence.png diff --git a/odl-aaa-moon/aaa/aaa-authn-api/src/main/docs/credential_auth_sequence.wsd b/odl-aaa-moon/aaa/aaa-authn-api/src/main/docs/credential_auth_sequence.wsd new file mode 100644 index 00000000..383d4031 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn-api/src/main/docs/credential_auth_sequence.wsd @@ -0,0 +1,18 @@ +title Credential Authentication Sequence + +# This walks through the credential authentication use case where a credential +# (typically username/password) is used to authenticate directly with the ODL +# controller. + +Client -> ServletContainer: request access token +note right of Client +(credentials, scope=domain) +end note +ServletContainer -> TokenEndpoint: credentials, domain +TokenEndpoint -> CredentialAuth: authenticate(Credentials, domain) +CredentialAuth -> TokenEndpoint: Claim +note left of CredentialAuth +(user/domain/roles) +end note +TokenEndpoint -> TokenEndpoint: createToken +TokenEndpoint -> Client: access token
\ No newline at end of file diff --git a/odl-aaa-moon/aaa/aaa-authn-api/src/main/docs/federated_auth_sequence.png b/odl-aaa-moon/aaa/aaa-authn-api/src/main/docs/federated_auth_sequence.png Binary files differnew file mode 100644 index 00000000..799cc909 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn-api/src/main/docs/federated_auth_sequence.png diff --git a/odl-aaa-moon/aaa/aaa-authn-api/src/main/docs/federated_auth_sequence.wsd b/odl-aaa-moon/aaa/aaa-authn-api/src/main/docs/federated_auth_sequence.wsd new file mode 100644 index 00000000..22d1d916 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn-api/src/main/docs/federated_auth_sequence.wsd @@ -0,0 +1,24 @@ +title Federated Authentication Sequence (w/ Claim Transformation) + +# This walks through the federated authentication sequence where a claim from a +# third-party IdP system is posted to the ODL token endpoint in exchange for an +# access token. The claim information is assumed to be in format specific to the +# third-party IdP system and assumed to be captured via either Apache environment +# variables (Servlet attributes) or HTTP headers. + +Client -> ServletContainer: request access token +note right of Client +(claim as Apache env/HTTP headers) +end note +ServletContainer -> ClaimAuthFilter: Servlet attributes/headers +loop foreach ClaimAuth + ClaimAuthFilter -> ClaimAuth: transform(Map<String, Object> claim) + ClaimAuth -> ClaimAuth: transformClaim +end +ClaimAuth -> ClaimAuthFilter: Claim +note left of ClaimAuth +(user/domain/roles) +end note +ClaimAuthFilter --> TokenEndpoint: Claim +TokenEndpoint -> TokenEndpoint: createToken +TokenEndpoint -> Client: access token
\ No newline at end of file diff --git a/odl-aaa-moon/aaa/aaa-authn-api/src/main/docs/mapping.rst b/odl-aaa-moon/aaa/aaa-authn-api/src/main/docs/mapping.rst new file mode 100644 index 00000000..33635502 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn-api/src/main/docs/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<String, Object> | ++-----------+------------+--------------------+---------------------+ +| array | ARRAY | list | List<Object> | ++-----------+------------+--------------------+---------------------+ +| 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<username>\\w+)@(?P<domain>.+)"], + ["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<username>\\w+)@(?P<domain>.+)"], + ["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/odl-aaa-moon/aaa/aaa-authn-api/src/main/docs/resource_access_sequence.png b/odl-aaa-moon/aaa/aaa-authn-api/src/main/docs/resource_access_sequence.png Binary files differnew file mode 100644 index 00000000..728b86ce --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn-api/src/main/docs/resource_access_sequence.png diff --git a/odl-aaa-moon/aaa/aaa-authn-api/src/main/docs/resource_access_sequence.wsd b/odl-aaa-moon/aaa/aaa-authn-api/src/main/docs/resource_access_sequence.wsd new file mode 100644 index 00000000..3a1c1474 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn-api/src/main/docs/resource_access_sequence.wsd @@ -0,0 +1,25 @@ +title Resource Access Sequence with Access Token + + This walks through a listing request of a secured resource (MD-SAL topology) + from a client to the ODL controller using an access token (either one generated + by the ODL token endpoint, or a token from a third-party IdP) and shows how the + authentication context get set upon successful token validation. If token + validation fails, the TokenAuthFilter will return a 401, and the REST layer + will be oblivious to the failed request. + +Client -> ServletContainer: list topologies +note right of Client +(Authorization = access token) +end note +ServletContainer -> TokenAuthFilter: access token +loop foreach TokenAuth + TokenAuthFilter -> TokenAuth: validate(token) + TokenAuth -> TokenAuth: validateToken +end +TokenAuth -> TokenAuthFilter: Authentication +note left of TokenAuth +(user/domain/roles/expiration) +end note +TokenAuthFilter -> AuthenticationService: set(Authentication) +TokenAuthFilter -> RestConf: list topologies +RestConf -> AuthenticationService: get: Authentication
\ No newline at end of file diff --git a/odl-aaa-moon/aaa/aaa-authn-api/src/main/docs/sssd_01.diag b/odl-aaa-moon/aaa/aaa-authn-api/src/main/docs/sssd_01.diag new file mode 100644 index 00000000..28317393 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn-api/src/main/docs/sssd_01.diag @@ -0,0 +1,6 @@ +blockdiag { + User <-> AAA; + User [numbered = 1, shape = actor] + AAA [numbered = 2, label = "App Server\nAAA"] +} + diff --git a/odl-aaa-moon/aaa/aaa-authn-api/src/main/docs/sssd_01.svg b/odl-aaa-moon/aaa/aaa-authn-api/src/main/docs/sssd_01.svg new file mode 100644 index 00000000..4056b10a --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn-api/src/main/docs/sssd_01.svg @@ -0,0 +1,32 @@ +<?xml version='1.0' encoding='UTF-8'?> +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd"> +<svg viewBox="0 0 448 120" xmlns="http://www.w3.org/2000/svg" xmlns:inkspace="http://www.inkscape.org/namespaces/inkscape" xmlns:xlink="http://www.w3.org/1999/xlink"> + <defs id="defs_block"> + <filter height="1.504" id="filter_blur" inkspace:collect="always" width="1.1575" x="-0.07875" y="-0.252"> + <feGaussianBlur id="feGaussianBlur3780" inkspace:collect="always" stdDeviation="4.2" /> + </filter> + </defs> + <title>blockdiag</title> + <desc>blockdiag { + User <-> AAA; + User [numbered = 1, shape = actor] + AAA [numbered = 2, label = "App Server\nAAA"] +} + +</desc> + <polygon fill="rgb(0,0,0)" points="134,56 134,61 151,61 151,66 134,66 134,71 148,86 141,86 131,76 121,86 114,86 128,71 128,66 111,66 111,61 128,61 128,56" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" /> + <ellipse cx="131" cy="51" fill="rgb(0,0,0)" rx="7" ry="7" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" /> + <rect fill="rgb(0,0,0)" height="40" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" width="128" x="259" y="46" /> + <polygon fill="rgb(255,255,255)" points="131,50 131,55 148,55 148,60 131,60 131,65 145,80 138,80 128,70 118,80 111,80 125,65 125,60 108,60 108,55 125,55 125,50" stroke="rgb(0,0,0)" /> + <ellipse cx="128" cy="45" fill="rgb(255,255,255)" rx="7" ry="7" stroke="rgb(0,0,0)" /> + <ellipse cx="64" cy="40" fill="pink" rx="12" ry="12" stroke="rgb(0,0,0)" /> + <text fill="rgb(0,0,0)" font-family="sansserif" font-size="11" font-style="normal" font-weight="normal" x="61" y="44">1</text> + <rect fill="rgb(255,255,255)" height="40" stroke="rgb(0,0,0)" width="128" x="256" y="40" /> + <text fill="rgb(0,0,0)" font-family="sansserif" font-size="11" font-style="normal" font-weight="normal" x="293" y="60">App Server</text> + <text fill="rgb(0,0,0)" font-family="sansserif" font-size="11" font-style="normal" font-weight="normal" x="310" y="70">AAA</text> + <ellipse cx="256" cy="40" fill="pink" rx="12" ry="12" stroke="rgb(0,0,0)" /> + <text fill="rgb(0,0,0)" font-family="sansserif" font-size="11" font-style="normal" font-weight="normal" x="253" y="44">2</text> + <path d="M 156 60 L 248 60" fill="none" stroke="rgb(0,0,0)" /> + <polygon fill="rgb(0,0,0)" points="149,60 156,56 156,64 149,60" stroke="rgb(0,0,0)" /> + <polygon fill="rgb(0,0,0)" points="255,60 248,56 248,64 255,60" stroke="rgb(0,0,0)" /> +</svg> diff --git a/odl-aaa-moon/aaa/aaa-authn-api/src/main/docs/sssd_02.diag b/odl-aaa-moon/aaa/aaa-authn-api/src/main/docs/sssd_02.diag new file mode 100644 index 00000000..2076dd16 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn-api/src/main/docs/sssd_02.diag @@ -0,0 +1,18 @@ +blockdiag { + span_width = 30 + User <-> Apache; + Proxy <-> AAA; + group { + Apache <-> Proxy; + group { + orientation = portrait + Apache <-> SSSD; + } + } + User [numbered = 1, shape = actor, width = 60] + Apache [numbered = 2, label = "Apache\nAuthenticates user"] + SSSD [numbered = 3, label = "SSSD\nProvides user info"] + Proxy [numbered = 4, label = "Proxy Transport\nRequest + Metadata"] + AAA [numbered = 5, label = "App Server\nAAA"] +} + diff --git a/odl-aaa-moon/aaa/aaa-authn-api/src/main/docs/sssd_02.svg b/odl-aaa-moon/aaa/aaa-authn-api/src/main/docs/sssd_02.svg new file mode 100644 index 00000000..42196b60 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn-api/src/main/docs/sssd_02.svg @@ -0,0 +1,79 @@ +<?xml version='1.0' encoding='UTF-8'?> +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd"> +<svg viewBox="0 0 594 200" xmlns="http://www.w3.org/2000/svg" xmlns:inkspace="http://www.inkscape.org/namespaces/inkscape" xmlns:xlink="http://www.w3.org/1999/xlink"> + <defs id="defs_block"> + <filter height="1.504" id="filter_blur" inkspace:collect="always" width="1.1575" x="-0.07875" y="-0.252"> + <feGaussianBlur id="feGaussianBlur3780" inkspace:collect="always" stdDeviation="4.2" /> + </filter> + </defs> + <title>blockdiag</title> + <desc>blockdiag { + span_width = 30 + User <-> Apache; + Proxy <-> AAA; + group { + Apache <-> Proxy; + group { + orientation = portrait + Apache <-> SSSD; + } + } + User [numbered = 1, shape = actor, width = 60] + Apache [numbered = 2, label = "Apache\nAuthenticates user"] + SSSD [numbered = 3, label = "SSSD\nProvides user info"] + Proxy [numbered = 4, label = "Proxy Transport\nRequest + Metadata"] + AAA [numbered = 5, label = "App Server\nAAA"] +} + +</desc> + <rect fill="rgb(243,152,0)" height="140" style="filter:url(#filter_blur)" width="292" x="117" y="30" /> + <rect fill="rgb(243,152,0)" height="140" style="filter:url(#filter_blur)" width="134" x="117" y="30" /> + <polygon fill="rgb(0,0,0)" points="66,56 66,61 83,61 83,66 66,66 66,71 80,86 73,86 63,76 53,86 46,86 60,71 60,66 43,66 43,61 60,61 60,56" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" /> + <ellipse cx="63" cy="51" fill="rgb(0,0,0)" rx="7" ry="7" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" /> + <rect fill="rgb(0,0,0)" height="40" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" width="128" x="123" y="46" /> + <rect fill="rgb(0,0,0)" height="40" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" width="128" x="123" y="126" /> + <rect fill="rgb(0,0,0)" height="40" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" width="128" x="281" y="46" /> + <rect fill="rgb(0,0,0)" height="40" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" width="128" x="439" y="46" /> + <polygon fill="rgb(255,255,255)" points="63,50 63,55 80,55 80,60 63,60 63,65 77,80 70,80 60,70 50,80 43,80 57,65 57,60 40,60 40,55 57,55 57,50" stroke="rgb(0,0,0)" /> + <ellipse cx="60" cy="45" fill="rgb(255,255,255)" rx="7" ry="7" stroke="rgb(0,0,0)" /> + <ellipse cx="30" cy="40" fill="pink" rx="12" ry="12" stroke="rgb(0,0,0)" /> + <text fill="rgb(0,0,0)" font-family="sansserif" font-size="11" font-style="normal" font-weight="normal" x="27" y="44">1</text> + <rect fill="rgb(255,255,255)" height="40" stroke="rgb(0,0,0)" width="128" x="120" y="40" /> + <text fill="rgb(0,0,0)" font-family="sansserif" font-size="11" font-style="normal" font-weight="normal" x="166" y="60">Apache</text> + <text fill="rgb(0,0,0)" font-family="sansserif" font-size="11" font-style="normal" font-weight="normal" x="133" y="70">Authenticates user</text> + <ellipse cx="120" cy="40" fill="pink" rx="12" ry="12" stroke="rgb(0,0,0)" /> + <text fill="rgb(0,0,0)" font-family="sansserif" font-size="11" font-style="normal" font-weight="normal" x="117" y="44">2</text> + <rect fill="rgb(255,255,255)" height="40" stroke="rgb(0,0,0)" width="128" x="120" y="120" /> + <text fill="rgb(0,0,0)" font-family="sansserif" font-size="11" font-style="normal" font-weight="normal" x="170" y="139">SSSD</text> + <text fill="rgb(0,0,0)" font-family="sansserif" font-size="11" font-style="normal" font-weight="normal" x="138" y="149">Provides user info</text> + <ellipse cx="120" cy="120" fill="pink" rx="12" ry="12" stroke="rgb(0,0,0)" /> + <text fill="rgb(0,0,0)" font-family="sansserif" font-size="11" font-style="normal" font-weight="normal" x="117" y="124">3</text> + <rect fill="rgb(255,255,255)" height="40" stroke="rgb(0,0,0)" width="128" x="278" y="40" /> + <text fill="rgb(0,0,0)" font-family="sansserif" font-size="11" font-style="normal" font-weight="normal" x="300" y="59">Proxy Transport</text> + <text fill="rgb(0,0,0)" font-family="sansserif" font-size="11" font-style="normal" font-weight="normal" x="289" y="71">Request + Metadata</text> + <ellipse cx="278" cy="40" fill="pink" rx="12" ry="12" stroke="rgb(0,0,0)" /> + <text fill="rgb(0,0,0)" font-family="sansserif" font-size="11" font-style="normal" font-weight="normal" x="275" y="44">4</text> + <rect fill="rgb(255,255,255)" height="40" stroke="rgb(0,0,0)" width="128" x="436" y="40" /> + <text fill="rgb(0,0,0)" font-family="sansserif" font-size="11" font-style="normal" font-weight="normal" x="473" y="60">App Server</text> + <text fill="rgb(0,0,0)" font-family="sansserif" font-size="11" font-style="normal" font-weight="normal" x="490" y="70">AAA</text> + <ellipse cx="436" cy="40" fill="pink" rx="12" ry="12" stroke="rgb(0,0,0)" /> + <text fill="rgb(0,0,0)" font-family="sansserif" font-size="11" font-style="normal" font-weight="normal" x="433" y="44">5</text> + <path d="M 88 60 L 112 60" fill="none" stroke="rgb(0,0,0)" /> + <polygon fill="rgb(0,0,0)" points="81,60 88,56 88,64 81,60" stroke="rgb(0,0,0)" /> + <polygon fill="rgb(0,0,0)" points="119,60 112,56 112,64 119,60" stroke="rgb(0,0,0)" /> + <path d="M 414 60 L 428 60" fill="none" stroke="rgb(0,0,0)" /> + <polygon fill="rgb(0,0,0)" points="407,60 414,56 414,64 407,60" stroke="rgb(0,0,0)" /> + <polygon fill="rgb(0,0,0)" points="435,60 428,56 428,64 435,60" stroke="rgb(0,0,0)" /> + <path d="M 184 88 L 184 112" fill="none" stroke="rgb(0,0,0)" /> + <polygon fill="rgb(0,0,0)" points="184,81 180,88 188,88 184,81" stroke="rgb(0,0,0)" /> + <polygon fill="rgb(0,0,0)" points="184,119 180,112 188,112 184,119" stroke="rgb(0,0,0)" /> + <path d="M 256 60 L 270 60" fill="none" stroke="rgb(0,0,0)" /> + <polygon fill="rgb(0,0,0)" points="249,60 256,56 256,64 249,60" stroke="rgb(0,0,0)" /> + <polygon fill="rgb(0,0,0)" points="277,60 270,56 270,64 277,60" stroke="rgb(0,0,0)" /> + <path d="M 184 88 L 184 112" fill="none" stroke="rgb(0,0,0)" /> + <polygon fill="rgb(0,0,0)" points="184,81 180,88 188,88 184,81" stroke="rgb(0,0,0)" /> + <polygon fill="rgb(0,0,0)" points="184,119 180,112 188,112 184,119" stroke="rgb(0,0,0)" /> + <path d="M 256 60 L 270 60" fill="none" stroke="rgb(0,0,0)" /> + <polygon fill="rgb(0,0,0)" points="249,60 256,56 256,64 249,60" stroke="rgb(0,0,0)" /> + <polygon fill="rgb(0,0,0)" points="277,60 270,56 270,64 277,60" stroke="rgb(0,0,0)" /> +</svg> diff --git a/odl-aaa-moon/aaa/aaa-authn-api/src/main/docs/sssd_03.diag b/odl-aaa-moon/aaa/aaa-authn-api/src/main/docs/sssd_03.diag new file mode 100644 index 00000000..6ece3760 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn-api/src/main/docs/sssd_03.diag @@ -0,0 +1,31 @@ +seqdiag { + // Set edge properties + //edge_length = 300; // default value is 192 + //span_height = 80; // default value is 40 + + // Set fontsize. + //default_fontsize = 12; // default value is 11 + + // Numbering edges automaticaly + autonumber = False; + + // Change note color + default_note_color = lightblue; + + Client -> Apache [label = "Request"]; + === Apache mod_auth_kerb === + Client <- Apache [label = "401 Unauthorized"]; + Client -> Apache [label = "Authorization: Credentials"]; + Apache -> Apache [label = "Set\nUser Name\nAuth Type"]; + === Apache mod_lookup_identity === + Apache -> SSSD [label = "Get User Info"]; + SSSD --> IdP [label = "Get User Info", leftnote = "Only if\nnot cached\nby SSSD"]; + SSSD <-- IdP [label = "Return User Info"]; + Apache <- SSSD [label = "Return User Info"]; + Apache -> Apache [label = "Set User specific\nenvironment\nvariables"]; + === Apache mod_proxy === + Apache -> Container [label = "Proxy With User's Metadata"]; + Apache <- Container [label = "Response"]; + Client <- Apache [label = "Response"]; + +} diff --git a/odl-aaa-moon/aaa/aaa-authn-api/src/main/docs/sssd_03.svg b/odl-aaa-moon/aaa/aaa-authn-api/src/main/docs/sssd_03.svg new file mode 100644 index 00000000..91e8b1be --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn-api/src/main/docs/sssd_03.svg @@ -0,0 +1,143 @@ +<?xml version='1.0' encoding='UTF-8'?> +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd"> +<svg viewBox="0 0 1024 1227" xmlns="http://www.w3.org/2000/svg" xmlns:inkspace="http://www.inkscape.org/namespaces/inkscape" xmlns:xlink="http://www.w3.org/1999/xlink"> + <defs id="defs_block"> + <filter height="1.504" id="filter_blur" inkspace:collect="always" width="1.1575" x="-0.07875" y="-0.252"> + <feGaussianBlur id="feGaussianBlur3780" inkspace:collect="always" stdDeviation="4.2" /> + </filter> + </defs> + <title>blockdiag</title> + <desc>seqdiag { + // Set edge properties + //edge_length = 300; // default value is 192 + //span_height = 80; // default value is 40 + + // Set fontsize. + //default_fontsize = 12; // default value is 11 + + // Numbering edges automaticaly + autonumber = False; + + // Change note color + default_note_color = lightblue; + + Client -> Apache [label = "Request"]; + === Apache mod_auth_kerb === + Client <- Apache [label = "401 Unauthorized"]; + Client -> Apache [label = "Authorization: Credentials"]; + Apache -> Apache [label = "Set\nUser Name\nAuth Type"]; + === Apache mod_lookup_identity === + Apache -> SSSD [label = "Get User Info"]; + SSSD --> IdP [label = "Get User Info", leftnote = "Only if\nnot cached\nby SSSD"]; + SSSD <-- IdP [label = "Return User Info"]; + Apache <- SSSD [label = "Return User Info"]; + Apache -> Apache [label = "Set User specific\nenvironment\nvariables"]; + === Apache mod_proxy === + Apache -> Container [label = "Proxy With User's Metadata"]; + Apache <- Container [label = "Response"]; + Client <- Apache [label = "Response"]; + +} +</desc> + <rect fill="rgb(0,0,0)" height="1065" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" width="8" x="127" y="140" /> + <rect fill="rgb(0,0,0)" height="142" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" width="8" x="319" y="140" /> + <rect fill="rgb(0,0,0)" height="815" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" width="8" x="319" y="344" /> + <rect fill="rgb(0,0,0)" height="200" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" width="8" x="511" y="586" /> + <rect fill="rgb(0,0,0)" height="70" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" width="8" x="703" y="654" /> + <rect fill="rgb(0,0,0)" height="64" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" width="8" x="895" y="1031" /> + <polygon fill="rgb(0,0,0)" points="420,636 491,636 499,644 499,672 420,672 420,636" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" /> + <rect fill="rgb(0,0,0)" height="40" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" width="128" x="67" y="46" /> + <rect fill="rgb(0,0,0)" height="40" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" width="128" x="259" y="46" /> + <rect fill="rgb(0,0,0)" height="40" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" width="128" x="451" y="46" /> + <rect fill="rgb(0,0,0)" height="40" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" width="128" x="643" y="46" /> + <rect fill="rgb(0,0,0)" height="40" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" width="128" x="835" y="46" /> + <path d="M 128 80 L 128 1215" fill="none" stroke="rgb(0,0,0)" stroke-dasharray="8 4" /> + <rect fill="moccasin" height="1065" stroke="rgb(0,0,0)" width="8" x="124" y="134" /> + <path d="M 320 80 L 320 1215" fill="none" stroke="rgb(0,0,0)" stroke-dasharray="8 4" /> + <rect fill="moccasin" height="142" stroke="rgb(0,0,0)" width="8" x="316" y="134" /> + <rect fill="moccasin" height="815" stroke="rgb(0,0,0)" width="8" x="316" y="338" /> + <path d="M 512 80 L 512 1215" fill="none" stroke="rgb(0,0,0)" stroke-dasharray="8 4" /> + <rect fill="moccasin" height="200" stroke="rgb(0,0,0)" width="8" x="508" y="580" /> + <path d="M 704 80 L 704 1215" fill="none" stroke="rgb(0,0,0)" stroke-dasharray="8 4" /> + <rect fill="moccasin" height="70" stroke="rgb(0,0,0)" width="8" x="700" y="648" /> + <path d="M 896 80 L 896 1215" fill="none" stroke="rgb(0,0,0)" stroke-dasharray="8 4" /> + <rect fill="moccasin" height="64" stroke="rgb(0,0,0)" width="8" x="892" y="1025" /> + <rect fill="rgb(255,255,255)" height="40" stroke="rgb(0,0,0)" width="128" x="64" y="40" /> + <text fill="rgb(0,0,0)" font-family="sansserif" font-size="11" font-style="normal" font-weight="normal" x="113" y="64">Client</text> + <rect fill="rgb(255,255,255)" height="40" stroke="rgb(0,0,0)" width="128" x="256" y="40" /> + <text fill="rgb(0,0,0)" font-family="sansserif" font-size="11" font-style="normal" font-weight="normal" x="302" y="65">Apache</text> + <rect fill="rgb(255,255,255)" height="40" stroke="rgb(0,0,0)" width="128" x="448" y="40" /> + <text fill="rgb(0,0,0)" font-family="sansserif" font-size="11" font-style="normal" font-weight="normal" x="498" y="64">SSSD</text> + <rect fill="rgb(255,255,255)" height="40" stroke="rgb(0,0,0)" width="128" x="640" y="40" /> + <text fill="rgb(0,0,0)" font-family="sansserif" font-size="11" font-style="normal" font-weight="normal" x="697" y="64">IdP</text> + <rect fill="rgb(255,255,255)" height="40" stroke="rgb(0,0,0)" width="128" x="832" y="40" /> + <text fill="rgb(0,0,0)" font-family="sansserif" font-size="11" font-style="normal" font-weight="normal" x="871" y="64">Container</text> + <path d="M 136 134 L 312 134" fill="none" stroke="rgb(0,0,0)" /> + <polygon fill="rgb(0,0,0)" points="304,130 312,134 304,138" stroke="rgb(0,0,0)" /> + <path d="M 136 276 L 312 276" fill="none" stroke="rgb(0,0,0)" /> + <polygon fill="rgb(0,0,0)" points="144,272 136,276 144,280" stroke="rgb(0,0,0)" /> + <path d="M 136 338 L 312 338" fill="none" stroke="rgb(0,0,0)" /> + <polygon fill="rgb(0,0,0)" points="304,334 312,338 304,342" stroke="rgb(0,0,0)" /> + <path d="M 328 422 L 416 422" fill="none" stroke="rgb(0,0,0)" /> + <path d="M 416 422 L 416 438" fill="none" stroke="rgb(0,0,0)" /> + <path d="M 416 438 L 328 438" fill="none" stroke="rgb(0,0,0)" /> + <polygon fill="rgb(0,0,0)" points="336,434 328,438 336,442" stroke="rgb(0,0,0)" /> + <path d="M 328 580 L 504 580" fill="none" stroke="rgb(0,0,0)" /> + <polygon fill="rgb(0,0,0)" points="496,576 504,580 496,584" stroke="rgb(0,0,0)" /> + <path d="M 520 648 L 696 648" fill="none" stroke="rgb(0,0,0)" stroke-dasharray="4" /> + <polygon fill="rgb(0,0,0)" points="688,644 696,648 688,652" stroke="rgb(0,0,0)" /> + <polygon fill="rgb(173,216,230)" points="417,630 488,630 496,638 496,666 417,666 417,630" stroke="rgb(0,0,0)" /> + <path d="M 488 630 L 488 638" fill="none" stroke="rgb(0,0,0)" /> + <path d="M 488 638 L 496 638" fill="none" stroke="rgb(0,0,0)" /> + <text fill="rgb(0,0,0)" font-family="sansserif" font-size="11" font-style="normal" font-weight="normal" x="425" y="642">Only if</text> + <text fill="rgb(0,0,0)" font-family="sansserif" font-size="11" font-style="normal" font-weight="normal" x="425" y="652">not cached</text> + <text fill="rgb(0,0,0)" font-family="sansserif" font-size="11" font-style="normal" font-weight="normal" x="425" y="664">by SSSD</text> + <path d="M 520 718 L 696 718" fill="none" stroke="rgb(0,0,0)" stroke-dasharray="4" /> + <polygon fill="rgb(0,0,0)" points="528,714 520,718 528,722" stroke="rgb(0,0,0)" /> + <path d="M 328 780 L 504 780" fill="none" stroke="rgb(0,0,0)" /> + <polygon fill="rgb(0,0,0)" points="336,776 328,780 336,784" stroke="rgb(0,0,0)" /> + <path d="M 328 864 L 416 864" fill="none" stroke="rgb(0,0,0)" /> + <path d="M 416 864 L 416 880" fill="none" stroke="rgb(0,0,0)" /> + <path d="M 416 880 L 328 880" fill="none" stroke="rgb(0,0,0)" /> + <polygon fill="rgb(0,0,0)" points="336,876 328,880 336,884" stroke="rgb(0,0,0)" /> + <path d="M 328 1025 L 888 1025" fill="none" stroke="rgb(0,0,0)" /> + <polygon fill="rgb(0,0,0)" points="880,1021 888,1025 880,1029" stroke="rgb(0,0,0)" /> + <path d="M 328 1089 L 888 1089" fill="none" stroke="rgb(0,0,0)" /> + <polygon fill="rgb(0,0,0)" points="336,1085 328,1089 336,1093" stroke="rgb(0,0,0)" /> + <path d="M 136 1153 L 312 1153" fill="none" stroke="rgb(0,0,0)" /> + <polygon fill="rgb(0,0,0)" points="144,1149 136,1153 144,1157" stroke="rgb(0,0,0)" /> + <text fill="rgb(0,0,0)" font-family="sansserif" font-size="11" font-style="normal" font-weight="normal" x="140" y="132">Request</text> + <text fill="rgb(0,0,0)" font-family="sansserif" font-size="11" font-style="normal" font-weight="normal" x="217" y="274">401 Unauthorized</text> + <text fill="rgb(0,0,0)" font-family="sansserif" font-size="11" font-style="normal" font-weight="normal" x="140" y="336">Authorization: Credentials</text> + <text fill="rgb(0,0,0)" font-family="sansserif" font-size="11" font-style="normal" font-weight="normal" x="328" y="398">Set</text> + <text fill="rgb(0,0,0)" font-family="sansserif" font-size="11" font-style="normal" font-weight="normal" x="328" y="408">User Name</text> + <text fill="rgb(0,0,0)" font-family="sansserif" font-size="11" font-style="normal" font-weight="normal" x="328" y="420">Auth Type</text> + <text fill="rgb(0,0,0)" font-family="sansserif" font-size="11" font-style="normal" font-weight="normal" x="332" y="578">Get User Info</text> + <text fill="rgb(0,0,0)" font-family="sansserif" font-size="11" font-style="normal" font-weight="normal" x="524" y="646">Get User Info</text> + <text fill="rgb(0,0,0)" font-family="sansserif" font-size="11" font-style="normal" font-weight="normal" x="608" y="716">Return User Info</text> + <text fill="rgb(0,0,0)" font-family="sansserif" font-size="11" font-style="normal" font-weight="normal" x="416" y="778">Return User Info</text> + <text fill="rgb(0,0,0)" font-family="sansserif" font-size="11" font-style="normal" font-weight="normal" x="328" y="842">Set User specific</text> + <text fill="rgb(0,0,0)" font-family="sansserif" font-size="11" font-style="normal" font-weight="normal" x="328" y="852">environment</text> + <text fill="rgb(0,0,0)" font-family="sansserif" font-size="11" font-style="normal" font-weight="normal" x="328" y="862">variables</text> + <text fill="rgb(0,0,0)" font-family="sansserif" font-size="11" font-style="normal" font-weight="normal" x="332" y="1023">Proxy With User's Metadata</text> + <text fill="rgb(0,0,0)" font-family="sansserif" font-size="11" font-style="normal" font-weight="normal" x="841" y="1087">Response</text> + <text fill="rgb(0,0,0)" font-family="sansserif" font-size="11" font-style="normal" font-weight="normal" x="265" y="1151">Response</text> + <path d="M 40 202 L 442 202" fill="none" stroke="rgb(0,0,0)" /> + <path d="M 40 206 L 442 206" fill="none" stroke="rgb(0,0,0)" /> + <path d="M 581 202 L 984 202" fill="none" stroke="rgb(0,0,0)" /> + <path d="M 581 206 L 984 206" fill="none" stroke="rgb(0,0,0)" /> + <rect fill="rgb(208,208,208)" height="18" stroke="rgb(0,0,0)" width="139" x="442" y="195" /> + <text fill="rgb(0,0,0)" font-family="sansserif" font-size="11" font-style="normal" font-weight="normal" x="452" y="209">Apache mod_auth_kerb</text> + <path d="M 40 506 L 429 506" fill="none" stroke="rgb(0,0,0)" /> + <path d="M 40 510 L 429 510" fill="none" stroke="rgb(0,0,0)" /> + <path d="M 594 506 L 984 506" fill="none" stroke="rgb(0,0,0)" /> + <path d="M 594 510 L 984 510" fill="none" stroke="rgb(0,0,0)" /> + <rect fill="rgb(208,208,208)" height="18" stroke="rgb(0,0,0)" width="165" x="429" y="499" /> + <text fill="rgb(0,0,0)" font-family="sansserif" font-size="11" font-style="normal" font-weight="normal" x="439" y="513">Apache mod_lookup_identity</text> + <path d="M 40 948 L 455 948" fill="none" stroke="rgb(0,0,0)" /> + <path d="M 40 952 L 455 952" fill="none" stroke="rgb(0,0,0)" /> + <path d="M 568 948 L 984 948" fill="none" stroke="rgb(0,0,0)" /> + <path d="M 568 952 L 984 952" fill="none" stroke="rgb(0,0,0)" /> + <rect fill="rgb(208,208,208)" height="18" stroke="rgb(0,0,0)" width="113" x="455" y="941" /> + <text fill="rgb(0,0,0)" font-family="sansserif" font-size="11" font-style="normal" font-weight="normal" x="465" y="955">Apache mod_proxy</text> +</svg> diff --git a/odl-aaa-moon/aaa/aaa-authn-api/src/main/docs/sssd_04.diag b/odl-aaa-moon/aaa/aaa-authn-api/src/main/docs/sssd_04.diag new file mode 100644 index 00000000..8f69a0b8 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn-api/src/main/docs/sssd_04.diag @@ -0,0 +1,25 @@ +blockdiag { + Connector -> SssdFilter; + SssdFilter -> ClaimAuthFilter; + ClaimAuthFilter -> SssdClaimAuth; + SssdClaimAuth -> Assertion [folded]; + + group { + orientation = portrait + Assertion -> JsonAssertion; + JsonAssertion -> IdPMapper; + IdPMapper -> JsonMapped; + } + + JsonMapped -> Claim; + + Connector [numbered = 1] + SssdFilter [numbered = 2] + ClaimAuthFilter [numbered = 3] + SssdClaimAuth [numbered = 4] + Assertion [numbered = 4.1] + JsonAssertion [numbered = 4.2] + IdPMapper [numbered = 4.3] + JsonMapped [numbered = 4.4] + Claim [numbered = 5] +} diff --git a/odl-aaa-moon/aaa/aaa-authn-api/src/main/docs/sssd_04.svg b/odl-aaa-moon/aaa/aaa-authn-api/src/main/docs/sssd_04.svg new file mode 100644 index 00000000..74850a85 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn-api/src/main/docs/sssd_04.svg @@ -0,0 +1,100 @@ +<?xml version='1.0' encoding='UTF-8'?> +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd"> +<svg viewBox="0 0 832 440" xmlns="http://www.w3.org/2000/svg" xmlns:inkspace="http://www.inkscape.org/namespaces/inkscape" xmlns:xlink="http://www.w3.org/1999/xlink"> + <defs id="defs_block"> + <filter height="1.504" id="filter_blur" inkspace:collect="always" width="1.1575" x="-0.07875" y="-0.252"> + <feGaussianBlur id="feGaussianBlur3780" inkspace:collect="always" stdDeviation="4.2" /> + </filter> + </defs> + <title>blockdiag</title> + <desc>blockdiag { + Connector -> SssdFilter; + SssdFilter -> ClaimAuthFilter; + ClaimAuthFilter -> SssdClaimAuth; + SssdClaimAuth -> Assertion [folded]; + + group { + orientation = portrait + Assertion -> JsonAssertion; + JsonAssertion -> IdPMapper; + IdPMapper -> JsonMapped; + } + + JsonMapped -> Claim; + + Connector [numbered = 1] + SssdFilter [numbered = 2] + ClaimAuthFilter [numbered = 3] + SssdClaimAuth [numbered = 4] + Assertion [numbered = 4.1] + JsonAssertion [numbered = 4.2] + IdPMapper [numbered = 4.3] + JsonMapped [numbered = 4.4] + Claim [numbered = 5] +} +</desc> + <rect fill="rgb(243,152,0)" height="300" style="filter:url(#filter_blur)" width="144" x="56" y="110" /> + <rect fill="rgb(0,0,0)" height="40" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" width="128" x="67" y="46" /> + <rect fill="rgb(0,0,0)" height="40" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" width="128" x="259" y="46" /> + <rect fill="rgb(0,0,0)" height="40" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" width="128" x="451" y="46" /> + <rect fill="rgb(0,0,0)" height="40" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" width="128" x="643" y="46" /> + <rect fill="rgb(0,0,0)" height="40" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" width="128" x="67" y="126" /> + <rect fill="rgb(0,0,0)" height="40" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" width="128" x="67" y="206" /> + <rect fill="rgb(0,0,0)" height="40" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" width="128" x="67" y="286" /> + <rect fill="rgb(0,0,0)" height="40" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" width="128" x="67" y="366" /> + <rect fill="rgb(0,0,0)" height="40" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" width="128" x="259" y="366" /> + <rect fill="rgb(255,255,255)" height="40" stroke="rgb(0,0,0)" width="128" x="64" y="40" /> + <text fill="rgb(0,0,0)" font-family="sansserif" font-size="11" font-style="normal" font-weight="normal" x="103" y="64">Connector</text> + <ellipse cx="64" cy="40" fill="pink" rx="12" ry="12" stroke="rgb(0,0,0)" /> + <text fill="rgb(0,0,0)" font-family="sansserif" font-size="11" font-style="normal" font-weight="normal" x="61" y="44">1</text> + <rect fill="rgb(255,255,255)" height="40" stroke="rgb(0,0,0)" width="128" x="256" y="40" /> + <text fill="rgb(0,0,0)" font-family="sansserif" font-size="11" font-style="normal" font-weight="normal" x="294" y="64">SssdFilter</text> + <ellipse cx="256" cy="40" fill="pink" rx="12" ry="12" stroke="rgb(0,0,0)" /> + <text fill="rgb(0,0,0)" font-family="sansserif" font-size="11" font-style="normal" font-weight="normal" x="253" y="44">2</text> + <rect fill="rgb(255,255,255)" height="40" stroke="rgb(0,0,0)" width="128" x="448" y="40" /> + <text fill="rgb(0,0,0)" font-family="sansserif" font-size="11" font-style="normal" font-weight="normal" x="471" y="64">ClaimAuthFilter</text> + <ellipse cx="448" cy="40" fill="pink" rx="12" ry="12" stroke="rgb(0,0,0)" /> + <text fill="rgb(0,0,0)" font-family="sansserif" font-size="11" font-style="normal" font-weight="normal" x="445" y="44">3</text> + <rect fill="rgb(255,255,255)" height="40" stroke="rgb(0,0,0)" width="128" x="640" y="40" /> + <text fill="rgb(0,0,0)" font-family="sansserif" font-size="11" font-style="normal" font-weight="normal" x="665" y="64">SssdClaimAuth</text> + <ellipse cx="640" cy="40" fill="pink" rx="12" ry="12" stroke="rgb(0,0,0)" /> + <text fill="rgb(0,0,0)" font-family="sansserif" font-size="11" font-style="normal" font-weight="normal" x="637" y="44">4</text> + <rect fill="rgb(255,255,255)" height="40" stroke="rgb(0,0,0)" width="128" x="64" y="120" /> + <text fill="rgb(0,0,0)" font-family="sansserif" font-size="11" font-style="normal" font-weight="normal" x="103" y="144">Assertion</text> + <ellipse cx="64" cy="120" fill="pink" rx="12" ry="12" stroke="rgb(0,0,0)" /> + <text fill="rgb(0,0,0)" font-family="sansserif" font-size="11" font-style="normal" font-weight="normal" x="56" y="124">4.1</text> + <rect fill="rgb(255,255,255)" height="40" stroke="rgb(0,0,0)" width="128" x="64" y="200" /> + <text fill="rgb(0,0,0)" font-family="sansserif" font-size="11" font-style="normal" font-weight="normal" x="91" y="224">JsonAssertion</text> + <ellipse cx="64" cy="200" fill="pink" rx="12" ry="12" stroke="rgb(0,0,0)" /> + <text fill="rgb(0,0,0)" font-family="sansserif" font-size="11" font-style="normal" font-weight="normal" x="56" y="204">4.2</text> + <rect fill="rgb(255,255,255)" height="40" stroke="rgb(0,0,0)" width="128" x="64" y="280" /> + <text fill="rgb(0,0,0)" font-family="sansserif" font-size="11" font-style="normal" font-weight="normal" x="102" y="305">IdPMapper</text> + <ellipse cx="64" cy="280" fill="pink" rx="12" ry="12" stroke="rgb(0,0,0)" /> + <text fill="rgb(0,0,0)" font-family="sansserif" font-size="11" font-style="normal" font-weight="normal" x="56" y="284">4.3</text> + <rect fill="rgb(255,255,255)" height="40" stroke="rgb(0,0,0)" width="128" x="64" y="360" /> + <text fill="rgb(0,0,0)" font-family="sansserif" font-size="11" font-style="normal" font-weight="normal" x="97" y="385">JsonMapped</text> + <ellipse cx="64" cy="360" fill="pink" rx="12" ry="12" stroke="rgb(0,0,0)" /> + <text fill="rgb(0,0,0)" font-family="sansserif" font-size="11" font-style="normal" font-weight="normal" x="56" y="364">4.4</text> + <rect fill="rgb(255,255,255)" height="40" stroke="rgb(0,0,0)" width="128" x="256" y="360" /> + <text fill="rgb(0,0,0)" font-family="sansserif" font-size="11" font-style="normal" font-weight="normal" x="307" y="384">Claim</text> + <ellipse cx="256" cy="360" fill="pink" rx="12" ry="12" stroke="rgb(0,0,0)" /> + <text fill="rgb(0,0,0)" font-family="sansserif" font-size="11" font-style="normal" font-weight="normal" x="253" y="364">5</text> + <path d="M 192 60 L 248 60" fill="none" stroke="rgb(0,0,0)" /> + <polygon fill="rgb(0,0,0)" points="255,60 248,56 248,64 255,60" stroke="rgb(0,0,0)" /> + <path d="M 384 60 L 440 60" fill="none" stroke="rgb(0,0,0)" /> + <polygon fill="rgb(0,0,0)" points="447,60 440,56 440,64 447,60" stroke="rgb(0,0,0)" /> + <path d="M 576 60 L 632 60" fill="none" stroke="rgb(0,0,0)" /> + <polygon fill="rgb(0,0,0)" points="639,60 632,56 632,64 639,60" stroke="rgb(0,0,0)" /> + <path d="M 704 80 L 704 100" fill="none" stroke="rgb(0,0,0)" /> + <path d="M 128 100 L 704 100" fill="none" stroke="rgb(0,0,0)" /> + <path d="M 128 100 L 128 112" fill="none" stroke="rgb(0,0,0)" /> + <polygon fill="rgb(0,0,0)" points="128,119 124,112 132,112 128,119" stroke="rgb(0,0,0)" /> + <path d="M 128 160 L 128 192" fill="none" stroke="rgb(0,0,0)" /> + <polygon fill="rgb(0,0,0)" points="128,199 124,192 132,192 128,199" stroke="rgb(0,0,0)" /> + <path d="M 128 240 L 128 272" fill="none" stroke="rgb(0,0,0)" /> + <polygon fill="rgb(0,0,0)" points="128,279 124,272 132,272 128,279" stroke="rgb(0,0,0)" /> + <path d="M 128 320 L 128 352" fill="none" stroke="rgb(0,0,0)" /> + <polygon fill="rgb(0,0,0)" points="128,359 124,352 132,352 128,359" stroke="rgb(0,0,0)" /> + <path d="M 192 380 L 248 380" fill="none" stroke="rgb(0,0,0)" /> + <polygon fill="rgb(0,0,0)" points="255,380 248,376 248,384 255,380" stroke="rgb(0,0,0)" /> +</svg> diff --git a/odl-aaa-moon/aaa/aaa-authn-api/src/main/docs/sssd_05.svg b/odl-aaa-moon/aaa/aaa-authn-api/src/main/docs/sssd_05.svg new file mode 100644 index 00000000..f4657f06 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn-api/src/main/docs/sssd_05.svg @@ -0,0 +1,613 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg + xmlns:osb="http://www.openswatchbook.org/uri/2009/osb" + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + width="689.19269" + height="212.05057" + id="svg2" + version="1.1" + inkscape:version="0.48.5 r10040" + sodipodi:docname="sssd_05.svg"> + <defs + id="defs4"> + <linearGradient + inkscape:collect="always" + id="linearGradient12785" + osb:paint="gradient"> + <stop + style="stop-color:#000000;stop-opacity:1;" + offset="0" + id="stop12787" /> + <stop + style="stop-color:#000000;stop-opacity:0;" + offset="1" + id="stop12789" /> + </linearGradient> + <linearGradient + id="linearGradient12777"> + <stop + style="stop-color:#ffcc00;stop-opacity:1;" + offset="0" + id="stop12779" /> + <stop + style="stop-color:#ffcc00;stop-opacity:0;" + offset="1" + id="stop12781" /> + </linearGradient> + <marker + inkscape:stockid="Scissors" + orient="auto" + refY="0" + refX="0" + id="Scissors" + style="overflow:visible"> + <path + id="schere" + d="M 9.0898857,-3.6061018 C 8.1198849,-4.7769976 6.3697607,-4.7358294 5.0623558,-4.2327734 l -8.2124046,3.0779029 c -2.3882933,-1.3067135 -4.7482873,-0.9325372 -4.7482873,-1.5687873 0,-0.4973164 0.4566662,-0.3883222 0.3883068,-1.6831941 -0.065635,-1.2432767 -1.3635771,-2.1630796 -2.5903987,-2.0816435 -1.227271,-0.00735 -2.499439,0.9331613 -2.510341,2.2300611 -0.09143,1.3063864 1.007209,2.5196896 2.306764,2.6052316 1.5223406,0.2266616 4.218258,-0.6955566 5.482945,1.57086006 -0.9422847,1.73825774 -2.6140244,1.74307674 -4.1255107,1.65607034 -1.2548743,-0.072235 -2.7620933,0.2873979 -3.3606483,1.5208605 -0.578367,1.1820862 -0.0112,2.8646022 1.316749,3.226412 1.3401912,0.4918277 3.1806689,-0.129711 3.4993722,-1.6707242 0.2456585,-1.187823 -0.5953659,-1.7459574 -0.2725074,-2.1771537 0.2436135,-0.32536 1.7907806,-0.1368452 4.5471053,-1.3748244 L 5.6763468,4.2330688 C 6.8000164,4.5467672 8.1730685,4.5362646 9.1684433,3.4313614 L -0.05164093,-0.05372222 9.0898857,-3.6061018 z m -18.3078016,-1.900504 c 1.294559,0.7227998 1.1888392,2.6835702 -0.1564272,3.0632889 -1.2165179,0.423661 -2.7710269,-0.7589694 -2.3831779,-2.0774648 0.227148,-1.0818519 1.653387,-1.480632 2.5396051,-0.9858241 z m 0.056264,8.0173649 c 1.3508301,0.4988648 1.1214429,2.7844356 -0.2522207,3.091609 -0.9110594,0.3163391 -2.2135494,-0.1387976 -2.3056964,-1.2121394 -0.177609,-1.305055 1.356085,-2.4841482 2.5579171,-1.8794696 z" + style="fill:#000000" + inkscape:connector-curvature="0" /> + </marker> + <marker + inkscape:stockid="DotL" + orient="auto" + refY="0" + refX="0" + id="DotL" + style="overflow:visible"> + <path + id="path4170" + d="m -2.5,-1 c 0,2.76 -2.24,5 -5,5 -2.76,0 -5,-2.24 -5,-5 0,-2.76 2.24,-5 5,-5 2.76,0 5,2.24 5,5 z" + style="fill-rule:evenodd;stroke:#000000;stroke-width:1pt" + transform="matrix(0.8,0,0,0.8,5.92,0.8)" + inkscape:connector-curvature="0" /> + </marker> + <marker + inkscape:stockid="StopL" + orient="auto" + refY="0" + refX="0" + id="StopL" + style="overflow:visible"> + <path + id="path4278" + d="M 0,5.65 0,-5.65" + style="fill:none;stroke:#000000;stroke-width:1pt" + transform="scale(0.8,0.8)" + inkscape:connector-curvature="0" /> + </marker> + <marker + inkscape:stockid="Arrow2Mstart" + orient="auto" + refY="0" + refX="0" + id="Arrow2Mstart" + style="overflow:visible"> + <path + id="path4133" + style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round" + d="M 8.7185878,4.0337352 -2.2072895,0.01601326 8.7185884,-4.0017078 c -1.7454984,2.3720609 -1.7354408,5.6174519 -6e-7,8.035443 z" + transform="scale(0.6,0.6)" + inkscape:connector-curvature="0" /> + </marker> + <marker + inkscape:stockid="Arrow2Mend" + orient="auto" + refY="0" + refX="0" + id="Arrow2Mend" + style="overflow:visible"> + <path + id="path4136" + style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round" + d="M 8.7185878,4.0337352 -2.2072895,0.01601326 8.7185884,-4.0017078 c -1.7454984,2.3720609 -1.7354408,5.6174519 -6e-7,8.035443 z" + transform="scale(-0.6,-0.6)" + inkscape:connector-curvature="0" /> + </marker> + <marker + inkscape:stockid="Arrow1Mend" + orient="auto" + refY="0" + refX="0" + id="Arrow1Mend" + style="overflow:visible"> + <path + id="path4118" + d="M 0,0 5,-5 -12.5,0 5,5 0,0 z" + style="fill-rule:evenodd;stroke:#000000;stroke-width:1pt" + transform="matrix(-0.4,0,0,-0.4,-4,0)" + inkscape:connector-curvature="0" /> + </marker> + <marker + inkscape:stockid="Arrow2Lend" + orient="auto" + refY="0" + refX="0" + id="Arrow2Lend" + style="overflow:visible"> + <path + id="path4130" + style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round" + d="M 8.7185878,4.0337352 -2.2072895,0.01601326 8.7185884,-4.0017078 c -1.7454984,2.3720609 -1.7354408,5.6174519 -6e-7,8.035443 z" + transform="matrix(-1.1,0,0,-1.1,-1.1,0)" + inkscape:connector-curvature="0" /> + </marker> + <filter + color-interpolation-filters="sRGB" + height="1.5039999" + id="filter_blur" + inkscape:collect="always" + width="1.1575" + x="-0.078749999" + y="-0.252"> + <feGaussianBlur + id="feGaussianBlur3780" + inkscape:collect="always" + stdDeviation="4.2" /> + </filter> + <marker + inkscape:stockid="Arrow2Mstart" + orient="auto" + refY="0" + refX="0" + id="Arrow2Mstart-7" + style="overflow:visible"> + <path + inkscape:connector-curvature="0" + id="path4133-8" + style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round" + d="M 8.7185878,4.0337352 -2.2072895,0.01601326 8.7185884,-4.0017078 c -1.7454984,2.3720609 -1.7354408,5.6174519 -6e-7,8.035443 z" + transform="scale(0.6,0.6)" /> + </marker> + <marker + inkscape:stockid="Arrow2Mend" + orient="auto" + refY="0" + refX="0" + id="Arrow2Mend-1" + style="overflow:visible"> + <path + inkscape:connector-curvature="0" + id="path4136-9" + style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round" + d="M 8.7185878,4.0337352 -2.2072895,0.01601326 8.7185884,-4.0017078 c -1.7454984,2.3720609 -1.7354408,5.6174519 -6e-7,8.035443 z" + transform="scale(-0.6,-0.6)" /> + </marker> + <filter + color-interpolation-filters="sRGB" + height="1.5039999" + id="filter_blur-1" + inkscape:collect="always" + width="1.1575" + x="-0.078749999" + y="-0.252"> + <feGaussianBlur + id="feGaussianBlur3780-1" + inkscape:collect="always" + stdDeviation="4.2" /> + </filter> + <filter + inkscape:collect="always" + id="filter18355"> + <feGaussianBlur + inkscape:collect="always" + stdDeviation="6.2598764" + id="feGaussianBlur18357" /> + </filter> + </defs> + <sodipodi:namedview + id="base" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1.0" + inkscape:pageopacity="0.0" + inkscape:pageshadow="2" + inkscape:zoom="1.4" + inkscape:cx="405.52492" + inkscape:cy="110.18507" + inkscape:document-units="px" + inkscape:current-layer="layer1" + showgrid="false" + inkscape:snap-grids="true" + inkscape:window-width="1920" + inkscape:window-height="992" + inkscape:window-x="0" + inkscape:window-y="27" + inkscape:window-maximized="1" + fit-margin-top="0" + fit-margin-left="0" + fit-margin-right="0" + fit-margin-bottom="0" /> + <metadata + id="metadata7"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title /> + </cc:Work> + </rdf:RDF> + </metadata> + <g + inkscape:label="Layer 1" + inkscape:groupmode="layer" + id="layer1" + transform="translate(-22.986913,-110.53072)"> + <rect + y="136.89983" + x="254.85715" + height="185.19879" + width="456.83981" + id="rect12822" + style="fill:#f39800;fill-opacity:1;stroke:#000000;stroke-width:0.96499999999999997;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;filter:url(#filter18355)" /> + <g + id="g18452"> + <rect + y="244.58766" + x="105.58965" + height="41.710945" + width="129.83621" + id="rect2987" + style="fill:#ffffff;stroke:#000000;stroke-width:1.41119610999999989px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;fill-opacity:1" /> + <text + sodipodi:linespacing="125%" + id="text2991" + y="261.25369" + x="112.20991" + style="font-size:12px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans" + xml:space="preserve"><tspan + id="tspan2995" + y="261.25369" + x="112.20991" + sodipodi:role="line">Apache mod_proxy:</tspan><tspan + id="tspan2997" + y="276.25369" + x="112.20991" + sodipodi:role="line">forward port 8383</tspan></text> + </g> + <g + id="g18364"> + <rect + y="167.43681" + x="304.33868" + height="50.483749" + width="98.582535" + id="rect2987-7" + style="fill:#ffffff;stroke:#000000;stroke-width:1.35282063000000008px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;fill-opacity:1" /> + <text + sodipodi:linespacing="125%" + id="text2991-2" + y="181.34079" + x="353.99908" + style="font-size:12px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:center;line-height:125%;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:middle;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans;-inkscape-font-specification:Sans" + xml:space="preserve"><tspan + id="tspan2997-2" + y="181.34079" + x="353.99908" + sodipodi:role="line">Connector:</tspan><tspan + id="tspan3813" + y="196.34079" + x="353.99908" + sodipodi:role="line">port = 80</tspan><tspan + id="tspan3908" + y="211.34079" + x="353.99908" + sodipodi:role="line">(web)</tspan></text> + </g> + <flowRoot + xml:space="preserve" + id="flowRoot3815" + style="font-size:40px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans"><flowRegion + id="flowRegion3817"><rect + id="rect3819" + width="201.02036" + height="90.913727" + x="174.25131" + y="117.466" /></flowRegion><flowPara + id="flowPara3821" /></flowRoot> <g + id="g18419"> + <rect + y="240.20126" + x="304.33868" + height="50.483749" + width="98.582535" + id="rect2987-7-6" + style="fill:#ffffff;stroke:#000000;stroke-width:1.35282063000000008px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;fill-opacity:1" /> + <text + sodipodi:linespacing="125%" + id="text2991-2-6" + y="253.64822" + x="353.63287" + style="font-size:12px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:center;line-height:125%;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:middle;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans;-inkscape-font-specification:Sans" + xml:space="preserve"><tspan + id="tspan2997-2-4" + y="253.64822" + x="353.63287" + sodipodi:role="line">Connector:</tspan><tspan + id="tspan3813-5" + y="268.64822" + x="353.63287" + sodipodi:role="line">port = 8383</tspan><tspan + id="tspan3908-2" + y="283.64822" + x="353.63287" + sodipodi:role="line">(auth proxy)</tspan></text> + </g> + <g + id="g7018" + transform="translate(-14,35.850205)"> + <g + id="g7023" + transform="translate(218.19295,1.0101525)"> + <g + id="g7028" + transform="translate(-97.984797,178.797)"> + <polygon + id="polygon6858" + style="opacity:0.7;fill:#000000;fill-opacity:1;filter:url(#filter_blur)" + points="60,61 60,56 66,56 66,61 83,61 83,66 66,66 66,71 80,86 73,86 63,76 53,86 46,86 60,71 60,66 43,66 43,61 " + transform="translate(-115.02286,-17.004219)" /> + <polygon + id="polygon6872" + points="57,55 57,50 63,50 63,55 80,55 80,60 63,60 63,65 77,80 70,80 60,70 50,80 43,80 57,65 57,60 40,60 40,55 " + style="fill:#ffffff;stroke:#000000" + transform="translate(-115.02286,-17.004219)" /> + <ellipse + d="m 67,45 c 0,3.865993 -3.134007,7 -7,7 -3.865993,0 -7,-3.134007 -7,-7 0,-3.865993 3.134007,-7 7,-7 3.865993,0 7,3.134007 7,7 z" + id="ellipse6874" + ry="7" + rx="7" + cy="45" + cx="60" + sodipodi:cx="60" + sodipodi:cy="45" + sodipodi:rx="7" + sodipodi:ry="7" + style="fill:#ffffff;stroke:#000000" + transform="translate(-115.02286,-17.004219)" /> + </g> + </g> + </g> + <rect + style="fill:#ffffff;stroke:#000000;stroke-width:1.35282063000000008px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;fill-opacity:1" + id="rect2987-7-5" + width="98.582535" + height="50.483749" + x="589.35858" + y="239.54141" /> + <text + xml:space="preserve" + style="font-size:12px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:center;line-height:125%;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:middle;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans;-inkscape-font-specification:Sans" + x="638.74945" + y="254.25693" + id="text2991-2-67" + sodipodi:linespacing="125%"><tspan + id="tspan10147" + sodipodi:role="line" + x="638.74945" + y="254.25693">AAA Servlet</tspan><tspan + id="tspan10204" + sodipodi:role="line" + x="638.74945" + y="269.25693">executes</tspan><tspan + id="tspan10206" + sodipodi:role="line" + x="638.74945" + y="284.25693">with roles</tspan></text> + <flowRoot + xml:space="preserve" + id="flowRoot10151" + style="font-size:40px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans"><flowRegion + id="flowRegion10153"><rect + id="rect10155" + width="139.90613" + height="110.10663" + x="648.01288" + y="147.2655" /></flowRegion><flowPara + id="flowPara10157" /></flowRoot> <g + id="g18431"> + <rect + y="169.04143" + x="589.86121" + height="50.483749" + width="98.582535" + id="rect2987-7-5-0" + style="fill:#ffffff;stroke:#000000;stroke-width:1.35282063000000008px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;fill-opacity:1" /> + <text + sodipodi:linespacing="125%" + id="text2991-2-67-9" + y="191.07236" + x="638.61047" + style="font-size:12px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:center;line-height:125%;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:middle;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans;-inkscape-font-specification:Sans" + xml:space="preserve"><tspan + y="191.07236" + x="638.61047" + sodipodi:role="line" + id="tspan10147-8">Non-AAA</tspan><tspan + y="206.07236" + x="638.61047" + sodipodi:role="line" + id="tspan10198">Servlet</tspan><tspan + y="221.07236" + x="638.61047" + sodipodi:role="line" + id="tspan10196" /></text> + </g> + <g + id="g18474"> + <rect + y="168.30391" + x="437.00925" + height="122.27845" + width="121.29423" + id="rect2987-7-2-2" + style="fill:#ffffff;stroke:#000000;stroke-width:2.33539009000000020px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;fill-opacity:1" /> + <text + sodipodi:linespacing="125%" + id="text2991-2-9-8" + y="181.0443" + x="497.75305" + style="font-size:12px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:center;line-height:125%;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:middle;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans;-inkscape-font-specification:Sans" + xml:space="preserve"><tspan + id="tspan3908-4-7" + y="181.0443" + x="497.75305" + sodipodi:role="line">ClaimAuthFilter:</tspan><tspan + id="tspan4038" + y="196.0443" + x="497.75305" + sodipodi:role="line">localPort in</tspan><tspan + id="tspan4040" + y="211.0443" + x="497.75305" + sodipodi:role="line">secureProxyPorts?</tspan><tspan + id="tspan4044" + y="226.0443" + x="497.75305" + sodipodi:role="line" /></text> + <g + id="g18469"> + <rect + style="fill:#ff0000;stroke:#000000;stroke-width:0.81352955000000005;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;fill-opacity:1" + id="rect10241" + width="98.994949" + height="23.733509" + x="448.15887" + y="220.00537" /> + <text + xml:space="preserve" + style="font-size:12px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans" + x="488.27258" + y="236.16118" + id="text10243" + sodipodi:linespacing="125%"><tspan + sodipodi:role="line" + id="tspan10245" + x="488.27258" + y="236.16118">No</tspan></text> + </g> + <g + id="g18461"> + <rect + style="fill:#00ff00;stroke:#000000;stroke-width:0.81352955000000005;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;fill-opacity:1" + id="rect10241-9" + width="98.994949" + height="23.733509" + x="448.15887" + y="253.81883" /> + <text + xml:space="preserve" + style="font-size:12px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans" + x="488.27258" + y="269.97464" + id="text10243-4" + sodipodi:linespacing="125%"><tspan + sodipodi:role="line" + id="tspan10245-6" + x="488.27258" + y="269.97464">Yes</tspan></text> + </g> + </g> + <g + id="g7018-9" + transform="translate(-15.11838,-36.914245)"> + <g + id="g7023-0" + transform="translate(218.19295,1.0101525)"> + <g + id="g7028-1" + transform="translate(-97.984797,178.797)"> + <polygon + id="polygon6858-6" + style="opacity:0.7;fill:#000000;fill-opacity:1;filter:url(#filter_blur-1)" + points="60,71 60,66 43,66 43,61 60,61 60,56 66,56 66,61 83,61 83,66 66,66 66,71 80,86 73,86 63,76 53,86 46,86 " + transform="translate(-115.02286,-17.004219)" /> + <polygon + id="polygon6872-6" + points="57,65 57,60 40,60 40,55 57,55 57,50 63,50 63,55 80,55 80,60 63,60 63,65 77,80 70,80 60,70 50,80 43,80 " + style="fill:#ffffff;stroke:#000000" + transform="translate(-115.02286,-17.004219)" /> + <ellipse + d="m 67,45 c 0,3.865993 -3.134007,7 -7,7 -3.865993,0 -7,-3.134007 -7,-7 0,-3.865993 3.134007,-7 7,-7 3.865993,0 7,3.134007 7,7 z" + id="ellipse6874-1" + ry="7" + rx="7" + cy="45" + cx="60" + sodipodi:cx="60" + sodipodi:cy="45" + sodipodi:rx="7" + sodipodi:ry="7" + style="fill:#ffffff;stroke:#000000" + transform="translate(-115.02286,-17.004219)" /> + </g> + </g> + </g> + <text + xml:space="preserve" + style="font-size:12px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans" + x="430.15594" + y="119.6479" + id="text12879" + sodipodi:linespacing="125%"><tspan + sodipodi:role="line" + id="tspan12881" + x="430.15594" + y="119.6479">Java EE Container</tspan></text> + <path + style="fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;marker-start:url(#Arrow2Mstart-7);marker-end:url(#Arrow2Mend-1)" + d="m 57.185293,265.44314 48.404357,0" + id="path13365" + inkscape:connector-type="polyline" + inkscape:connector-curvature="0" + inkscape:connection-start="#g7018" + inkscape:connection-start-point="d4" /> + <path + style="fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;marker-start:url(#Arrow2Mstart-7);marker-end:url(#Arrow2Mend-1)" + d="m 235.42587,265.44314 68.91281,0" + id="path14574" + inkscape:connector-type="polyline" + inkscape:connector-curvature="0" /> + <path + style="fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;marker-start:url(#Arrow2Mstart-7);marker-end:url(#Arrow2Mend-1)" + d="m 402.92122,265.52611 45.23767,0.0762" + id="path14999" + inkscape:connector-type="polyline" + inkscape:connector-curvature="0" /> + <path + style="fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;marker-start:url(#Arrow2Mstart-7);marker-end:url(#Arrow1Mend)" + d="m 402.92122,206.09216 51.12769,13.91321" + id="path15397" + inkscape:connector-type="polyline" + inkscape:connector-curvature="0" /> + <path + style="fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;marker-start:url(#Arrow2Mstart-7);marker-end:url(#Arrow2Mend-1)" + d="m 542.32654,220.00537 47.53467,-12.62771" + id="path15795" + inkscape:connector-type="polyline" + inkscape:connector-curvature="0" /> + <path + style="fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;marker-start:url(#Arrow2Mstart-7);marker-end:url(#Arrow2Mend-1)" + d="m 547.15383,265.36883 42.20475,-0.2701" + id="path16193" + inkscape:connector-type="polyline" + inkscape:connector-curvature="0" /> + <path + style="fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;marker-start:url(#Arrow2Mstart-7);marker-end:url(#Arrow2Mend-1)" + d="m 56.066913,192.67869 248.271767,0" + id="path17038" + inkscape:connector-type="polyline" + inkscape:connector-curvature="0" + inkscape:connection-start="#g7018-9" + inkscape:connection-start-point="d4" /> + </g> +</svg> diff --git a/odl-aaa-moon/aaa/aaa-authn-api/src/main/docs/sssd_auth_sequence.png b/odl-aaa-moon/aaa/aaa-authn-api/src/main/docs/sssd_auth_sequence.png Binary files differnew file mode 100644 index 00000000..9f9a0b49 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn-api/src/main/docs/sssd_auth_sequence.png diff --git a/odl-aaa-moon/aaa/aaa-authn-api/src/main/docs/sssd_auth_sequence.wsd b/odl-aaa-moon/aaa/aaa-authn-api/src/main/docs/sssd_auth_sequence.wsd new file mode 100644 index 00000000..f97ed1ee --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn-api/src/main/docs/sssd_auth_sequence.wsd @@ -0,0 +1,23 @@ +title Federated Authentication with SSSD + +# This walks through the federated authentication sequence where a claim from a +# third-party IdP system is posted to the ODL token endpoint in exchange for an +# access token. The claim information is assumed to be in format specific to the +# third-party IdP system and assumed to be captured via either Apache environment +# variables (Servlet attributes) or HTTP headers. + +Client -> Apache WebServer: authenticate +note right of Client +credentials +end note +Apache WebServer -> SSSD: authenticate +SSSD -> LDAP/AD : authenticate +SSSD -> Apache WebServer: claim +Apache WebServer -> ServletContainer: CGI variables +ServletContainer -> SSSD Plugin: Servlet attributes/headers +SSSD Plugin -> SSSD Plugin : transformClaim +SSSD Plugin -> TokenEndPoint : claim +TokenEndPoint -> TokenEndPoint : createToken +TokenEndPoint -> Client : refresh token, list of authorized domains +Client -> TokenEndPoint : refresh token, domain +TokenEndPoint -> Client : access token diff --git a/odl-aaa-moon/aaa/aaa-authn-api/src/main/docs/sssd_configuration.rst b/odl-aaa-moon/aaa/aaa-authn-api/src/main/docs/sssd_configuration.rst new file mode 100644 index 00000000..7f912d94 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn-api/src/main/docs/sssd_configuration.rst @@ -0,0 +1,1687 @@ +################################################ +Federated Authentication Utilizing Apache & SSSD +################################################ + +:Author: John Dennis +:Email: jdennis@redhat.com + +.. contents:: Table of Contents + +************ +Introduction +************ + +Applications should not need to handle the burden of authentication +and authorization. These are complex technologies further complicated +by the existence of a wide variety of authentication +mechanisms. Likewise there are numerous identity providers (IdP) which +one may wish to utilize, perhaps in a federated manner. The potential +to make critical mistakes are high while consuming significant +engineering resources. Ideally an application should "outsource" it's +authentication to an "expert" and avoid unnecessary development costs. + +For web based applications (both conventional HTML and REST API) there +has been a trend to embed a simple HTTP server in the application or +application server which handles the HTTP requests eschewing the use +of a traditional web server such as Apache. + +.. figure:: sssd_01.png + :align: center + + _`Figure 1.` + +But traditional web servers have a lot of advantages. They often come +with extensive support for technologies you might wish to utilize in +your application. It would require signification software engineering +to add support for those technologies in your application. The problem +is compounded by the fact many of these technologies demand domain +expertise which is unlikely to be available in the application +development team. Another problem is the libraries needed to utilize +the technology may not even be available in the programming language +the application is being developed in. Fundamentally an application +developer should focus on developing their application instead of +investing resources into implementing complex code for the ancillary +technologies the application may wish to utilize. + +Therefore fronting your application with a web server such as Apache +makes a lot of sense. One should allow Apache to handle complex tasks +such as multiple authentication mechanisms talking to multiple +IdP's. Suppose you want your application to handle Single Sign-On +(SSO) via Kerberos or authentication based on X509 certificates +(i.e. PKI). Apache already has extensions to handle these which have +been field proven, it would be silly to try and support these in your +application. Apache also comes with other useful extensions such as +``mod_identity_lookup`` which can extract metadata about an +authenticated user from multiple sources such as LDAP, +Active Directory, NIS, etc. + +By fronting your application with Apache and allowing Apache to handle +the complex task of authentication, identity lookups etc. you've +greatly increased the features of your application while at the same +time reducing application development time along with increasing +application security and robustness. + +.. figure:: sssd_02.png + :align: center + + _`Figure 2.` + +When Apache fronts your application you will be passed the results of +authentication and identity lookups. Your application only needs a +simple mechanism to accept these values. There are a variety of ways +the values can be passed from Apache to your application which will be +discussed in later sections. + +Authentication & Identity Properties +==================================== + +Authentication is proving that a user is who they claim to be, in +other words after authentication the user has a proven identity. In +security parlance the authenticated entity is call a +principal. Principals may be humans, machines or +services. Authorization is distinct from authentication. Authorization +declares what actions an authenticated principal may perform. For +example, does a principal have permission to read a certain file, run +a specific command, etc. Identity metadata is typically bound to the +principal to provide extra information. Examples include the users +full name, their organization, the groups they are members of, etc. + +Apache can provide both authentication and identity metadata to an +application freeing the application of this task. Authorization +usually will remain the province of the application. A typical +design pattern is to assign roles to a principal based on identity +properties. As the application executes on behalf of a principal the +application will check if the principal has the necessary role needed +to perform the operation. + +Apache ships with a wide variety of authentication modules. After an +Apache authentication module successfully authenticates a principal, it +sets internal variables identifying the principal and the +authentication method used to authenticate the principal. These are +exported as the CGI variables REMOTE_USER and AUTH_TYPE respectively +(see `CGI Export Issues`_ for further information). + +Identity Properties +------------------- + +Most Apache authentication modules do not have access to any of the +identity properties bound to the authenticated principal. Those +identity properties must be provided by some other mechanism. Typical +mechanisms include lookups in LDAP, Active Directory, NIS, POSIX +passwd/gecos and SQL. Managing these lookups can be difficult +especially in a networked environment where services may be +temporarily unavailable and/or in a enterprise deployment where +identity sources must be multiplexed across a variety of services +according to enterprise wide policy. + +`SSSD`_ (System Security Services Daemon) is designed to alleviate many +of the problems surrounding authentication and identity property +lookup. SSSD can provide identity properties via D-Bus using it's +InfoPipe (IFP) feature. The `mod_identity_lookup`_ Apache module is +given the name of the authenticated principal and makes available +identity properties via Apache environment variables (see `Configure +SSSD IFP`_ for details). + +Exporting & Consuming Identity Metadata +======================================= + +The authenticated principal (REMOTE_USER), the mechanism used to +authenticate the principal (AUTH_TYPE) and identity properties +(supplied by SSSD IFP) are exported to the application which trusts +this metadata to be valid. + +How is this identity metadata exported from Apache and then be +consumed by a Java EE Servlet? + +The architectural design inside Apache tries to capitalize on the +existing CGI standard (`CGI RFC`_) as much as possible. CGI defines +these relevant environment variables: + + * REMOTE_USER + * AUTH_TYPE + * REMOTE_ADDR + * REMOTE_HOST + + +Transporting Identity Metadata from Apache to a Java EE Servlet +=============================================================== + +In following figure we can see that the user connects to Apache +instead of the servlet container. Apache authenticates the user, looks +up the principal's identity information and then proxies the request +to the servlet container. The additional identity metadata must be +included in the proxy request in order for the servlet to extract it. + +.. figure:: sssd_03.png + :align: center + + _`Figure 3.` + +The Java EE Servlet API is designed with the HTTP protocol in mind +however the servlet never directly accesses the HTTP protocol stream. +Instead it uses the servlet API to get access to HTTP request +data. The responsibility for HTTP communication rests with the +container's ``Connector`` objects. When the servlet API needs +information it works in conjunction with the ``Connector`` to supply +it. For example the ``HttpServletRequest.getRemoteHost()`` method +interrogates information the ``Connector`` placed on the internal +request object. Analogously ``HttpServletRequest.getRemoteUser()`` +interrogates information placed on the internal request object by an +authentication filter. + +But what happens when a HTTP request is proxied to a servlet container +by Apache and ``getRemoteHost()`` or ``getRemoteUser()`` is called? Most +``Connector`` objects do not understand the proxy scenario, to them +a request from a proxy looks just like a request sent directly to the +servlet container. Therefore ``getRemoteHost()`` or ``getRemoteUser()`` +ends up returning information relative to the proxy instead of the +user who connected to the proxy because it's the proxy who connected +to the servlet container and not the end user. There are 2 fundamental +approaches which allow the servlet API to return data supplied by the +proxy: + + 1. Proxy uses special protocol (e.g. AJP) to embed metadata. + 2. Metadata is embedded in an HTTP extension by the proxy (i.e. headers) + +Proxy With AJP Protocol +----------------------- + +The AJP_ protocol was designed as a protocol to exchange HTTP requests +and responses between Apache and a Java EE Servlet Container. One of +its design goals was to improve performance by translating common text +values appearing in HTTP requests to a more compact binary form. At +the same time AJP provided a mechanism to supply metadata about the +request to the servlet container. That metadata is encoded in an AJP +attribute (a name/value pair). The Apache AJP Proxy module looks up +information in the internal Apache request object (e.g. remote user, +remote address, etc.) and encodes that metadata in AJP attributes. On +the servlet container side a AJP ``Connector`` object is aware of these +metadata attributes, extracts them from the protocol and supplies +their values to the upper layers of the servlet API. Thus a call to +``HttpServletRequest.getRemoteUser()`` made by a servlet will receive +the value set by Apache prior to the proxy. This is the desired and +expected behavior. A servlet should be ignorant of the consequences of +proxies; the servlet API should behave the same regardless of the +presence of a proxy. + +The AJP protocol also has a general purpose attribute mechanism whereby +any arbitrary name/value pair can be passed. This proxy metadata can +be retrieved by a servlet by calling ``ServletRequest.getAttribute()`` +[1]_ When Apache mod_proxy_ajp is being used the authentication +metadata for the remote user and auth type are are automatically +inserted into the AJP protocol and the AJP ``Connector`` object on +the servlet receiving end supplies those values to +``HttpServletRequest.getRemoteHost()`` and +``HttpServletRequest.getRemoteUser()`` respectively. But the identity +metadata supplied by ``mod_identity_lookup`` needs to be explicitly +encoded into an AJP attribute (see `Configure SSSD IFP`_ for details) +that can later be retrieved by ``ServletRequest.getAttribute()``. + +Proxy With HTTP Protocol +------------------------ + +Although the AJP protocol offers a number of nice advantages sometimes +it's not an option. Not all servlet containers support AJP or there +may be some other deployment constraint that precludes its use. In this +case option 2 from above needs to be used. Option 2 requires only the +defined HTTP protocol be used without any "out of band" metadata. The +conventional way to attach extension metadata to a HTTP request is to +add extension HTTP headers. + +One problem with using extension HTTP headers to pass metadata to a +servlet is the expectation the servlet API will have the same +behavior. In other words the value returned by +``HttpServletRequest.getRemoteUser()`` should not depend on whether the +proxy request was exchanged with the AJP protocol or the HTTP +protocol. The solution to this is to wrap the ``HttpServletRequest`` +object in a servlet filter. The wrapper overrides certain request +methods (e.g. ``getRemoteUser()``). The override method looks to see if +the metadata is in the extension HTTP headers, if so it returns the +value found in the extension HTTP header otherwise it defers to the +existing servlet implementation. The ``ServletRequest.getAttribute()`` is +overridden in an analogous manner in the wrapper filter. Any call to +``ServletRequest.getAttribute()`` is first checked to see if the value +exists in the extension HTTP header first. + +Metadata supplied by Apache that is **not** part of the normal Java +EE Servlet API **always** appears to the servlet via the +``ServletRequest.getAttribute()`` method regardless of the proxy +transport mechanism. The consequence of this is a servlet +continues to utilize the existing Java EE Servlet API without concern +for intermediary proxies, *and* any other metadata supplied by a proxy +is *always* retrieved via ``ServletRequest.getAttribute()`` (see the +caveat about ``ServletRequest.getAttributeNames()`` [1]_). + +******************* +Configuration Guide +******************* + +Although Apache authentication and SSSD identity lookup can operate +with a variety of authentication mechanisms, IdP's and identity +metadata providers we will demonstrate a configuration example which +utilizes the FreeIPA_ IdP. FreeIPA excels at Kerberos SSO authentication, +Active Directory integration, LDAP based identity metadata storage and +lookup, DNS services, host based RBAC, SSH key management, certificate +management, friendly web based console, command line tools and many +other advanced IdP features. + +The following configuration steps will need to be performed: + +1. Install FreeIPA_ by following the installation guides in the FreeIPA_ + documentation area. When you install FreeIPA_ you will need to select a + realm (a.k.a domain) in which your users and hosts will exist. In + our example we will use the ``EXAMPLE.COM`` realm. + +2. Install and configure the Apache HTTP web server. The + recommendation is to install and run the Apache HTTP web server on + the same system the Java EE Container running AAA is installed on. + +3. Configure the proxy connector in the Java EE Container and set the + ``secureProxyPorts``. + +We will also illustrate the operation of the system by adding an +example user named ``testuser`` who will be a member of the +``odl_users`` and ``odl_admin`` groups. + +Add Example User and Groups to FreeIPA +====================================== + +After installing FreeIPA you will need to populate FreeIPA with your users, +groups and other data. Refer to the documentation in FreeIPA_ for the +variety of ways this task can be performed; it runs the gamut from web +based console to command line utilities. For simplicity we will use +the command line utilities. + +Identify yourself to FreeIPA as an administrator; this will give you the +necessary privileges needed to create and modify data in FreeIPA. You do +this by obtaining a Kerberos ticket for the ``admin`` user (or any +other user in FreeIPA with administrator privileges. + +:: + + % kinit admin@EXAMPLE.COM + +Create the example ``odl_users`` and `odl_admin`` groups. + +:: + + % ipa group-add odl_users --desc 'OpenDaylight Users' + % ipa group-add odl_admin --desc 'OpenDaylight Administrators' + +Create the example user ``testuser`` with the first name "Test" and a +last name of "User" and an email address of "test.user@example.com" + +:: + + % ipa user-add testuser --first Test --last User --email test.user@example.com + +Now add ``testuser`` to the ``odl_users`` and ``odl_admin`` groups. + +:: + + % ipa group-add-member odl_users --user testuser + % ipa group-add-member odl_admin --user testuser + +Configure Apache +================ + +A number of Apache configuration directives will need to be specified +to implement the Apache to application binding. Although these +configuration directives can be located in any number of different +Apache configuration files the most sensible approach is to co-locate +them in a single application configuration file. This greatly +simplifies the deployment of your application and isolates your +application configuration from other applications and services sharing +the Apache installation. In the examples that follow our application +will be named ``my_app`` and the Apache application configuration file +will be named ``my_app.conf`` which should be located in Apache's +``conf.d/`` directory. The web resource we are protecting and +supplying identity metadata for will be named ``my_resource``. + + +Configure Apache for Kerberos +----------------------------- + +When FreeIPA is deployed Kerberos is the preferred authentication mechanism +for Single Sign-On (SSO). FreeIPA also provides identity metadata via +Apache ``mod_identity_lookup``. To protect your ``my_resource`` resource +with Kerberos authentication identify your resource as requiring +Kerberos authentication in your ``my_app.conf`` Apache +configuration. For example: + +:: + + <Location my_resource> + AuthType Kerberos + AuthName "Kerberos Login" + KrbMethodNegotiate On + KrbMethodK5Passwd Off + KrbAuthRealms EXAMPLE.COM + Krb5KeyTab /etc/http.keytab + require valid-user + </Location> + +You will need to replace EXAMPLE.COM in the KrbAuthRealms declaration +with the Kerberos realm for your deployment. + + +Configure SSSD IFP +------------------ + +To use the Apache ``mod_identity_lookup`` module to supply identity +metadata you need to do the following in ``my_app.conf``: + +1. Enable the module + + :: + + LoadModule lookup_identity_module modules/mod_lookup_identity.so + +2. Apply the identity metadata lookup to specific URL's + (e.g. ``my_resource``) via an Apache location directive. In this + example we look up the "mail" attribute and assign it to the + REMOTE_USER_EMAIL environment variable. + + :: + + <LocationMatch "my_resource"> + LookupUserAttr mail REMOTE_USER_EMAIL + </LocationMatch> + +3. Export the environment variable via the desired proxy protocol, see + `Exporting Environment Variables to the Proxy`_ + +Exporting Environment Variables to the Proxy +-------------------------------------------- + +First you need to decide which proxy protocol you're going to use, AJP +or HTTP and then determine the target address and port to proxy to. The +recommended configuration is to run both the Apache server and the +servlet container on the same host and to proxy requests over the +local loopback interface (see `Declaring the Connector Ports for +Authentication Proxies`_). In our examples we'll use port 8383. Thus +in ``my_app.conf`` add a proxy declaration. + +For HTTP Proxy + +:: + + ProxyPass / http://localhost:8383/ + ProxyPassReverse / http://localhost:8383/ + +For AJP Proxy + +:: + + ProxyPass / ajp://localhost:8383/ + ProxyPassReverse / ajp://localhost:8383/ + +AJP Exports +^^^^^^^^^^^ + +AJP automatically forwards REMOTE_USER and AUTH_TYPE making them +available to the ``HttpServletRequest`` API, thus you do not need to +explicitly forward these in the proxy configuration. However all other +``mod_identity_lookup`` metadata must be explicitly forwarded as an AJP +attribute. These AJP attributes become visible in the +``ServletRequest.getAttribute()`` method [1]_. + +The Apache ``mod_proxy_ajp`` module automatically sends any Apache +environment variable prefixed with "AJP\_" as an AJP attribute which +can be retrieved with ``ServletRequest.getAttribute()``. Therefore the +``mod_identity_lookup`` directives which specify the Apache environment +variable to set with the result of a lookup must be prefixed with +"AJP\_". Using the above example of looking up the principal's email +address we modify the environment variable to include the "AJP\_" +prefix. Thusly: + + :: + + <LocationMatch "my_resource"> + LookupUserAttr mail AJP_REMOTE_USER_EMAIL + </LocationMatch> + +The sequence of events is as follows: + + 1. When the URL matches "my_resource". + + 2. ``mod_identity_lookup`` retrieves the mail attribute for the + principal. + + 3. ``mod_identity_lookup`` assigns the value of the mail attribute + lookup to the AJP_REMOTE_USER_EMAIL Apache environment variable. + + 4. ``mod_proxy_ajp`` encodes AJP_REMOTE_USER_EMAIL environment + variable into an AJP attribute in the AJP protocol because the + environment variable is prefixed with "AJP\_". The name of the + attribute is stripped of it's "AJP\_" prefix thus the + AJP_REMOTE_USER_EMAIL environment variable is transferred as the + AJP attribute REMOTE_USER_EMAIL. + + 5. The request is forwarded (i.e. proxied) to servlet container + using the AJP protocol. + + 6. The servlet container's AJP ``Connector`` object is assigned each AJP + attribute to the set of attributes on the ``ServletRequest`` + attribute list. Thus a call to + ``ServletRequest.getAttribute("REMOTE_USER_EMAIL")`` yields the + value set by ``mod_identity_lookup``. + + +HTTP Exports +^^^^^^^^^^^^ + +When HTTP proxy is used there are no automatic or implicit metadata +transfers; every metadata attribute must be explicitly handled on both +ends of the proxy connection. All identity metadata attributes are +transferred as extension HTTP headers, by convention those headers are +prefixed with "X-SSSD-". + +Using the original example of looking up the principal's email +address we must now perform two independent actions: + + 1. Lookup the value via ``mod_identity_lookup`` and assign to an + Apache environment variable. + + 2. Export the environment variable in the request header with the + "X-SSSD-" prefix. + + :: + + <LocationMatch "my_resource"> + LookupUserAttr mail REMOTE_USER_EMAIL + RequestHeader set X-SSSD-REMOTE_USER_EMAIL %{REMOTE_USER_EMAIL}e + </LocationMatch> + +The sequence of events is as follows: + + 1. When the URL matches "my_resource". + + 2. ``mod_identity_lookup`` retrieves the mail attribute for the + principal. + + 3. ``mod_identity_lookup`` assigns the value of the mail attribute + lookup to the REMOTE_USER_EMAIL Apache environment variable. + + 4. Apache's RequestHeader directive executes just prior to the + request being forwarded (i.e. in the Apache fixup stage). It adds + the header X-SSSD-REMOTE_USER_EMAIL and assigns the value for + REMOTE_USER_EMAIL found in the set of environment variables. It + does this because the syntax %{XXX} is a variable reference for + the name XXX and the 'e' appended after the closing brace + indicates the lookup is to be performed in the set of environment + variables. + + 5. The request is forwarded (i.e. proxied) to the servlet container + using the HTTP protocol. + + 6. When ``ServletRequest.getAttribute()`` is called the ``SssdFilter`` + wrapper intercepts the ``getAttribute()`` method. It looks for an + HTTP header of the same name with "X-SSSD-" prefixed to it. In + this case ``getAttribute("REMOTE_USER_EMAIL")`` causes the lookup of + "X-SSSD-REMOTE_USER_EMAIL" in the HTTP headers, if found that + value is returned. + +AJP Proxy Example Configuration +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +If you are using AJP proxy to the Java EE Container on port 8383 your +``my_app.conf`` Apache configuration file will probably look like +this: + +:: + + <LocationMatch "my_resource"> + + ProxyPass / ajp://localhost:8383/ + ProxyPassReverse / ajp://localhost:8383/ + + LookupUserAttr mail AJP_REMOTE_USER_EMAIL " " + LookupUserAttr givenname AJP_REMOTE_USER_FIRSTNAME + LookupUserAttr sn AJP_REMOTE_USER_LASTNAME + LookupUserGroups AJP_REMOTE_USER_GROUPS ":" + + </LocationMatch> + +Note the specification of the colon separator for the +``LookupUserGroups`` operation. [3]_ + +HTTP Proxy Example Configuration +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +If you are using a conventional HTTP proxy to the Java EE Container on +port 8383 your ``my_app.conf`` Apache configuration file will probably +look like this: + +:: + + <LocationMatch "my_resource"> + + ProxyPass / http://localhost:8383/ + ProxyPassReverse / http://localhost:8383/ + + RequestHeader set X-SSSD-REMOTE_USER expr=%{REMOTE_USER} + RequestHeader set X-SSSD-AUTH_TYPE expr=%{AUTH_TYPE} + RequestHeader set X-SSSD-REMOTE_HOST expr=%{REMOTE_HOST} + RequestHeader set X-SSSD-REMOTE_ADDR expr=%{REMOTE_ADDR} + + LookupUserAttr mail REMOTE_USER_EMAIL + RequestHeader set X-SSSD-REMOTE_USER_EMAIL %{REMOTE_USER_EMAIL}e + + LookupUserAttr givenname REMOTE_USER_FIRSTNAME + RequestHeader set X-SSSD-REMOTE_USER_FIRSTNAME %{REMOTE_USER_FIRSTNAME}e + + LookupUserAttr sn REMOTE_USER_LASTNAME + RequestHeader set X-SSSD-REMOTE_USER_LASTNAME %{REMOTE_USER_LASTNAME}e + + LookupUserGroups REMOTE_USER_GROUPS ":" + RequestHeader set X-SSSD-REMOTE_USER_GROUPS %{REMOTE_USER_GROUPS}e + + </LocationMatch> + +Note the specification of the colon separator for the +``LookupUserGroups`` operation. [3]_ + + +Configure Java EE Container Proxy Connector +=========================================== + +The Java EE Container must be configured to listen for connections +from the Apache web server. A Java EE Container specifies connections +via a ``Connector`` object. A ``Connector`` **must** be dedicated +**exclusively** for handling authenticated requests from the Apache +web server. The reason for this is explained in `The Proxy +Problem`_. In addition ``ClaimAuthFilter`` needs to validate that any +request it processes originated from the trusted Apache instance. This +is accomplished by dedicating one or more ports exclusively for use by +the trusted Apache server and enumerating them in the +``secureProxyPorts`` configuration as explained in `Locking Down the +Apache to Java EE Container Channel`_ and `Declaring the Connector +Ports for Authentication Proxies`_. + +Configure Tomcat Proxy Connector +-------------------------------- + +The Tomcat Java EE Container defines Connectors in its ``server.xml`` +configuration file. + +:: + + <Connector + address="127.0.0.1" + port="8383" + protocol="HTTP/1.1" + tomcatAuthentication="false" + connectionTimeout="20000" + redirectPort="8443" + /> + + +:address: + This should be the loopback address as explained `Locking Down the + Apache to Java EE Container Channel`_. + +:port: + In our examples we've been using port 8383 as the proxy port. The + exact port is not important but it must be consistent with the + Apache proxy port, the ``Connector`` declaration, and the port value + in ``secureProxyPorts``. + +:protocol: + As explained in `Transporting Identity Metadata from Apache to a + Java EE Servlet`_ you will need to decide if you are using HTTP or + AJP as the proxy protocol. In the example above the protocol is set + for HTTP, if you use AJP instead the protocol should instead be + "AJP/1.3". + +:tomcatAuthentication: + This boolean flag tells Tomcat whether Tomcat should perform + authentication on the incoming requests or not. Since authentication + is performed by Apache we do not want Tomcat to perform + authentication therefore this flag must be set to false. + +The AAA system needs to know which port(s) the trusted Apache proxy +will be sending requests on so it can trust the request authentication +metadata. See `Declaring the Connector Ports for Authentication +Proxies`_ for more information). Set ``secureProxyPorts`` in the +FederationConfiguration. + +:: + + secureProxyPorts=8383 + + +Configure Jetty Proxy Connector +------------------------------- + +The Jetty Java EE Container defines Connectors in its ``jetty.xml`` +configuration file. + +:: + + <!-- Trusted Authentication Federation proxy connection --> + <Call name="addConnector"> + <Arg> + <New class="org.eclipse.jetty.server.nio.SelectChannelConnector"> + <Set name="host">127.0.0.1</Set> + <Set name="port">8383</Set> + <Set name="maxIdleTime">300000</Set> + <Set name="Acceptors">2</Set> + <Set name="statsOn">false</Set> + <Set name="confidentialPort">8445</Set> + <Set name="name">federationConn</Set> + <Set name="lowResourcesConnections">20000</Set> + <Set name="lowResourcesMaxIdleTime">5000</Set> + </New> + </Arg> + </Call> + +:host: + This should be the loopback address as explained `Locking Down the + Apache to Java EE Container Channel`_. + +:port: + In our examples we've been using port 8383 as the proxy port. The + exact port is not important but it must be consistent with the + Apache proxy port, the ``Connector`` declaration, and the port value + in ``secureProxyPorts``. + + +Note, values in Jetty XML can also be parameterized so that they may +be passed from property files or set on the command line. Thus +typically the port is set within Jetty XML, but uses the Property +element to be customizable. Thus the above ``host`` and ``port`` +properties could be specificed this way: + +:: + + <Set name="host"> + <Property name="jetty.host" default="127.0.0.1"/> + </Set> + <Set name="port"> + <Property name="jetty.port" default="8383"/> + </Set> + + +The AAA system needs to know which port(s) the trusted Apache proxy +will be sending requests on so it can trust the request authentication +metadata. See `Declaring the Connector Ports for Authentication +Proxies`_ for more information). Set ``secureProxyPorts`` in the +FederationConfiguration. + +************************************************ +How Apache Identity Metadata is Processed in AAA +************************************************ + +`Figure 2.`_ and `Figure 3.`_ illustrates the fact the first stage in +processing a request from a user begins with Apache where the user is +authenticated and SSSD supplies additional metadata about the +user. The original request along with the metadata are subsequently +forwarded by Apache to the Java EE Container. `Figure 4.`_ illustrates +the processing inside the Java EE Container once it receives the +request on one of its secure connectors. + + +.. figure:: sssd_04.png + :align: center + + _`Figure 4.` + +:Step 1: + One or more Connectors have been configured to listen for requests + being forwarded from a trusted Apache instance. The Connector is + configured to communicate using either the HTTP or AJP protocols. + See `Exporting Environment Variables to the Proxy`_ for more + information on selecting a proxy transport protocol. + +:Step 2: + The identity metadata bound to the request needs to be extracted + differently depending upon whether HTTP or AJP is the transport + protocol. To allow later stages in the pipeline to be ignorant of + the transport protocol semantics the ``SssdFilter`` servlet filter + is introduced. The ``SssdFilter`` wraps the ``HttpServletRequest`` + class and intercepts calls which might return the identity + metadata. The wrapper in the filter looks in protocol specific + locations for the metadata. In this manner users of the + ``HttpServletRequest`` are isolated from protocol differences. + + +:Step 3: + + The ``ClaimAuthFilter`` is responsible for determining if identity + metadata is bound to the request. If so all identity metadata is + packaged into an assertion which is then handed off to + ``SssdClaimAuth`` which will transform the identity metadata in the + assertion into a AAA Claim which is the authorizing token for the user. + +:Step 4: + The ``SssdClaimAuth`` object is responsible for transforming the + external federated identity metadata provided by Apache and SSSD into + a AAA claim. The AAA claim is an authorization token which includes + information about the user plus a set of roles. These roles provide the + authorization to perform AAA tasks. Although how roles are assigned is + flexible the expectation is domain and/or group membership will be the + primary criteria for role assignment. Because deciding how to handle + external federated identity metadata is site and deployment specific + we need a loadable policy mechanism. This is accomplished by a set of + transformation rules which transforms the incoming IdP identity + metadata into a AAA claim. For greater clarity this important step is + broken down into smaller units in the shaded box in `Figure 4.`_. + +:Step 4.1: + `The Mapping Rule Processor`_ is designed to accept a JSON object + (set of key/value pairs) as input and emit a different JSON object + as output effectively operating as a transformation engine on + key/value pairs. + +:Step 4.2: + The input assertion is rewritten as a JSON object in the format + required by the Mapping Rule Processor. The JSON assertion is then + passed into the Mapping Rule Processor. + +:Step 4.3: + `The Mapping Rule Processor`_ identified as ``IdPMapper`` evaluates + the input JSON assertion in the context of the mapping rules defined + for the site deployment. If ``IdPMapper`` is able to successfully + transform the input it will return a JSON object which we called the + *mapped* result. If the input JSON assertion is not compatible with + the site specific rules loaded into the ``IdPMapper`` then NULL is + returned by the ``IdPMapper``. + +:Step 4.4: + If a mapped JSON object is returned by the ``IdPMapper`` the mapping + was successful. The values in the mapped result are re-written into + an AAA Claim token. + +How Apache Identity Metadata is Mapped to AAA Values +==================================================== + +A federated IdP supplies metadata in a form unique to the IdP. This is +called an assertion. That assertion must be transformed into a format +and data understood by AAA. More importantly that assertion needs to +yield *authorization roles specific to AAA*. In `Figure 4.`_ Step 4.3 +the ``IdPMapper`` provides the transformation from an external IdP +assertion to an AAA specific claim. It does this via a Mapping Rule +Processor which reads a site specific set of transformation +rules. These mapping rules define how to transform an external IdP +assertion into a AAA claim. The mapping rules also are responsible for +validating the external IdP claim to make sure it is consistent with +the site specific requirements. The operation of the Mapping Rule +Processor and the syntax of the mapping rules are defined in `The +Mapping Rule Processor`_. + +Below is an example mapping rule which might be loaded into the +Mapping Rule Processor. It is assumed there are two AAA roles which +may be assigned [4]_: + +``user`` + A role granting standard permissions for normal ODL users. + +``admin`` + A special role granting full administrative permissions. + +In this example assigning the ``user`` and ``admin`` roles +will be based on group membership in the following groups: + +``odl_users`` + Members of this group are normal ODL users with restricted permissions. + +``odl_admin`` + Members of this group are ODL administrators with permission to + perform all operations. + +Granting of the ``user`` and/or ``admin`` roles based on +membership in the ``odl_users`` and ``odl_admin`` is illustrated in +the follow mapping rule example which also extracts the user principal +and domain information in the preferred format for the site +(e.g. usernames are lowercase without domain suffixes and the domain +is uppercase and supplied separately). + +_`Mapping Rule Example 1.` + +:: + + 1 [ + 2 {"mapping": {"ClientId": "$client_id", + 3 "UserId": "$user_id", + 4 "User": "$username", + 5 "Domain": "$domain", + 6 "roles": "$roles", + 7 }, + 8 "statement_blocks": [ + 9 [ + 10 ["set", "$groups", []], + 11 ["set", "$roles", []] + 12 ], + 13 [ + 14 ["in", "REMOTE_USER", "$assertion"], + 15 ["exit", "rule_fails", "if_not_success"], + 16 ["regexp", "$assertion[REMOTE_USER]", "(?<username>\\w+)@(?<domain>.+)"], + 17 ["exit", "rule_fails", "if_not_success"], + 18 ["lower", "$username", "$regexp_map[username]"], + 19 ["upper", "$domain", "$regexp_map[domain]"], + 20 ], + 21 [ + 22 ["in", "REMOTE_USER_GROUPS", "$assertion"], + 23 ["exit", "rule_fails", "if_not_success"], + 24 ["split", "$groups", "$assertion[REMOTE_USER_GROUPS]", ":"], + 25 ], + 26 [ + 27 ["in", "odl_users", "$groups"], + 28 ["continue", "if_not_success"], + 29 ["append", "$roles", "user"], + 30 ], + 31 [ + 32 ["in", "odl_admin", "$groups"], + 33 ["continue", "if_not_success"], + 34 ["append", "$roles", "admin"] + 35 ], + 36 [ + 37 ["unique", "$roles", "$roles"], + 38 ["length", "$n_roles", "$roles"], + 39 ["compare", "$n_roles", ">", 0], + 40 ["exit", "rule_fails", "if_not_success"], + 41 ], + 42 ] + 43 } + 44 ] + +:Line 1: + Starts a list of rules. In this example only 1 rule is defined. Each + rule is a JSON object containing a ``mapping`` and a required list + of ``statement_blocks``. The ``mapping`` may either be specified + inside a rule as it is here or may be referenced by name in a table + of mappings (this is easier to manage if you have a large number of + rules and small number of mappings). + +:Lines 2-7: + Defines the JSON mapped result. Each key maps to AAA claim. The + value is a rule variable whose value will be substituted if the rule + succeeds. Thus for example the AAA claim value ``User`` will be + assigned the value from the ``$username`` rule variable. +:Line 8: + Begins the list of statement blocks. A statement must be contained + inside a block. +:Lines 9-12: + The first block usually initializes variables that will be + referenced later. Here we initialize ``$groups`` and ``$roles`` to + empty arrays. These arrays may be appended to in later blocks and + may be referenced in the final ``mapping`` output. +:Lines 13-20: + This block sets the user and domain information based on + ``REMOTE_USER`` and exits the rule if ``REMOTE_USER`` is not defined. +:Lines 14-15: + This test is critical, it assures ``REMOTE_USER`` is defined in the + assertion, if not the rule is skipped because we depend on + ``REMOTE_USER``. +:Lines 16-17: + Performs a regular expression match against ``REMOTE_USER`` to split + the username from the domain. The regular expression uses named + groups, in this instance ``username`` and ``domain``. If the regular + expression does not match the rule is skipped. +:Lines 18-19: + These lines reference the previous result of the regular expression + match which are stored in the special variable ``$regexp_map``. The + username is converted to lower case and stored in ``$username`` and + the domain is converted to upper case and stored in ``$domain``. The + choice of case is purely by convention and site requirements. +:Lines 21-35: + These 3 blocks assign roles based on group membership. +:Lines 21-25: + Assures ``REMOTE_USER_GROUPS`` is defined in the assertion; if not, the + rule is skipped. ``REMOTE_USER_GROUPS`` is colon separated list of group + names. In order to operate on the individual group names appearing + in ``REMOTE_USER_GROUPS`` line 24 splits the string on the colon + separator and stores the result in the ``$groups`` array. +:Lines 27-30: + This block assigns the ``user`` role if the user is a member of the + ``odl_users`` group. +:Lines 31-35: + This block assigns the ``admin`` role if the user is a + member of the ``odl_admin`` group. +:Lines 36-41: + This block performs final clean up actions for the rule. First it + assures there are no duplicates in the ``$roles`` array by calling + the ``unique`` function. Then it gets a count of how many items are + in the ``$roles`` array and tests to see if it's empty. If there are + no roles assigned the rule is skipped. +:Line 43: + This is the end of the rule. If we reach the end of the rule it + succeeds. When a rule succeeds the mapping associated with the rule + is looked up. Any rule variable appearing in the mapping is + substituted with its value. + +Using the rules in `Mapping Rule Example 1.`_ and following example assertion +in JSON format: + +_`Assertion Example 1.` + +:: + + { + "REMOTE_USER": "TestUser@example.com", + "REMOTE_AUTH_TYPE": "Negotiate", + "REMOTE_USER_GROUPS": "odl_users:odl_admin", + "REMOTE_USER_EMAIL": "test.user@example.com", + "REMOTE_USER_FIRSTNAME": "Test", + "REMOTE_USER_LASTNAME": "User" + } + +Then the mapper will return the following mapped JSON document. This +is the ``mapping`` defined on line 2 of `Mapping Rule Example 1.`_ with the +variables substituted after the rule successfully executed. Note any +valid JSON data type can be returned, in this example the ``null`` +value is returned for ``ClientId`` and ``UserId``, normal strings for +``User`` and ``Domain`` and an array of strings for the ``roles`` value. + +_`Mapped Result Example 1.` + +:: + + { + "ClientId": null, + "UserId": null, + "User": "testuser", + "Domain": "EXAMPLE.COM", + "roles": ["user", "admin"] + } + + +************************** +The Mapping Rule Processor +************************** + +The Mapping Rule Processor is designed to be as flexible and generic +as possible. It accepts a JSON object as input and returns a JSON +object as output. JSON was chosen because virtually all data can be +represented in JSON, JSON has extensive support and JSON is human +readable. The rules loaded into the Mapping Rule Processor are also +expressed in JSON. One advantage of this is it makes it easy for a +site administrator to define hardcoded values which are always +returned and/or static tables of white and black listed users or users +who are always mapped into certain roles. + +.. include:: mapping.rst + +*********************** +Security Considerations +*********************** + +Attack Vectors +============== + +A Java EE Container fronted by Apache has by definition 2 major +components: + +* Apache +* Java EE Container + +Each of these needs to be secure in its own right. There is extensive +documentation on securing each of these components and the reader is +encouraged to review this material. For the purpose of this discussion +we are most interested in how Apache and the Java EE +Container cooperate to form an integrated security system. Because +Apache is performing authentication on behalf of the Java EE Container, +it views Apache as a trusted partner. Our primary concern is the +communication channel between Apache and the Java EE Container. We +must assure the Java EE Container knows who it's trusted partner is +and that it only accepts security sensitive data from that partner, +this can best be described as `The Proxy Problem`_. + +Forged REMOTE_USER +------------------ + +HTTP request handling is often implemented as a processing pipeline +where individual handlers are passed the request, they may then attach +additional metadata to the request or transform it in some manner +before handing it off to the next stage in the pipeline. A request +handler may also short circuit the request processing pipeline and +cause a response to be generated. Authentication is typically +implemented an as early stage request handler. If a request gets past +an authentication handler later stage handlers can safely assume the +request belongs to an authenticated user. Authorization metadata may +also have been attached to the request. Later stage handlers use the +authentication/authorization metadata to make decisions as to whether +the operations in the request can be satisfied. + +When a request is fielded by a traditional web server with CGI (Common +Gateway Interface, RFC 3875) the request metadata is passed via CGI +meta-variables. CGI meta-variables are often implemented as environment +variables, but in practical terms CGI metadata is really just a set of +name/value pairs a later stage (i.e. CGI script, servlet, etc.) can +reference to learn information about the request. + +The CGI meta-variables REMOTE_USER and AUTH_TYPE relate to +authentication. REMOTE_USER is the identity of the authenticated user +and AUTH_TYPE is the authentication mechanism that was used to +authenticate the user. + +**If a later stage request handler sees REMOTE_USER and AUTH_TYPE as +non-null values it assumes the user is fully authenticated! Therefore +is it essential REMOTE_USER and AUTH_TYPE can only enter the request +pipeline via a trusted source.** + +The Proxy Problem +================= + +In a traditional monolithic web server the CGI meta-variables are +created and managed by the web server, which then passes them to CGI +scripts and executables in a very controlled environment where they +execute in the context of the web server. Forgery of CGI +meta-variables is generally not possible unless the web server has +been compromised in some fashion. + +However in our configuration the Apache web server acts as an identity +processor, which then forwards (i.e. proxies) the request to the Java +EE container (i.e Tomcat, Jetty, etc.). One could think of the Java +EE container as just another CGI script which receives CGI +meta-variables provided by the Apache web server. Where this analogy +breaks down is how Apache invokes the CGI script. Instead of forking a +child process where the child's environment and input/output pipes are +carefully controlled by Apache the request along with its additional +metadata is forwarded over a transport (typically TCP/IP) to another +process, the proxy, which listens on socket. + +The proxy (in this case the Java EE container) reads the request and +the attached metadata and acts upon it. If the request read by the +proxy contains the REMOTE_USER and AUTH_TYPE CGI meta-variables the +proxy will consider the request **fully authenticated!**. Therefore +when the Java EE container is configured as a proxy it is +**essential** it only reads requests from a **trusted** Apache web +server. If any other client aside from the trusted Apache web server +is permitted to connect to the Java EE container that client could +present forged REMOTE_USER and AUTH_TYPE meta-variables, which would be +automatically accepted as valid thus opening a huge security hole. + + +Possible Approaches to Lock Down a Proxy Channel +================================================ + +Tomcat Valves +------------- + +You can use a `Tomcat Remote Address Valve`_ valve to filter by IP or +hostname to only allow a subset of machines to connect. This can be +configured at the Engine, Host, or Context level in the +conf/server.xml by adding something like the following: + +:: + + <!-- allow only LAN IPs to connect --> + <Valve className="org.apache.catalina.valves.RemoteAddrValve" + allow="192.168.1.*"> + </Valve> + +The problem with valves is they are a Tomcat only concept, the +``RemoteAddrValve`` only checks addresses, not port numbers (although +it should be easy to add port checking) and they don't offer anything +better than what is described in `Locking Down the Apache to Java EE +Container Channel`_, which is not container specific. Servlet filters +are always available regardless of the container the servlet is +running in. A filter can check both the address and port number and +refuse to operate on the request if the address and port are not known to +be a trusted authentication proxy. Also note that if the Java EE +Container is configured to accept connections other than from the +trusted HTTP proxy server (a very likely scenario) then filtering at +the connector level is not sufficient because a servlet which trusts +``REMOTE_USER`` must be assured the request arrived only on a +trusted HTTP proxy server connection, not one of the other possible +connections. + +SSL/TLS with client auth +------------------------ + +SSL with client authentication is the ultimate way to lock down a HTTP +Server to Java EE Container proxy connection. SSL with client +authentication provides authenticity, integrity, and +confidentiality. However those desirable attributes come at a +performance cost which may be excessive. Unless a persistent TCP +connection is established between the HTTP server and the Java EE +Container a SSL handshake will need to occur on each request being +proxied, SSL handshakes are expensive. Given that the HTTP server and +the Java EE Container will likely be deployed on the same compute node +(or at a minimum on a secure subnet) the advantage of SSL for proxy +connections may not be warranted because other options are available +for these configuration scenarios; see `Locking Down the Apache to Java EE +Container Channel`_. Also note that if the Java EE +Container is configured to accept connections other than from the +trusted HTTP proxy server (a very likely scenario), then filtering at +the connector level is not sufficient because a servlet which trusts +``REMOTE_USER`` must be assured that the request arrived only on a +trusted HTTP proxy server connection, not one of the other possible +connections. + + +Java Security Manager Permissions +--------------------------------- + +The Java Security Manager allows you define permissions which are +checked at run time before code executes. +``java.net.SocketPermission`` and ``java.net.NetPermission`` would +appear to offer solutions for restricting which host and port a +request containing ``REMOTE_USER`` will be trusted. However security +permissions are applied *after* a request is accepted by a +connector. They are also more geared towards what connections code can +subsequently utilize as opposed to what connection a request was +presented on. Therefore security manager permissions seem to offer little +value for our purpose. One can simply test to see which host sent the +proxy request and on what port it arrived on by looking at the +connection information in the request. Restricting which proxies can +submit trusted requests is better handled at the level of the +connector, which unfortunately is a container implementation +issue. Tomcat and Jetty have different ways of handling connector +specifications. + +AJP requiredSecret +------------------ + +The AJP protocol includes an attribute called ``requiredSecret``, which +can be used to secure the connection between AJP endpoints. When an +HTTP server sends an AJP proxy request to a Java EE Container it +embeds in the protocol transmission a string (``requiredSecret``) +known only to the HTTP server and the Java EE Container. The AJP +connector on the Java EE Container is configured with the +``requiredSecret`` value and will reject as unauthorized any AJP +requests whose ``requiredSecret`` does not match. + +There are two problems with `requiredSecret``. First of all it's not +particularly secure. In fact, it's fundamentally no different than +sending a cleartext password. If the AJP request is not encrypted it +means the ``requiredSecret`` will be sent in the clear which is +probably one of the most egregious security mistakes. If the AJP +request is transmitted in a manner where the traffic can be sniffed, it +would be trivial to recover the ``requiredSecret`` and forge a request +with it. On the other hand encrypting the communication channel +between the HTTP server and the Java EE Container means using SSL +which is fairly heavyweight. But more to the point, if one is using +SSL to encrypt the channel there is a *far better* mechanism to ensure +the HTTP server is who it claims to be than embedding +``requiredSecret``. If one is using SSL you might as well use SSL +client authentication where the HTTP identifies itself via a client +certificate. SSL client authentication is a very robust authentication +mechanism. But doing SSL client authentication, or for that matter +just SSL encryption, for *every* AJP protocol request is prohibitively +expensive from a performance standpoint. + +The second problem with ``requiredSecret`` is that despite being documented +in a number of places it's not actually implemented in Apache +``mod_proxy_ajp``. This is detailed in `bug 53098`_. You can set +``requiredSecret`` in the ``mod_proxy_ajp`` configuration, but it won't +be included in the wire protocol. There is a patch to implement +``requiredSecret`` but, it hasn't made it into any shipping version of +Apache yet. But even if ``requiredSecret`` was implemented it's not +useful. Also one could construct the equivalent of ``requiredSecret`` +from other AJP attributes and/or an HTTP extension header but those +would suffer from the same security issues ``requiredSecret`` has, +therefore it's mostly pointless. + +Java EE Container Issues +======================== + +Jetty Issues +------------ + +Jetty is a Java EE Container which can be used +as alternative to Tomcat. Jetty is an Eclipse project. Recent versions +of Jetty have dropped support for AJP; this is described in the +`Jetty AJP Configuration Guide`_ which states: + + Configuring AJP13 Using mod_jk or mod_proxy_ajp. Support for this + feature has been dropped with Jetty 9. If you feel this should be + brought back please file a bug. + +Eclipse `Bug 387928`_ *Retire jetty-ajp* was opened to track the +removal of AJP in Jetty and is now closed. + +Tomcat Issues +------------- + +You should refer the `Tomcat Security How-To`_ for a full discussion +of Tomcat security issues. + +The tomcatAuthentication attribute is used with the AJP connectors to +determine if Tomcat should authenticate the user or if authentication +can be delegated to the reverse proxy that will then pass the +authenticated username to Tomcat as part of the AJP protocol. + +The requiredSecret attribute in AJP connectors configures a shared +secret between Tomcat and the reverse proxy in front of Tomcat. It is used +to prevent unauthorized connections over AJP protocol. + +Locking Down the Apache to Java EE Container Channel +==================================================== + +The recommended approach to lock down the proxy channel is: + + * Run both Apache and the servlet container on the same host. + + * Configure Apache to forward the proxy request on the loopback + interface (e.g. 127.0.0.1 also known as ``localhost``). This + prohibits any external IP address from connecting, only processes + running on the locked down host can communicate over + ``localhost``. + + * Reserve one or more ports for communication **exclusively** for + proxy communication between Apache and the servlet container. The + servlet container may listen on other ports for non-critical + non-authenticated requests. + + * The ``ClaimAuthFilter`` that reads the identity metadata **must** + assure that requests have arrived only on a **trusted port**. To + achieve this the ``FederationConfiguration`` defines the + ``secureProxyPorts`` configuration option. ``secureProxyPorts`` is + a space delimited list of ports which during deployment the + administrator has configured such that they are **exclusively** + dedicated for use by the Apache server(s) providing authentication + and identity information. These ports are set in the servlet + container's ``Connector`` declarations. See `Declaring the + Connector Ports for Authentication Proxies`_ for more + information). + + * When the ``ClaimAuthFilter`` receives a request, the first thing + it does is check the ``ServletRequest.getLocalPort()`` value and + verifies it is a member of the ``secureProxyPorts`` configuration + option. If the port is a member of ``secureProxyPorts``, it will + trust every identity assertion found in the request. If the local + port is not a member of ``secureProxyPorts``, a HTTP 401 + (unauthorized) error status will be returned for the request. A + warning message will be logged the first time this occurs. + + +Declaring the Connector Ports for Authentication Proxies +-------------------------------------------------------- + +As described in `The Proxy Problem`_ the AAA authentication system +**must** confirm the request it is processing originated from a *trusted +HTTP proxy server*. This is accomplished with port isolation. + +The administrator deploying a federated AAA solution with SSSD +identity lookups must declare in the AAA federation configuration +which ports the proxy requests from the trusted HTTP server will +arrive on by setting the ``secureProxyPorts`` configuration +item. These ports **must** only be used for the trusted HTTP proxy +server. The AAA federation software will not perform authentication +for any request arriving on a port other than those listed in +``secureProxyPorts``. + +.. figure:: sssd_05.png + :align: center + + _`Figure 5.` + +``secureProxyPorts`` configuration option is set either in the +``federation.cfg`` file or in the +``org.opendaylight.aaa.federation.secureProxyPorts`` bundle +configuration. ``secureProxyPorts`` is a space-delimited list of port +numbers on which a trusted HTTP proxy performing authentication +forwards pre-authenticated requests. For example: + +:: + + secureProxyPorts=8383 + +Means a request which arrived on port 8383 is from a trusted HTTP +proxy server and the value of ``REMOTE_USER`` and other authentication +metadata in request can be trusted. + +######## +Appendix +######## + +***************** +CGI Export Issues +***************** + +Apache processes requests as a series of steps in a pipeline +fashion. The ordering of these steps is important. Core Apache is +fairly minimal, most of Apache's features are supplied by loadable +modules. When a module is loaded it registers a set of *hooks* +(function pointers) which are to be run at specific stages in the +Apache request processing pipeline. Thus a module can execute code at +any of a number of stages in the request pipeline. + +The user metadata supplied by Apache is initialized in two distinct +parts of Apache. + + 1. an authentication module (e.g. mod_auth_kerb) + 2. the ``mod_lookup_identity`` module. + +After successful authentication the authentication module will set the +name of the user principal and the mechanism used for authentication +in the request structure. + + * ``request->user`` + * ``request->ap_auth_type`` + +Authentication hooks run early in the request pipeline for the obvious +reason a request should not be processed if not authenticated. The +specific authentication module that runs is defined by ``Location`` +directive in the Apache configuration which binds specific +authentication to specific URL's. The ``mod_lookup_identity`` module +must run *after* authentication module runs because it depends on +knowing who the authenticated principal is so it can lookup the data +on that principal. + +When reading ``mod_lookup_identity`` documentation one often sees +references to the ``REMOTE_USER`` CGI environment variable with the +implication ``REMOTE_USER`` is how one accesses the name of the +authenticated principal. This is a bit misleading, ``REMOTE_USER`` is +a CGI environment variable. CGI environment variables are only set by +Apache when it believes the request is going to be processed by a CGI +implementation. In this case ``REMOTE_USER`` is initialized from the +``request->user`` value. + +How is the authenticated principal actually forwarded to our proxy? +=================================================================== + +If we are using the AJP proxy protocol the ``mod_proxy_ajp`` module +when preparing the proxy request will read the value of +``request->user`` and insert it into the ``SC_A_REMOTE_USER`` AJP +attribute. On the receiving end ``SC_A_REMOTE_USER`` will be extracted +from the AJP request and used to populate the value returned +by``HttpServletRequest.getRemoteUser()``. The exchange of the +authenticated principal when using AJP is transparent to both the +sender and receiver, nothing special needs to be done. See +`Transporting Identity Metadata from Apache to a Java EE Servlet`_ +for details on how metadata can be exchanged with the proxy. + +However, if AJP is not being used to proxy the request the +authenticated principal must be passed through some other mechanism, +an HTTP extension header is the obvious solution. The Apache +``mod_headers`` module can be used to add HTTP request headers to the +proxy request, for example: + +:: + + RequestHeader set MY_HEADER MY_VALUE + +Where does the value MY_VALUE come from? It can be hardcoded into the +``RequestHeader`` statement or it can reference an existing +environment variable like this: + +:: + + RequestHeader set MY_HEADER %{FOOBAR}e + +where the notation ``%{FOOBAR}e`` is the contents of the environment +variable FOOBAR. Thus we might expect we could do this: + +:: + + RequestHeader set REMOTE_USER %{REMOTE_USER}e + +The conundrum is the presumption the ``REMOTE_USER`` environment +variable has already been set at the time ``mod_headers`` executes the +``RequestHeader`` statement. Unfortunately this often is not the +case. + +The Apache environment variables ``REMOTE_USER`` and ``AUTH_TYPE`` are +set by the Apache function ``ap_add_common_vars()`` defined in +server/util_script.c. ``ap_add_common_vars()`` and is called by the +following modules: + + * mod_authnz_fcgi + * mod_proxy_fcgi + * mod_proxy_scgi + * mod_isapi + * mod_ext_filter + * mod_include + * mod_cgi + * mod_cgid + +Apache variables +================ + +Apache modules provide access to variables which can be referenced by +configuration directives. Unfortunately there isn't a lot of +uniformity to what the variables are and how they're referenced; it +mostly depends on how a given Apache module was implemented. As you +might imagine a bit of inconsistent historical cruft has accumulated +over the years, it can be confusing. The Apache Foundation is trying +to clean some of this up bringing uniformity to modules by utilizing +the common ``expr`` (expression) module `ap_expr`_. The idea being modules will +forgo their home grown expression syntax with its numerous quirks and +instead expose the common ``expr`` language. However this is a work in +progress and at the time of this writing only a few modules have acquired +``expr`` expression support. + +Among the existing Apache modules there currently are three different +sets of variables. + + 1. Server variables. + 2. Environment variables. + 3. SSL variables. + +Server variables (item 1) are names given to internal values. The set +of names for server variables and what they map to are defined by the +module implementing the server variable lookup. For example +``mod_rewrite`` has its own variable lookup implementation. + +Environment variables (item 2) are variables *exported* to a +subprocess. Internally they are stored in +``request->subprocess_env``. The most common use of environment +variables exported to a subprocess are the CGI variables. + +SSL variables are connection specific values describing the SSL +connection. The lookup is implemented by ``ssl_var_lookup()``, which +given a variable name looks in a variety of internal data structures to +find the matching value. + +The important thing to remember is **server variables != environment +variables**. This can be confusing because they often share the same +name. For example, there is the server variable ``REMOTE_USER`` and +there is the environment variable ``REMOTE_USER``. The environment +variable ``REMOTE_USER`` only exists if some module has called +``ap_add_common_vars()``. To complicate matters, some modules allow you +to access *server variables*, other modules allow you to access +*environment variables* and some modules provide access to both +*server variables* and *environment variables*. + +Coming back to our goal of setting an HTTP extension header to the +value of ``REMOTE_USER``, we observe that ``mod_headers`` provides the +needed ``RequestHeader`` operation to set a HTTP header in the +request. Looking at the documentation for ``RequestHeader`` we see a +value can be specified with one of the following lookups: + +%{VARNAME}e + The contents of the environment variable VARNAME. + +%{VARNAME}s + The contents of the SSL environment variable VARNAME, if mod_ssl is enabled. + +But wait! This only gives us access to *environment variables* and the +``REMOTE_USER`` environment variable is only set if +``ap_add_common_vars()`` is called by a module **after** an +authentication module runs! ``ap_add_common_vars()`` is usually only +invoked if the request is going to be passed to a CGI script. But +we're not doing CGI; instead we're proxying the request. The +likelihood the ``REMOTE_USER`` environment variable will be set is +quite low. See `Setting the REMOTE_USER environment variable`_. + +``mod_headers`` is the only way to set a HTTP extension header and +``mod_headers`` only gives you access to environment variables and the +``REMOTE_USER`` environment variable is not set. Therefore if we're +not using AJP and must depend on setting a HTTP extension header for +``REMOTE_USER``, we have a **serious problem**. + +But there is a solution; you can either try the machinations described +in `Setting the REMOTE_USER environment variable`_ or assure you're +running at least Apache version 2.4.10. In Apache 2.4.10 the +``mod_headers`` module added support for `ap_expr`_. `ap_expr`_ +provides access to *server variables* by using the ``%{VARIABLE}`` +notation. `ap_expr`_ also can lookup subprocess environment variables +and operating system environment variables using its ``reqenv()`` and +``osenv()`` functions respectively. + +Thus the simple solution for exporting the ``REMOTE_USER`` HTTP +extension header if you're running Apache 2.4.10 or later is: + +:: + + RequestHeader set X-SSSD-REMOTE_USER expr=%{REMOTE_USER} + +The ``expr=%{REMOTE_USER}`` in the above statement says pass +``%{REMOTE_USER}`` as an expression to `ap_expr`_, evaluate the +expression and return the value. In this case the expression +``%{REMOTE_USER}`` is very simple, just the value of the server +variables ``REMOTE_USER``. Because ``RequestHeader`` runs after +authentication ``request->user`` will have been set. + +Setting the REMOTE_USER environment variable +============================================ + +If you do a web search on how to export ``REMOTE_USER`` in a HTTP +extension header for a proxy you will discover this is a common +problem that has frustrated a lot of people [2]_. The usual advice seems to +be to use ``mod_rewrite`` with a look-ahead. In fact this is even +documented in the `mod_rewrite documentation for REMOTE_USER`_ which says: + + %{LA-U:variable} can be used for look-aheads which perform an + internal (URL-based) sub-request to determine the final value of + variable. This can be used to access variable for rewriting which is + not available at the current stage, but will be set in a later + phase. + + For instance, to rewrite according to the REMOTE_USER variable from + within the per-server context (httpd.conf file) you must use + %{LA-U:REMOTE_USER} - this variable is set by the authorization + phases, which come after the URL translation phase (during which + mod_rewrite operates). + +One suggested solution is this: + +:: + + RewriteCond %{LA-U:REMOTE_USER} (.+) + RewriteRule .* - [E=RU:%1] + RequestHeader set X_REMOTE_USER %{RU}e + +1. The RewriteCond with the %{LA-U:} construct performs an internal + redirect to obtain the value of ``REMOTE_USER`` *server variable*, + if that value is non-empty because the (.+) regular expression + matched the rewrite condition succeeds and the following + RewriteRule executes. + +2. The RewriteRule executes, the first parameter is a pattern, the + second parameter is the replacement which can be followed by + optional flags inside brackets. The .* pattern is a regular + expression that matches anything, the - replacement is a special + value which indicates no replacement is to be performed. In other + words the pattern and replacement are no-ops and the RewriteRule is + just being used for it's side effect defined in the flags. The + E=NAME:VALUE notation says set the NAME environment variable to + VALUE. In this case the environment variable is RU and the value is + %1. The documentation for RewriteRule tells us that %N are + back-references to the last matched RewriteCond pattern, in this + case it's the value of ``REMOTE_USER``. + +3. Finally ``RequestHeader`` sets the request header + ``X_REMOTE_USER`` to the value of the ``RU`` environment variable. + +Another suggested solution is this: + +:: + + RewriteRule .* - [E=REMOTE_USER:%{LA-U:REMOTE_USER}] + +The Problem with mod_rewrite lookahead +-------------------------------------- + +I **do not recommend** using mod_rewrite's lookahead to gain access to +authentication data values. Although the above suggestions will work +to get access to ``REMOTE_USER`` it is *extremely inefficient* because +it causes Apache to reprocess the request with an internal +redirect. The documentation suggests a lookahead reference will cause +one internal redirect. However from examining Apache debug logs the +``mod_rewite`` lookahead caused ``mod_lookup_identity`` to be invoked +**11 times** while handling one request. If the ``mod_rewrite`` +lookahead is removed and another technique is used to get access to +``REMOTE_USER`` then ``mod_lookup_identity`` is invoked exactly once +as expected. + +But it's not just ``REMOTE_USER`` which we need access to, we also need +to reference ``AUTH_TYPE`` which has the identical issues associated +with ``REMOTE_USER``. If an equivalent ``mod_rewrite`` block is added +to the configuration for ``AUTH_TYPE`` so that both ``REMOTE_USER`` +and ``auth_type`` are resolved using a lookahead Apache appears to go +into an infinite loop and the request stalls. + +I tried to debug what was occurring when Apache was configured this way +and why it seemed to be executing the same code over and over but I +was not able to figure it out. My conclusion is **using mod_rewrite +lookahead's is not a viable solution!** Other web posts also make +reference to the inefficiency but they seem to be unaware of just how +bad it is. + +.. [1] + Tomcat has a bug/feature, not all attributes are enumerated by + getAttributeNames() therefore getAttributeNames() cannot be used to + obtain the full set of attributes. However if you know the name of + the attribute a priori you can call getAttribute() and obtain the + value. Therefore we maintain a list of attribute names + (httpAttributes) which will be used to call getAttribute() with so we + don't miss essential attributes. + + This is the Tomcat bug, note it is marked WONTFIX. Bug 25363 - + request.getAttributeNames() not working properly Status: RESOLVED + WONTFIX https://issues.apache.org/bugzilla/show_bug.cgi?id=25363 + + The solution adopted by Tomcat is to document the behavior in the + "The Apache Tomcat Connector - Reference Guide" under the JkEnvVar + property where is says: + + You can retrieve the variables on Tomcat as request attributes via + request.getAttribute(attributeName). Note that the variables send via + JkEnvVar will not be listed in request.getAttributeNames(). + +.. [2] + Some examples of posts concerning the export of ``REMOTE_USER`` include: + http://www.jaddog.org/2010/03/22/how-to-proxy-pass-remote_user/ and + http://serverfault.com/questions/23273/apache-proxy-passing-on-remote-user-to-backend-server/ + +.. [3] + The ``mod_lookup_identity`` ``LookupUserGroups`` option accepts an + optional parameter to specify the separator used to separate group + names. By convention this is normally the colon (:) character. In + our examples we explicitly specify the colon separator because the + mapping rules split the value found in ``REMOTE_USER_GROUPS`` on + the colon character. + +.. [4] + The example of using the `The Mapping Rule Processor`_ to establish + the set of roles assigned to a user based on group membership is + for illustrative purposes in order to show features of the + federated IdP and mapping mechanism. Role assignment in AAA may be + done in other ways. For example an unscoped token without roles can + be used to acquire a scoped token with roles by presenting it to + the appropriate REST API endpoint. In actual deployments this may + be preferable because it places the responsibility of deciding who + has what role/permission on what part of the controller/network + resources more in the hands of the SDN controller administrator + than the IdP administrator. + +.. _FreeIPA: http://www.freeipa.org/ + +.. _SSSD: https://fedorahosted.org/sssd/ + +.. _mod_identity_lookup: http://www.adelton.com/apache/mod_lookup_identity/ + +.. _AJP: http://tomcat.apache.org/connectors-doc/ajp/ajpv13a.html + +.. _Tomcat Security How-To: http://tomcat.apache.org/tomcat-7.0-doc/security-howto.html + +.. _The Apache Tomcat Connector - Generic HowTo: http://tomcat.apache.org/connectors-doc/generic_howto/printer/proxy.html + +.. _CGI RFC: http://www.ietf.org/rfc/rfc3875 + +.. _ap_expr: http://httpd.apache.org/docs/current/expr.html + +.. _mod_rewrite documentation for REMOTE_USER: http://httpd.apache.org/docs/current/mod/mod_rewrite.html#rewritecond + +.. _bug 53098: https://issues.apache.org/bugzilla/show_bug.cgi?id=53098 + +.. _Jetty AJP Configuration Guide: http://wiki.eclipse.org/Jetty/Howto/Configure_AJP13 + +.. _Bug 387928: https://bugs.eclipse.org/bugs/show_bug.cgi?id=387928 + +.. _Tomcat Remote Address Valve: http://tomcat.apache.org/tomcat-7.0-doc/config/valve.html#Remote_Address_Filter diff --git a/odl-aaa-moon/aaa/aaa-authn-api/src/main/java/org/opendaylight/aaa/api/Authentication.java b/odl-aaa-moon/aaa/aaa-authn-api/src/main/java/org/opendaylight/aaa/api/Authentication.java new file mode 100644 index 00000000..25ba898b --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn-api/src/main/java/org/opendaylight/aaa/api/Authentication.java @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2014 Hewlett-Packard Development Company, L.P. and others. 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.api; + +/** + * An immutable authentication context. + * + * @author liemmn + */ +public interface Authentication extends Claim { + + /** + * Get the authentication expiration date/time in number of milliseconds + * since start of epoch. + * + * @return expiration milliseconds since start of UTC epoch + */ + long expiration(); + +} diff --git a/odl-aaa-moon/aaa/aaa-authn-api/src/main/java/org/opendaylight/aaa/api/AuthenticationException.java b/odl-aaa-moon/aaa/aaa-authn-api/src/main/java/org/opendaylight/aaa/api/AuthenticationException.java new file mode 100644 index 00000000..d4621527 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn-api/src/main/java/org/opendaylight/aaa/api/AuthenticationException.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2014 Hewlett-Packard Development Company, L.P. and others. 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.api; + +/** + * A catch-all authentication exception. + * + * @author liemmn + * + */ +public class AuthenticationException extends RuntimeException { + private static final long serialVersionUID = -187422301135305719L; + + public AuthenticationException(String msg) { + super(msg); + } + + public AuthenticationException(String msg, Throwable cause) { + super(msg, cause); + } + + public AuthenticationException(Throwable cause) { + super(cause); + } +} diff --git a/odl-aaa-moon/aaa/aaa-authn-api/src/main/java/org/opendaylight/aaa/api/AuthenticationService.java b/odl-aaa-moon/aaa/aaa-authn-api/src/main/java/org/opendaylight/aaa/api/AuthenticationService.java new file mode 100644 index 00000000..24ae9238 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn-api/src/main/java/org/opendaylight/aaa/api/AuthenticationService.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2014 Hewlett-Packard Development Company, L.P. and others. 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.api; + +/** + * Authentication service to provide authentication context. + */ +public interface AuthenticationService { + /** + * Retrieve the current security context, or null if none exists. + * + * @return security context + */ + Authentication get(); + + /** + * Set the current security context. Only {@link TokenAuth} should set + * security context based on the authentication result. + * + * @param auth + * security context + */ + void set(Authentication auth); + + /** + * Clear the current security context. + */ + void clear(); + + /** + * Checks to see if authentication is enabled. + * + * @return true if it is, false otherwise + */ + boolean isAuthEnabled(); +} diff --git a/odl-aaa-moon/aaa/aaa-authn-api/src/main/java/org/opendaylight/aaa/api/Claim.java b/odl-aaa-moon/aaa/aaa-authn-api/src/main/java/org/opendaylight/aaa/api/Claim.java new file mode 100644 index 00000000..7d9a229a --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn-api/src/main/java/org/opendaylight/aaa/api/Claim.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2014, 2015 Hewlett-Packard Development Company, L.P. and others. 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.api; + +import java.util.Set; + +/** + * A claim typically provided by an identity provider after validating the + * needed identity and credentials. + * + * @author liemmn + * + */ +public interface Claim { + /** + * Get the id of the authorized client. If the id is an empty string, it + * means that the client is anonymous. + * + * @return id of the authorized client, or empty string if anonymous + */ + String clientId(); + + /** + * Get the user id. User IDs are system-created. + * + * @return unique user id + */ + String userId(); + + /** + * Get the user name. User names are externally created. + * + * @return unique user name + */ + String user(); + + /** + * Get the fully-qualified domain name. Domain names are externally created. + * + * @return unique domain name, or empty string for a claim tied to no domain + */ + String domain(); + + /** + * Get a set of user roles. Roles are externally created. + * + * @return set of user roles + */ + Set<String> roles(); +}
\ No newline at end of file diff --git a/odl-aaa-moon/aaa/aaa-authn-api/src/main/java/org/opendaylight/aaa/api/ClaimAuth.java b/odl-aaa-moon/aaa/aaa-authn-api/src/main/java/org/opendaylight/aaa/api/ClaimAuth.java new file mode 100644 index 00000000..447ffb35 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn-api/src/main/java/org/opendaylight/aaa/api/ClaimAuth.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2014 Hewlett-Packard Development Company, L.P. and others. 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.api; + +import java.util.Map; + +/** + * An interface for in-bound claim transformation. + * + * @author liemmn + * + */ +public interface ClaimAuth { + + /** + * Transform a map of opaque in-bound claims into a {@link Claim} object. An + * example of an opaque claim map entry is + * <code>"USER_NAME" -> "joe".</code> + * <p> + * If there is no applicable claim information for the current + * implementation, this method should return a <code>null</code>. + * <p> + * In-bound claims are extracted from HttpServletRequest attributes, + * headers, and CGI variables as documented per Servlet specs. + * + * @param claim + * opaque claim + * @return normalized claim, or null if not applicable + */ + Claim transform(Map<String, Object> claim); +} diff --git a/odl-aaa-moon/aaa/aaa-authn-api/src/main/java/org/opendaylight/aaa/api/ClientService.java b/odl-aaa-moon/aaa/aaa-authn-api/src/main/java/org/opendaylight/aaa/api/ClientService.java new file mode 100644 index 00000000..c11eec1c --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn-api/src/main/java/org/opendaylight/aaa/api/ClientService.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2014 Hewlett-Packard Development Company, L.P. and others. 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.api; + +/** + * A service for managing authorized clients to the controller. + * + * @author liemmn + * + */ +public interface ClientService { + + void validate(String clientId, String clientSecret) throws AuthenticationException; +} diff --git a/odl-aaa-moon/aaa/aaa-authn-api/src/main/java/org/opendaylight/aaa/api/CredentialAuth.java b/odl-aaa-moon/aaa/aaa-authn-api/src/main/java/org/opendaylight/aaa/api/CredentialAuth.java new file mode 100644 index 00000000..341e49ae --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn-api/src/main/java/org/opendaylight/aaa/api/CredentialAuth.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2014 Hewlett-Packard Development Company, L.P. and others. 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.api; + +/** + * An interface for direct authentication with some given credentials. + * + * @author liemmn + */ +public interface CredentialAuth<T extends Credentials> { + + /** + * Authenticate a claim with the given credentials and domain scope. + * + * @param cred + * credentials + * @throws AuthenticationException + * if failed authentication + * @return authenticated claim + */ + Claim authenticate(T cred) throws AuthenticationException; +} diff --git a/odl-aaa-moon/aaa/aaa-authn-api/src/main/java/org/opendaylight/aaa/api/Credentials.java b/odl-aaa-moon/aaa/aaa-authn-api/src/main/java/org/opendaylight/aaa/api/Credentials.java new file mode 100644 index 00000000..7d2f19e5 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn-api/src/main/java/org/opendaylight/aaa/api/Credentials.java @@ -0,0 +1,15 @@ +/* + * Copyright (c) 2014 Hewlett-Packard Development Company, L.P. and others. 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.api; + +/** + * An interface to represent user credentials. + */ +public interface Credentials { +} diff --git a/odl-aaa-moon/aaa/aaa-authn-api/src/main/java/org/opendaylight/aaa/api/IDMStoreException.java b/odl-aaa-moon/aaa/aaa-authn-api/src/main/java/org/opendaylight/aaa/api/IDMStoreException.java new file mode 100644 index 00000000..026c11ce --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn-api/src/main/java/org/opendaylight/aaa/api/IDMStoreException.java @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2015 Cisco Systems, Inc. and others. 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.api; + +/* + * @author - Sharon Aicler (saichler@cisco.com) + */ +public class IDMStoreException extends Exception { + + private static final long serialVersionUID = -7534127680943957878L; + + public IDMStoreException(Exception e) { + super(e); + } + + public IDMStoreException(String msg) { + super(msg); + } +} diff --git a/odl-aaa-moon/aaa/aaa-authn-api/src/main/java/org/opendaylight/aaa/api/IDMStoreUtil.java b/odl-aaa-moon/aaa/aaa-authn-api/src/main/java/org/opendaylight/aaa/api/IDMStoreUtil.java new file mode 100644 index 00000000..07dd522f --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn-api/src/main/java/org/opendaylight/aaa/api/IDMStoreUtil.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2015 Cisco Systems, Inc. and others. 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.api; + +import javax.naming.OperationNotSupportedException; + +/* + * This class is a utility to construct the different elements keys for the different data stores. + * For not making mistakes around the code constructing an element key, this class standardize the + * way the key is constructed to be used by the different data stores. + * + * @author - Sharon Aicler (saichler@cisco.com) + */ + +public class IDMStoreUtil { + private IDMStoreUtil() throws OperationNotSupportedException { + throw new OperationNotSupportedException(); + } + + public static String createDomainid(String domainName) { + return domainName; + } + + public static String createUserid(String username, String domainid) { + return username + "@" + domainid; + } + + public static String createRoleid(String rolename, String domainid) { + return rolename + "@" + domainid; + } + + public static String createGrantid(String userid, String domainid, String roleid) { + return userid + "@" + roleid + "@" + domainid; + } +} diff --git a/odl-aaa-moon/aaa/aaa-authn-api/src/main/java/org/opendaylight/aaa/api/IIDMStore.java b/odl-aaa-moon/aaa/aaa-authn-api/src/main/java/org/opendaylight/aaa/api/IIDMStore.java new file mode 100644 index 00000000..7b031e05 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn-api/src/main/java/org/opendaylight/aaa/api/IIDMStore.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2015 Cisco Systems, Inc. and others. 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.api; + +import org.opendaylight.aaa.api.model.Domain; +import org.opendaylight.aaa.api.model.Domains; +import org.opendaylight.aaa.api.model.Grant; +import org.opendaylight.aaa.api.model.Grants; +import org.opendaylight.aaa.api.model.Role; +import org.opendaylight.aaa.api.model.Roles; +import org.opendaylight.aaa.api.model.User; +import org.opendaylight.aaa.api.model.Users; + +/** + * @author - Sharon Aicler (saichler@cisco.com) + **/ +public interface IIDMStore { + public String DEFAULT_DOMAIN = "sdn"; + + // Domain methods + public Domain writeDomain(Domain domain) throws IDMStoreException; + + public Domain readDomain(String domainid) throws IDMStoreException; + + public Domain deleteDomain(String domainid) throws IDMStoreException; + + public Domain updateDomain(Domain domain) throws IDMStoreException; + + public Domains getDomains() throws IDMStoreException; + + // Role methods + public Role writeRole(Role role) throws IDMStoreException; + + public Role readRole(String roleid) throws IDMStoreException; + + public Role deleteRole(String roleid) throws IDMStoreException; + + public Role updateRole(Role role) throws IDMStoreException; + + public Roles getRoles() throws IDMStoreException; + + // User methods + public User writeUser(User user) throws IDMStoreException; + + public User readUser(String userid) throws IDMStoreException; + + public User deleteUser(String userid) throws IDMStoreException; + + public User updateUser(User user) throws IDMStoreException; + + public Users getUsers() throws IDMStoreException; + + public Users getUsers(String username, String domain) throws IDMStoreException; + + // Grant methods + public Grant writeGrant(Grant grant) throws IDMStoreException; + + public Grant readGrant(String grantid) throws IDMStoreException; + + public Grant deleteGrant(String grantid) throws IDMStoreException; + + public Grants getGrants(String domainid, String userid) throws IDMStoreException; + + public Grants getGrants(String userid) throws IDMStoreException; + + public Grant readGrant(String domainid, String userid, String roleid) throws IDMStoreException; +} diff --git a/odl-aaa-moon/aaa/aaa-authn-api/src/main/java/org/opendaylight/aaa/api/IdMService.java b/odl-aaa-moon/aaa/aaa-authn-api/src/main/java/org/opendaylight/aaa/api/IdMService.java new file mode 100644 index 00000000..1d698da5 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn-api/src/main/java/org/opendaylight/aaa/api/IdMService.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2014 Hewlett-Packard Development Company, L.P. and others. 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.api; + +import java.util.List; + +/** + * A service to provide identity information. + * + * @author liemmn + * + */ +public interface IdMService { + /** + * List all domains that the given user has at least one role on. + * + * @param userId + * id of user + * @return list of all domains that the given user has access to + */ + List<String> listDomains(String userId); + + /** + * List all roles that the given user has on the given domain. + * + * @param userId + * id of user + * @param domain + * domain + * @return list of roles + */ + List<String> listRoles(String userId, String domain); +} diff --git a/odl-aaa-moon/aaa/aaa-authn-api/src/main/java/org/opendaylight/aaa/api/PasswordCredentials.java b/odl-aaa-moon/aaa/aaa-authn-api/src/main/java/org/opendaylight/aaa/api/PasswordCredentials.java new file mode 100644 index 00000000..e5fa346d --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn-api/src/main/java/org/opendaylight/aaa/api/PasswordCredentials.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2014 Hewlett-Packard Development Company, L.P. and others. 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.api; + +/** + * Good 'ole username/password. + */ +public interface PasswordCredentials extends Credentials { + String username(); + + String password(); + + String domain(); +} diff --git a/odl-aaa-moon/aaa/aaa-authn-api/src/main/java/org/opendaylight/aaa/api/SHA256Calculator.java b/odl-aaa-moon/aaa/aaa-authn-api/src/main/java/org/opendaylight/aaa/api/SHA256Calculator.java new file mode 100644 index 00000000..903fe3de --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn-api/src/main/java/org/opendaylight/aaa/api/SHA256Calculator.java @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2015 Cisco Systems, Inc. and others. 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.api; + +import java.security.MessageDigest; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock.WriteLock; +import javax.xml.bind.DatatypeConverter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * @author Sharon Aicler (saichler@cisco.com) + */ +public class SHA256Calculator { + + private static final Logger LOG = LoggerFactory.getLogger(SHA256Calculator.class); + + private static MessageDigest md = null; + private static ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); + private static WriteLock writeLock = lock.writeLock(); + + public static String generateSALT() { + StringBuffer salt = new StringBuffer(); + for (int i = 0; i < 12; i++) { + int random = (int) (Math.random() * 24 + 1); + salt.append((char) (65 + random)); + } + return salt.toString(); + } + + public static String getSHA256(byte data[], String salt) { + byte SALT[] = salt.getBytes(); + byte temp[] = new byte[data.length + SALT.length]; + System.arraycopy(data, 0, temp, 0, data.length); + System.arraycopy(SALT, 0, temp, data.length, SALT.length); + + if (md == null) { + try { + writeLock.lock(); + if (md == null) { + try { + md = MessageDigest.getInstance("SHA-256"); + } catch (Exception err) { + LOG.error("Error calculating SHA-256 for SALT", err); + } + } + } finally { + writeLock.unlock(); + } + } + + byte by[] = null; + + try { + writeLock.lock(); + md.update(temp); + by = md.digest(); + } finally { + writeLock.unlock(); + } + //Make sure the outcome hash does not contain special characters + return DatatypeConverter.printBase64Binary(by); + } + + public static String getSHA256(String password, String salt) { + return getSHA256(password.getBytes(), salt); + } +} diff --git a/odl-aaa-moon/aaa/aaa-authn-api/src/main/java/org/opendaylight/aaa/api/TokenAuth.java b/odl-aaa-moon/aaa/aaa-authn-api/src/main/java/org/opendaylight/aaa/api/TokenAuth.java new file mode 100644 index 00000000..bbf6fa2b --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn-api/src/main/java/org/opendaylight/aaa/api/TokenAuth.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2014 Hewlett-Packard Development Company, L.P. and others. 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.api; + +import java.util.List; +import java.util.Map; + +/** + * An interface for in-bound token authentication. + * + * @author liemmn + */ +public interface TokenAuth { + + /** + * Validate the given token contained in the in-bound headers. + * <p> + * If there is no token signature in the given headers for this + * implementation, this method should return a null. If there is an + * applicable token signature, but the token validation fails, this method + * should throw an {@link AuthenticationException}. + * + * @param headers + * headers containing token to validate + * @return authenticated context, or null if not applicable + * @throws AuthenticationException + * if authentication fails + */ + Authentication validate(Map<String, List<String>> headers) throws AuthenticationException; + +} diff --git a/odl-aaa-moon/aaa/aaa-authn-api/src/main/java/org/opendaylight/aaa/api/TokenStore.java b/odl-aaa-moon/aaa/aaa-authn-api/src/main/java/org/opendaylight/aaa/api/TokenStore.java new file mode 100644 index 00000000..4cd7aa78 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn-api/src/main/java/org/opendaylight/aaa/api/TokenStore.java @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2014 Hewlett-Packard Development Company, L.P. and others. 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.api; + +/** + * A datastore for auth tokens. + * + * @author liemmn + * + */ +public interface TokenStore { + void put(String token, Authentication auth); + + Authentication get(String token); + + boolean delete(String token); + + long tokenExpiration(); +} diff --git a/odl-aaa-moon/aaa/aaa-authn-api/src/main/java/org/opendaylight/aaa/api/model/Claim.java b/odl-aaa-moon/aaa/aaa-authn-api/src/main/java/org/opendaylight/aaa/api/model/Claim.java new file mode 100644 index 00000000..180bddfb --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn-api/src/main/java/org/opendaylight/aaa/api/model/Claim.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2014, 2015 Hewlett-Packard Development Company, L.P. and others. 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.api.model; + +/** + * + * @author peter.mellquist@hp.com + * + */ + +import java.util.List; + +import javax.xml.bind.annotation.XmlRootElement; + +@XmlRootElement(name = "Claim") +public class Claim { + private String domainid; + private String userid; + private String username; + private List<Role> roles; + + public String getDomainid() { + return domainid; + } + + public void setDomainid(String id) { + this.domainid = id; + } + + public String getUserid() { + return userid; + } + + public void setUserid(String id) { + this.userid = id; + } + + public String getUsername() { + return username; + } + + public void setUsername(String name) { + this.username = name; + } + + public List<Role> getRoles() { + return roles; + } + + public void setRoles(List<Role> roles) { + this.roles = roles; + } + +}
\ No newline at end of file diff --git a/odl-aaa-moon/aaa/aaa-authn-api/src/main/java/org/opendaylight/aaa/api/model/Domain.java b/odl-aaa-moon/aaa/aaa-authn-api/src/main/java/org/opendaylight/aaa/api/model/Domain.java new file mode 100644 index 00000000..a42e0b6d --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn-api/src/main/java/org/opendaylight/aaa/api/model/Domain.java @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2014, 2015 Hewlett-Packard Development Company, L.P. and others. 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.api.model; + +/** + * + * @author peter.mellquist@hp.com + * + */ + +import javax.xml.bind.annotation.XmlRootElement; + +@XmlRootElement(name = "domain") +public class Domain { + private String domainid; + private String name; + private String description; + private Boolean enabled; + + public String getDomainid() { + return domainid; + } + + public void setDomainid(String id) { + this.domainid = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public Boolean isEnabled() { + return enabled; + } + + public void setEnabled(Boolean enabled) { + this.enabled = enabled; + } + + @Override + public int hashCode() { + return this.name.hashCode(); + } + + @Override + public boolean equals(Object obj) { + Domain other = (Domain) obj; + if (other == null) + return false; + if (compareValues(getName(), other.getName()) + && compareValues(getDomainid(), other.getDomainid()) + && compareValues(getDescription(), other.getDescription())) + return true; + return false; + } + + private boolean compareValues(Object a, Object b) { + if (a == null && b != null) + return false; + if (a != null && b == null) + return false; + if (a == null && b == null) + return true; + if (a.equals(b)) + return true; + return false; + } +} diff --git a/odl-aaa-moon/aaa/aaa-authn-api/src/main/java/org/opendaylight/aaa/api/model/Domains.java b/odl-aaa-moon/aaa/aaa-authn-api/src/main/java/org/opendaylight/aaa/api/model/Domains.java new file mode 100644 index 00000000..a8f2064b --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn-api/src/main/java/org/opendaylight/aaa/api/model/Domains.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2014, 2015 Hewlett-Packard Development Company, L.P. and others. 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.api.model; + +/** + * + * @author peter.mellquist@hp.com + * + */ + +import java.util.ArrayList; +import java.util.List; + +import javax.xml.bind.annotation.XmlRootElement; + +@XmlRootElement(name = "domains") +public class Domains { + private List<Domain> domains = new ArrayList<Domain>(); + + public void setDomains(List<Domain> domains) { + this.domains = domains; + } + + public List<Domain> getDomains() { + return domains; + } + +} diff --git a/odl-aaa-moon/aaa/aaa-authn-api/src/main/java/org/opendaylight/aaa/api/model/Grant.java b/odl-aaa-moon/aaa/aaa-authn-api/src/main/java/org/opendaylight/aaa/api/model/Grant.java new file mode 100644 index 00000000..20c2d128 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn-api/src/main/java/org/opendaylight/aaa/api/model/Grant.java @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2014, 2015 Hewlett-Packard Development Company, L.P. and others. 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.api.model; + +/** + * + * @author peter.mellquist@hp.com + * + */ + +import javax.xml.bind.annotation.XmlRootElement; + +@XmlRootElement(name = "grant") +public class Grant { + private String grantid; + private String domainid; + private String userid; + private String roleid; + + public String getGrantid() { + return this.grantid; + } + + public void setGrantid(String id) { + this.grantid = id; + } + + public String getDomainid() { + return domainid; + } + + public void setDomainid(String id) { + this.domainid = id; + } + + public String getUserid() { + return userid; + } + + public void setUserid(String id) { + this.userid = id; + } + + public String getRoleid() { + return roleid; + } + + public void setRoleid(String id) { + this.roleid = id; + } + + @Override + public int hashCode() { + return this.getUserid().hashCode(); + } + + @Override + public boolean equals(Object obj) { + Grant other = (Grant) obj; + if (other == null) + return false; + if (compareValues(getDomainid(), other.getDomainid()) + && compareValues(getRoleid(), other.getRoleid()) + && compareValues(getUserid(), other.getUserid())) + return true; + return false; + } + + private boolean compareValues(Object a, Object b) { + if (a == null && b != null) + return false; + if (a != null && b == null) + return false; + if (a == null && b == null) + return true; + if (a.equals(b)) + return true; + return false; + } +} diff --git a/odl-aaa-moon/aaa/aaa-authn-api/src/main/java/org/opendaylight/aaa/api/model/Grants.java b/odl-aaa-moon/aaa/aaa-authn-api/src/main/java/org/opendaylight/aaa/api/model/Grants.java new file mode 100644 index 00000000..ce0d9b85 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn-api/src/main/java/org/opendaylight/aaa/api/model/Grants.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2014, 2015 Hewlett-Packard Development Company, L.P. and others. 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.api.model; + +/** + * + * @author peter.mellquist@hp.com + * + */ + + +import java.util.ArrayList; +import java.util.List; + +import javax.xml.bind.annotation.XmlRootElement; + +@XmlRootElement(name = "grants") +public class Grants { + private List<Grant> grants = new ArrayList<Grant>(); + + public void setGrants(List<Grant> grants) { + this.grants = grants; + } + + public List<Grant> getGrants() { + return grants; + } + +} diff --git a/odl-aaa-moon/aaa/aaa-authn-api/src/main/java/org/opendaylight/aaa/api/model/IDMError.java b/odl-aaa-moon/aaa/aaa-authn-api/src/main/java/org/opendaylight/aaa/api/model/IDMError.java new file mode 100644 index 00000000..f44c43d9 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn-api/src/main/java/org/opendaylight/aaa/api/model/IDMError.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2014, 2015 Hewlett-Packard Development Company, L.P. and others. 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.api.model; + +/** + * + * @author peter.mellquist@hp.com + * + */ + +import javax.ws.rs.core.Response; +import javax.xml.bind.annotation.XmlRootElement; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@XmlRootElement(name = "idmerror") +public class IDMError { + private static final Logger LOG = LoggerFactory.getLogger(IDMError.class); + + private String message; + private String details; + private int code = 500; + + public IDMError() { + }; + + public IDMError(int statusCode, String msg, String msgDetails) { + code = statusCode; + message = msg; + details = msgDetails; + } + + public String getMessage() { + return message; + } + + public void setMessage(String msg) { + this.message = msg; + } + + public String getDetails() { + return details; + } + + public void setDetails(String details) { + this.details = details; + } + + public Response response() { + LOG.error("error: {} details: {} status: {}", this.message, this.details, code); + return Response.status(this.code).entity(this).build(); + } + +}
\ No newline at end of file diff --git a/odl-aaa-moon/aaa/aaa-authn-api/src/main/java/org/opendaylight/aaa/api/model/Role.java b/odl-aaa-moon/aaa/aaa-authn-api/src/main/java/org/opendaylight/aaa/api/model/Role.java new file mode 100644 index 00000000..de707496 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn-api/src/main/java/org/opendaylight/aaa/api/model/Role.java @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2014, 2015 Hewlett-Packard Development Company, L.P. and others. 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.api.model; + +/** + * + * @author peter.mellquist@hp.com + * + */ + +import javax.xml.bind.annotation.XmlRootElement; + +@XmlRootElement(name = "role") +public class Role { + private String roleid; + private String name; + private String description; + private String domainid; + + public String getRoleid() { + return roleid; + } + + public void setRoleid(String id) { + this.roleid = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + @Override + public int hashCode() { + return this.name.hashCode(); + } + + @Override + public boolean equals(Object obj) { + Role other = (Role) obj; + if (other == null) + return false; + if (compareValues(getName(), other.getName()) + && compareValues(getRoleid(), other.getRoleid()) + && compareValues(getDescription(), other.getDescription())) + return true; + return false; + } + + public void setDomainid(String domainid) { + this.domainid = domainid; + } + + public String getDomainid() { + return this.domainid; + } + + private boolean compareValues(Object a, Object b) { + if (a == null && b != null) + return false; + if (a != null && b == null) + return false; + if (a == null && b == null) + return true; + if (a.equals(b)) + return true; + return false; + } +} diff --git a/odl-aaa-moon/aaa/aaa-authn-api/src/main/java/org/opendaylight/aaa/api/model/Roles.java b/odl-aaa-moon/aaa/aaa-authn-api/src/main/java/org/opendaylight/aaa/api/model/Roles.java new file mode 100644 index 00000000..33521028 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn-api/src/main/java/org/opendaylight/aaa/api/model/Roles.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2014, 2015 Hewlett-Packard Development Company, L.P. and others. 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.api.model; + +/** + * + * @author peter.mellquist@hp.com + * + */ + +import java.util.ArrayList; +import java.util.List; + +import javax.xml.bind.annotation.XmlRootElement; + +@XmlRootElement(name = "roles") +public class Roles { + private List<Role> roles = new ArrayList<Role>(); + + public void setRoles(List<Role> roles) { + this.roles = roles; + } + + public List<Role> getRoles() { + return roles; + } + +} diff --git a/odl-aaa-moon/aaa/aaa-authn-api/src/main/java/org/opendaylight/aaa/api/model/User.java b/odl-aaa-moon/aaa/aaa-authn-api/src/main/java/org/opendaylight/aaa/api/model/User.java new file mode 100644 index 00000000..c6c1f9a6 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn-api/src/main/java/org/opendaylight/aaa/api/model/User.java @@ -0,0 +1,126 @@ +/* + * Copyright (c) 2014, 2015 Hewlett-Packard Development Company, L.P. and others. 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.api.model; + +/** + * + * @author peter.mellquist@hp.com + * + */ + +import javax.xml.bind.annotation.XmlRootElement; + +@XmlRootElement(name = "user") +public class User { + private String userid; + private String name; + private String description; + private Boolean enabled; + private String email; + private String password; + private String salt; + private String domainid; + + public String getUserid() { + return userid; + } + + public void setUserid(String id) { + this.userid = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public Boolean isEnabled() { + return enabled; + } + + public void setEnabled(Boolean enabled) { + this.enabled = enabled; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getEmail() { + return email; + } + + public void setPassword(String password) { + this.password = password; + } + + public String getPassword() { + return password; + } + + public void setSalt(String s) { + this.salt = s; + } + + public String getSalt() { + return this.salt; + } + + public String getDomainid() { + return domainid; + } + + public void setDomainid(String domainid) { + this.domainid = domainid; + } + + @Override + public int hashCode() { + return this.name.hashCode(); + } + + @Override + public boolean equals(Object obj) { + User other = (User) obj; + if (other == null) + return false; + if (compareValues(getName(), other.getName()) + && compareValues(getEmail(), other.getEmail()) + && compareValues(isEnabled(), other.isEnabled()) + && compareValues(getPassword(), other.getPassword()) + && compareValues(getSalt(), other.getSalt()) + && compareValues(getUserid(), other.getUserid()) + && compareValues(getDescription(), other.getDescription())) + return true; + return false; + } + + private boolean compareValues(Object a, Object b) { + if (a == null && b != null) + return false; + if (a != null && b == null) + return false; + if (a == null && b == null) + return true; + if (a.equals(b)) + return true; + return false; + } +} diff --git a/odl-aaa-moon/aaa/aaa-authn-api/src/main/java/org/opendaylight/aaa/api/model/UserPwd.java b/odl-aaa-moon/aaa/aaa-authn-api/src/main/java/org/opendaylight/aaa/api/model/UserPwd.java new file mode 100644 index 00000000..4750616d --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn-api/src/main/java/org/opendaylight/aaa/api/model/UserPwd.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2014, 2015 Hewlett-Packard Development Company, L.P. and others. 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.api.model; + +/** + * + * @author peter.mellquist@hp.com + * + */ + +import javax.xml.bind.annotation.XmlRootElement; + +@XmlRootElement(name = "userpwd") +public class UserPwd { + private String username; + private String userpwd; + + public String getUsername() { + return username; + } + + public void setUsername(String name) { + this.username = name; + } + + public String getUserpwd() { + return userpwd; + } + + public void setUserpwd(String pwd) { + this.userpwd = pwd; + } + +} diff --git a/odl-aaa-moon/aaa/aaa-authn-api/src/main/java/org/opendaylight/aaa/api/model/Users.java b/odl-aaa-moon/aaa/aaa-authn-api/src/main/java/org/opendaylight/aaa/api/model/Users.java new file mode 100644 index 00000000..a0a001bd --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn-api/src/main/java/org/opendaylight/aaa/api/model/Users.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2014, 2015 Hewlett-Packard Development Company, L.P. and others. 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.api.model; + +/** + * + * @author peter.mellquist@hp.com + * + */ + +import java.util.ArrayList; +import java.util.List; + +import javax.xml.bind.annotation.XmlRootElement; + +@XmlRootElement(name = "users") +public class Users { + private List<User> users = new ArrayList<User>(); + + public void setUsers(List<User> users) { + this.users = users; + } + + public List<User> getUsers() { + return users; + } + +} diff --git a/odl-aaa-moon/aaa/aaa-authn-api/src/main/java/org/opendaylight/aaa/api/model/Version.java b/odl-aaa-moon/aaa/aaa-authn-api/src/main/java/org/opendaylight/aaa/api/model/Version.java new file mode 100644 index 00000000..a88c1f80 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn-api/src/main/java/org/opendaylight/aaa/api/model/Version.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2014, 2015 Hewlett-Packard Development Company, L.P. and others. 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.api.model; + +/** + * + * @author peter.mellquist@hp.com + * + */ + +import javax.xml.bind.annotation.XmlRootElement; + +@XmlRootElement(name = "version") +public class Version { + private String id; + private String updated; + private String status; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getUpdated() { + return updated; + } + + public void setUpdated(String name) { + this.updated = name; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + +} diff --git a/odl-aaa-moon/aaa/aaa-authn-basic/pom.xml b/odl-aaa-moon/aaa/aaa-authn-basic/pom.xml new file mode 100644 index 00000000..47562896 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn-basic/pom.xml @@ -0,0 +1,76 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + <parent> + <groupId>org.opendaylight.aaa</groupId> + <artifactId>aaa-parent</artifactId> + <version>0.3.2-Beryllium-SR2</version> + <relativePath>../parent</relativePath> + </parent> + + <artifactId>aaa-authn-basic</artifactId> + <packaging>bundle</packaging> + + <dependencies> + <dependency> + <groupId>org.opendaylight.aaa</groupId> + <artifactId>aaa-authn</artifactId> + </dependency> + <dependency> + <groupId>org.opendaylight.aaa</groupId> + <artifactId>aaa-authn-api</artifactId> + </dependency> + <dependency> + <groupId>org.slf4j</groupId> + <artifactId>slf4j-api</artifactId> + </dependency> + <dependency> + <groupId>com.sun.jersey</groupId> + <artifactId>jersey-server</artifactId> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>org.osgi</groupId> + <artifactId>org.osgi.core</artifactId> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>org.apache.felix</groupId> + <artifactId>org.apache.felix.dependencymanager</artifactId> + <scope>provided</scope> + </dependency> + <!-- Testing Dependencies --> + <dependency> + <groupId>junit</groupId> + <artifactId>junit</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.slf4j</groupId> + <artifactId>slf4j-simple</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.mockito</groupId> + <artifactId>mockito-all</artifactId> + <scope>test</scope> + </dependency> + </dependencies> + + <build> + <plugins> + <plugin> + <groupId>org.apache.felix</groupId> + <artifactId>maven-bundle-plugin</artifactId> + <extensions>true</extensions> + <configuration> + <instructions> + <Bundle-Activator>org.opendaylight.aaa.basic.Activator</Bundle-Activator> + </instructions> + <manifestLocation>${project.basedir}/META-INF</manifestLocation> + </configuration> + </plugin> + </plugins> + </build> +</project> diff --git a/odl-aaa-moon/aaa/aaa-authn-basic/src/main/java/org/opendaylight/aaa/basic/Activator.java b/odl-aaa-moon/aaa/aaa-authn-basic/src/main/java/org/opendaylight/aaa/basic/Activator.java new file mode 100644 index 00000000..bd57c9d3 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn-basic/src/main/java/org/opendaylight/aaa/basic/Activator.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2014, 2015 Hewlett-Packard Development Company, L.P. and others. 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.basic; + +import org.apache.felix.dm.DependencyActivatorBase; +import org.apache.felix.dm.DependencyManager; +import org.opendaylight.aaa.api.CredentialAuth; +import org.opendaylight.aaa.api.TokenAuth; +import org.osgi.framework.BundleContext; + +public class Activator extends DependencyActivatorBase { + + @Override + public void init(BundleContext context, DependencyManager manager) throws Exception { + manager.add(createComponent() + .setInterface(new String[] { TokenAuth.class.getName() }, null) + .setImplementation(HttpBasicAuth.class) + .add(createServiceDependency().setService(CredentialAuth.class).setRequired(true))); + } + + @Override + public void destroy(BundleContext context, DependencyManager manager) throws Exception { + } + +} diff --git a/odl-aaa-moon/aaa/aaa-authn-basic/src/main/java/org/opendaylight/aaa/basic/HttpBasicAuth.java b/odl-aaa-moon/aaa/aaa-authn-basic/src/main/java/org/opendaylight/aaa/basic/HttpBasicAuth.java new file mode 100644 index 00000000..eff47e63 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn-basic/src/main/java/org/opendaylight/aaa/basic/HttpBasicAuth.java @@ -0,0 +1,129 @@ +/* + * Copyright (c) 2014, 2015 Hewlett-Packard Development Company, L.P. and others. 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.basic; + +import com.sun.jersey.core.util.Base64; +import java.util.List; +import java.util.Map; +import org.opendaylight.aaa.AuthenticationBuilder; +import org.opendaylight.aaa.PasswordCredentialBuilder; +import org.opendaylight.aaa.api.Authentication; +import org.opendaylight.aaa.api.AuthenticationException; +import org.opendaylight.aaa.api.Claim; +import org.opendaylight.aaa.api.CredentialAuth; +import org.opendaylight.aaa.api.PasswordCredentials; +import org.opendaylight.aaa.api.TokenAuth; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * An HTTP Basic authenticator. Note that this is provided as a Hydrogen + * backward compatible authenticator, but usage of this authenticator or HTTP + * Basic Authentication is highly discouraged due to its vulnerability. + * + * To obtain a token using the HttpBasicAuth Strategy, add a header to your HTTP + * request in the form: + * <code>Authorization: Basic BASE_64_ENCODED_CREDENTIALS</code> + * + * Where <code>BASE_64_ENCODED_CREDENTIALS</code> is the base 64 encoded value + * of the user's credentials in the following form: <code>user:password</code> + * + * For example, assuming the user is "admin" and the password is "admin": + * <code>Authorization: Basic YWRtaW46YWRtaW4=</code> + * + * @author liemmn + * + */ +public class HttpBasicAuth implements TokenAuth { + + public static final String AUTH_HEADER = "Authorization"; + + public static final String AUTH_SEP = ":"; + + public static final String BASIC_PREFIX = "Basic "; + + // TODO relocate this constant + public static final String DEFAULT_DOMAIN = "sdn"; + + /** + * username and password + */ + private static final int NUM_HEADER_CREDS = 2; + + /** + * username, password and domain + */ + private static final int NUM_TOKEN_CREDS = 3; + + private static final Logger LOG = LoggerFactory.getLogger(HttpBasicAuth.class); + + volatile CredentialAuth<PasswordCredentials> credentialAuth; + + private static boolean checkAuthHeaderFormat(final String authHeader) { + return (authHeader != null && authHeader.startsWith(BASIC_PREFIX)); + } + + private static String extractAuthHeader(final Map<String, List<String>> headers) { + return headers.get(AUTH_HEADER).get(0); + } + + private static String[] extractCredentialArray(final String authHeader) { + return new String(Base64.base64Decode(authHeader.substring(BASIC_PREFIX.length()))) + .split(AUTH_SEP); + } + + private static boolean verifyCredentialArray(final String[] creds) { + return (creds != null && creds.length == NUM_HEADER_CREDS); + } + + private static String[] addDomainToCredentialArray(final String[] creds) { + String newCredentialArray[] = new String[NUM_TOKEN_CREDS]; + System.arraycopy(creds, 0, newCredentialArray, 0, creds.length); + newCredentialArray[2] = DEFAULT_DOMAIN; + return newCredentialArray; + } + + private static Authentication generateAuthentication( + CredentialAuth<PasswordCredentials> credentialAuth, final String[] creds) + throws ArrayIndexOutOfBoundsException { + final PasswordCredentials pc = new PasswordCredentialBuilder().setUserName(creds[0]) + .setPassword(creds[1]).setDomain(creds[2]).build(); + final Claim claim = credentialAuth.authenticate(pc); + return new AuthenticationBuilder(claim).build(); + } + + @Override + public Authentication validate(final Map<String, List<String>> headers) + throws AuthenticationException { + if (headers.containsKey(AUTH_HEADER)) { + final String authHeader = extractAuthHeader(headers); + if (checkAuthHeaderFormat(authHeader)) { + // HTTP Basic Auth + String[] creds = extractCredentialArray(authHeader); + // If no domain was supplied then use the default one, which is + // "sdn". + if (verifyCredentialArray(creds)) { + creds = addDomainToCredentialArray(creds); + } + // Assumes correct formatting in form Base64("user:password"). + // Throws an exception if an unknown format is used. + try { + return generateAuthentication(this.credentialAuth, creds); + } catch (ArrayIndexOutOfBoundsException e) { + final String message = "Login Attempt in Bad Format." + + " Please provide user:password in Base64 format."; + LOG.info(message); + throw new AuthenticationException(message); + } + } + } + return null; + } + +} diff --git a/odl-aaa-moon/aaa/aaa-authn-basic/src/test/java/org/opendaylight/aaa/basic/HttpBasicAuthTest.java b/odl-aaa-moon/aaa/aaa-authn-basic/src/test/java/org/opendaylight/aaa/basic/HttpBasicAuthTest.java new file mode 100644 index 00000000..4ee439df --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn-basic/src/test/java/org/opendaylight/aaa/basic/HttpBasicAuthTest.java @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2014, 2015 Hewlett-Packard Development Company, L.P. and others. 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.basic; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.sun.jersey.core.util.Base64; +import java.io.UnsupportedEncodingException; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.junit.Before; +import org.junit.Test; +import org.opendaylight.aaa.ClaimBuilder; +import org.opendaylight.aaa.PasswordCredentialBuilder; +import org.opendaylight.aaa.api.AuthenticationException; +import org.opendaylight.aaa.api.Claim; +import org.opendaylight.aaa.api.CredentialAuth; + +public class HttpBasicAuthTest { + private static final String USERNAME = "admin"; + private static final String PASSWORD = "admin"; + private static final String DOMAIN = "sdn"; + private HttpBasicAuth auth; + + @SuppressWarnings("unchecked") + @Before + public void setup() { + auth = new HttpBasicAuth(); + auth.credentialAuth = mock(CredentialAuth.class); + when( + auth.credentialAuth.authenticate(new PasswordCredentialBuilder() + .setUserName(USERNAME).setPassword(PASSWORD).setDomain(DOMAIN).build())) + .thenReturn( + new ClaimBuilder().setUser("admin").addRole("admin").setUserId("123") + .build()); + when( + auth.credentialAuth.authenticate(new PasswordCredentialBuilder() + .setUserName(USERNAME).setPassword("bozo").setDomain(DOMAIN).build())) + .thenThrow(new AuthenticationException("barf")); + } + + @Test + public void testValidateOk() throws UnsupportedEncodingException { + String data = USERNAME + ":" + PASSWORD + ":" + DOMAIN; + Map<String, List<String>> headers = new HashMap<>(); + headers.put("Authorization", + Arrays.asList("Basic " + new String(Base64.encode(data.getBytes("utf-8"))))); + Claim claim = auth.validate(headers); + assertNotNull(claim); + assertEquals(USERNAME, claim.user()); + assertEquals("admin", claim.roles().iterator().next()); + } + + @Test(expected = AuthenticationException.class) + public void testValidateBadPassword() throws UnsupportedEncodingException { + String data = USERNAME + ":bozo:" + DOMAIN; + Map<String, List<String>> headers = new HashMap<>(); + headers.put("Authorization", + Arrays.asList("Basic " + new String(Base64.encode(data.getBytes("utf-8"))))); + auth.validate(headers); + } + + @Test(expected = AuthenticationException.class) + public void testValidateBadPasswordNoDOMAIN() throws UnsupportedEncodingException { + String data = USERNAME + ":bozo"; + Map<String, List<String>> headers = new HashMap<>(); + headers.put("Authorization", + Arrays.asList("Basic " + new String(Base64.encode(data.getBytes("utf-8"))))); + auth.validate(headers); + } + + @Test(expected = AuthenticationException.class) + public void testBadHeaderFormatNoPassword() throws UnsupportedEncodingException { + // just provide the username + String data = USERNAME; + Map<String, List<String>> headers = new HashMap<>(); + headers.put("Authorization", + Arrays.asList("Basic " + new String(Base64.encode(data.getBytes("utf-8"))))); + auth.validate(headers); + } + + @Test(expected = AuthenticationException.class) + public void testBadHeaderFormat() throws UnsupportedEncodingException { + // provide username: + String data = USERNAME + "$" + PASSWORD; + Map<String, List<String>> headers = new HashMap<>(); + headers.put("Authorization", + Arrays.asList("Basic " + new String(Base64.encode(data.getBytes("utf-8"))))); + auth.validate(headers); + } +} diff --git a/odl-aaa-moon/aaa/aaa-authn-federation/pom.xml b/odl-aaa-moon/aaa/aaa-authn-federation/pom.xml new file mode 100644 index 00000000..e217f48c --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn-federation/pom.xml @@ -0,0 +1,132 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + <parent> + <groupId>org.opendaylight.aaa</groupId> + <artifactId>aaa-parent</artifactId> + <version>0.3.2-Beryllium-SR2</version> + <relativePath>../parent</relativePath> + </parent> + + <artifactId>aaa-authn-federation</artifactId> + <packaging>bundle</packaging> + + <dependencies> + <dependency> + <groupId>org.opendaylight.aaa</groupId> + <artifactId>aaa-authn-api</artifactId> + </dependency> + <dependency> + <groupId>org.opendaylight.aaa</groupId> + <artifactId>aaa-authn</artifactId> + </dependency> + <dependency> + <groupId>org.slf4j</groupId> + <artifactId>slf4j-api</artifactId> + </dependency> + <dependency> + <groupId>com.sun.jersey</groupId> + <artifactId>jersey-server</artifactId> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>javax.servlet</groupId> + <artifactId>javax.servlet-api</artifactId> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>org.apache.oltu.oauth2</groupId> + <artifactId>org.apache.oltu.oauth2.authzserver</artifactId> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>org.apache.oltu.oauth2</groupId> + <artifactId>org.apache.oltu.oauth2.common</artifactId> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>org.apache.oltu.oauth2</groupId> + <artifactId>org.apache.oltu.oauth2.resourceserver</artifactId> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>org.osgi</groupId> + <artifactId>org.osgi.core</artifactId> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>org.apache.felix</groupId> + <artifactId>org.apache.felix.dependencymanager</artifactId> + <scope>provided</scope> + </dependency> + <!-- Testing Dependencies --> + <dependency> + <groupId>com.sun.jersey.jersey-test-framework</groupId> + <artifactId>jersey-test-framework-grizzly2</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.eclipse.jetty</groupId> + <artifactId>jetty-servlet-tester</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>junit</groupId> + <artifactId>junit</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.mockito</groupId> + <artifactId>mockito-all</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.slf4j</groupId> + <artifactId>slf4j-simple</artifactId> + <scope>test</scope> + </dependency> + </dependencies> + + <build> + <plugins> + <plugin> + <groupId>org.apache.felix</groupId> + <artifactId>maven-bundle-plugin</artifactId> + <extensions>true</extensions> + <configuration> + <instructions> + <Import-Package>*,com.sun.jersey.spi.container.servlet</Import-Package> + <Web-ContextPath>/oauth2/federation</Web-ContextPath> + <Web-Connectors>federationConn</Web-Connectors> + <Bundle-Activator>org.opendaylight.aaa.federation.Activator</Bundle-Activator> + <manifestLocation>${project.basedir}/META-INF</manifestLocation> + </instructions> + </configuration> + </plugin> + <plugin> + <groupId>org.codehaus.mojo</groupId> + <artifactId>build-helper-maven-plugin</artifactId> + <executions> + <execution> + <id>attach-artifacts</id> + <phase>package</phase> + <goals> + <goal>attach-artifact</goal> + </goals> + <configuration> + <artifacts> + <artifact> + <file>${project.build.directory}/classes/federation.cfg</file> + <type>cfg</type> + <classifier>config</classifier> + </artifact> + </artifacts> + </configuration> + </execution> + </executions> + </plugin> + </plugins> + </build> + +</project> diff --git a/odl-aaa-moon/aaa/aaa-authn-federation/src/main/java/org/opendaylight/aaa/federation/Activator.java b/odl-aaa-moon/aaa/aaa-authn-federation/src/main/java/org/opendaylight/aaa/federation/Activator.java new file mode 100644 index 00000000..4ae027c8 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn-federation/src/main/java/org/opendaylight/aaa/federation/Activator.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2014, 2015 Hewlett-Packard Development Company, L.P. and others. 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.federation; + +import java.util.Dictionary; +import org.apache.felix.dm.DependencyActivatorBase; +import org.apache.felix.dm.DependencyManager; +import org.opendaylight.aaa.api.ClaimAuth; +import org.opendaylight.aaa.api.IdMService; +import org.opendaylight.aaa.api.TokenStore; +import org.osgi.framework.BundleContext; +import org.osgi.framework.Constants; +import org.osgi.service.cm.ManagedService; + +/** + * An activator for the secure token server to inject in a + * <code>CredentialAuth</code> implementation. + * + * @author liemmn + * + */ +public class Activator extends DependencyActivatorBase { + private static final String FEDERATION_PID = "org.opendaylight.aaa.federation"; + + @Override + public void init(BundleContext context, DependencyManager manager) throws Exception { + manager.add(createComponent() + .setImplementation(ServiceLocator.getInstance()) + .add(createServiceDependency().setService(TokenStore.class).setRequired(true)) + .add(createServiceDependency().setService(IdMService.class).setRequired(true)) + .add(createServiceDependency().setService(ClaimAuth.class).setRequired(false) + .setCallbacks("claimAuthAdded", "claimAuthRemoved"))); + context.registerService(ManagedService.class, FederationConfiguration.instance(), + addPid(FederationConfiguration.defaults)); + } + + @Override + public void destroy(BundleContext context, DependencyManager manager) throws Exception { + } + + private Dictionary<String, ?> addPid(Dictionary<String, String> dict) { + dict.put(Constants.SERVICE_PID, FEDERATION_PID); + return dict; + } +} diff --git a/odl-aaa-moon/aaa/aaa-authn-federation/src/main/java/org/opendaylight/aaa/federation/ClaimAuthFilter.java b/odl-aaa-moon/aaa/aaa-authn-federation/src/main/java/org/opendaylight/aaa/federation/ClaimAuthFilter.java new file mode 100644 index 00000000..10a1277d --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn-federation/src/main/java/org/opendaylight/aaa/federation/ClaimAuthFilter.java @@ -0,0 +1,249 @@ +/* + * Copyright (c) 2014, 2015 Hewlett-Packard Development Company, L.P. and others. 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.federation; + +import static javax.servlet.http.HttpServletResponse.SC_UNAUTHORIZED; +import static org.opendaylight.aaa.federation.FederationEndpoint.AUTH_CLAIM; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.opendaylight.aaa.api.Claim; +import org.opendaylight.aaa.api.ClaimAuth; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A generic {@link Filter} for {@link ClaimAuth} implementations. + * <p> + * This filter trusts any authentication metadata bound to a request. A request + * with fake authentication claims could be forged by an attacker and submitted + * to one of the Connector ports the engine is listening on and we would blindly + * accept the forged information in this filter. Therefore it is vital we only + * accept authentication claims from a trusted proxy. It is incumbent upon the + * site administrator to dedicate specific connector ports on which previously + * authenticated requests from a trusted proxy will be sent to and to assure + * only a trusted proxy can connect to that port. The site administrator must + * enumerate those ports in the configuration. We reject any request which did + * not originate on one of the configured secure proxy ports. + * + * @author liemmn + * + */ +public class ClaimAuthFilter implements Filter { + private static final Logger LOG = LoggerFactory.getLogger(ClaimAuthFilter.class); + + private static final String CGI_AUTH_TYPE = "AUTH_TYPE"; + private static final String CGI_PATH_INFO = "PATH_INFO"; + private static final String CGI_PATH_TRANSLATED = "PATH_TRANSLATED"; + private static final String CGI_QUERY_STRING = "QUERY_STRING"; + private static final String CGI_REMOTE_ADDR = "REMOTE_ADDR"; + private static final String CGI_REMOTE_HOST = "REMOTE_HOST"; + private static final String CGI_REMOTE_PORT = "REMOTE_PORT"; + private static final String CGI_REMOTE_USER = "REMOTE_USER"; + private static final String CGI_REMOTE_USER_GROUPS = "REMOTE_USER_GROUPS"; + private static final String CGI_REQUEST_METHOD = "REQUEST_METHOD"; + private static final String CGI_SCRIPT_NAME = "SCRIPT_NAME"; + private static final String CGI_SERVER_PROTOCOL = "SERVER_PROTOCOL"; + + static final String UNAUTHORIZED_PORT_ERR = "Unauthorized proxy port"; + + @Override + public void init(FilterConfig fc) throws ServletException { + } + + @Override + public void destroy() { + } + + @Override + public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) + throws IOException, ServletException { + Set<Integer> secureProxyPorts; + int localPort; + + // Check to see if we are communicated over an authorized port or not + secureProxyPorts = FederationConfiguration.instance().secureProxyPorts(); + localPort = req.getLocalPort(); + if (!secureProxyPorts.contains(localPort)) { + ((HttpServletResponse) resp).sendError(SC_UNAUTHORIZED, UNAUTHORIZED_PORT_ERR); + return; + } + + // Let's do some transformation! + List<ClaimAuth> claimAuthCollection = ServiceLocator.getInstance().getClaimAuthCollection(); + for (ClaimAuth ca : claimAuthCollection) { + Claim claim = ca.transform(claims((HttpServletRequest) req)); + if (claim != null) { + req.setAttribute(AUTH_CLAIM, claim); + // No need to do further transformation since it has been done + break; + } + } + chain.doFilter(req, resp); + } + + // Extract attributes and headers out of the request + private Map<String, Object> claims(HttpServletRequest req) { + String name; + Object objectValue; + String stringValue; + Map<String, Object> claims = new HashMap<>(); + + /* + * Tomcat has a bug/feature, not all attributes are enumerated by + * getAttributeNames() therefore getAttributeNames() cannot be used to + * obtain the full set of attributes. However if you know the name of + * the attribute a priori you can call getAttribute() and obtain the + * value. Therefore we maintain a list of attribute names + * (httpAttributes) which will be used to call getAttribute() with so we + * don't miss essential attributes. + * + * This is the Tomcat bug, note it is marked WONTFIX. Bug 25363 - + * request.getAttributeNames() not working properly Status: RESOLVED + * WONTFIX https://issues.apache.org/bugzilla/show_bug.cgi?id=25363 + * + * The solution adopted by Tomcat is to document the behavior in the + * "The Apache Tomcat Connector - Reference Guide" under the JkEnvVar + * property where is says: + * + * You can retrieve the variables on Tomcat as request attributes via + * request.getAttribute(attributeName). Note that the variables send via + * JkEnvVar will not be listed in request.getAttributeNames(). + */ + + // Capture attributes which can be enumerated ... + @SuppressWarnings("unchecked") + Enumeration<String> attrs = req.getAttributeNames(); + while (attrs.hasMoreElements()) { + name = attrs.nextElement(); + objectValue = req.getAttribute(name); + if (objectValue instanceof String) { + // metadata might be i18n, assume UTF8 and decode + stringValue = decodeUTF8((String) objectValue); + objectValue = stringValue; + } + claims.put(name, objectValue); + } + + // Capture specific attributes which cannot be enumerated ... + for (String attr : FederationConfiguration.instance().httpAttributes()) { + name = attr; + objectValue = req.getAttribute(name); + if (objectValue instanceof String) { + // metadata might be i18n, assume UTF8 and decode + stringValue = decodeUTF8((String) objectValue); + objectValue = stringValue; + } + claims.put(name, objectValue); + } + + /* + * In general we should not utilize HTTP headers as validated security + * assertions because they are too easy to forge. Therefore in general + * we don't include HTTP headers, however in certain circumstances + * specific headers may be acceptable, thus we permit an admin to + * configure the capture of specific headers. + */ + for (String header : FederationConfiguration.instance().httpHeaders()) { + claims.put(header, req.getHeader(header)); + } + + // Capture standard CGI variables... + claims.put(CGI_AUTH_TYPE, req.getAuthType()); + claims.put(CGI_PATH_INFO, req.getPathInfo()); + claims.put(CGI_PATH_TRANSLATED, req.getPathTranslated()); + claims.put(CGI_QUERY_STRING, req.getQueryString()); + claims.put(CGI_REMOTE_ADDR, req.getRemoteAddr()); + claims.put(CGI_REMOTE_HOST, req.getRemoteHost()); + claims.put(CGI_REMOTE_PORT, req.getRemotePort()); + // remote user might be i18n, assume UTF8 and decode + claims.put(CGI_REMOTE_USER, decodeUTF8(req.getRemoteUser())); + claims.put(CGI_REMOTE_USER_GROUPS, req.getAttribute(CGI_REMOTE_USER_GROUPS)); + claims.put(CGI_REQUEST_METHOD, req.getMethod()); + claims.put(CGI_SCRIPT_NAME, req.getServletPath()); + claims.put(CGI_SERVER_PROTOCOL, req.getProtocol()); + + if (LOG.isDebugEnabled()) { + LOG.debug("ClaimAuthFilter claims = {}", claims.toString()); + } + + return claims; + } + + /** + * Decode from UTF-8, return Unicode. + * + * If we're unable to UTF-8 decode the string the fallback is to return the + * string unmodified and log a warning. + * + * Some data, especially metadata attached to a user principal may be + * internationalized (i18n). The classic examples are the user's name, + * location, organization, etc. We need to be able to read this metadata and + * decode it into unicode characters so that we properly handle i18n string + * values. + * + * One of the the prolems is we often don't know the encoding (i.e. charset) + * of the string. RFC-5987 is supposed to define how non-ASCII values are + * transmitted in HTTP headers, this is a follow on from the work in + * RFC-2231. However at the time of this writing these RFC's are not + * implemented in the Servlet Request classes. Not only are these RFC's + * unimplemented but they are specific to HTTP headers, much of our metadata + * arrives via attributes as opposed to being in a header. + * + * Note: ASCII encoding is a subset of UTF-8 encoding therefore any strings + * which are pure ASCII will decode from UTF-8 just fine. However on the + * other hand Latin-1 (ISO-8859-1) encoding is not compatible with UTF-8 for + * code points in the range 128-255 (i.e. beyond 7-bit ascii). ISO-8859-1 is + * the default encoding for HTTP and HTML 4, however the consensus is the + * use of ISO-8859-1 was a mistake and Unicode with UTF-8 encoding is now + * the norm. If a string value is transmitted encoded in ISO-8859-1 + * contaiing code points in the range 128-255 and we try to UTF-8 decode it + * it will either not be the correct decoded string or it will throw a + * decoding exception. + * + * Conventional practice at the moment is for the sending side to encode + * internationalized values in UTF-8 with the receving end decoding the + * value back from UTF-8. We do not expect the use of ISO-8859-1 on these + * attributes. However due to peculiarities of the Java String + * implementation we have to specify the raw bytes are encoded in ISO-8859-1 + * just to get back the raw bytes to be able to feed into the UTF-8 decoder. + * This doesn't seem right but it is because we need the full 8-bit byte and + * the only way to say "unmodified 8-bit bytes" in Java is to call it + * ISO-8859-1. Ugh! + * + * @param string + * The input string in UTF-8 to be decoded. + * @return Unicode string + */ + private String decodeUTF8(String string) { + if (string == null) { + return null; + } + try { + return new String(string.getBytes("ISO8859-1"), "UTF-8"); + } catch (UnsupportedEncodingException e) { + LOG.warn("Unable to UTF-8 decode: ", string, e); + return string; + } + } + +} diff --git a/odl-aaa-moon/aaa/aaa-authn-federation/src/main/java/org/opendaylight/aaa/federation/FederationConfiguration.java b/odl-aaa-moon/aaa/aaa-authn-federation/src/main/java/org/opendaylight/aaa/federation/FederationConfiguration.java new file mode 100644 index 00000000..a68dc15c --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn-federation/src/main/java/org/opendaylight/aaa/federation/FederationConfiguration.java @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2014, 2015 Hewlett-Packard Development Company, L.P. and others. 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.federation; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Dictionary; +import java.util.Enumeration; +import java.util.Hashtable; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; +import java.util.concurrent.ConcurrentHashMap; +import org.osgi.service.cm.ConfigurationException; +import org.osgi.service.cm.ManagedService; + +/** + * AAA federation configurations in OSGi. + * + * @author liemmn + * + */ +public class FederationConfiguration implements ManagedService { + private static final String FEDERATION_CONFIG_ERR = "Error saving federation configuration"; + + static final String HTTP_HEADERS = "httpHeaders"; + static final String HTTP_ATTRIBUTES = "httpAttributes"; + static final String SECURE_PROXY_PORTS = "secureProxyPorts"; + + static FederationConfiguration instance = new FederationConfiguration(); + + static final Hashtable<String, String> defaults = new Hashtable<>(); + static { + defaults.put(HTTP_HEADERS, ""); + defaults.put(HTTP_ATTRIBUTES, ""); + } + private static Map<String, String> configs = new ConcurrentHashMap<>(); + + // singleton + private FederationConfiguration() { + } + + public static FederationConfiguration instance() { + return instance; + } + + @Override + public void updated(Dictionary<String, ?> props) throws ConfigurationException { + if (props == null) { + configs.clear(); + configs.putAll(defaults); + } else { + try { + Enumeration<String> keys = props.keys(); + while (keys.hasMoreElements()) { + String key = keys.nextElement(); + configs.put(key, (String) props.get(key)); + } + } catch (Throwable t) { + throw new ConfigurationException(null, FEDERATION_CONFIG_ERR, t); + } + } + } + + public List<String> httpHeaders() { + String headers = configs.get(HTTP_HEADERS); + return (headers == null) ? new ArrayList<String>() : Arrays.asList(headers.split(" ")); + } + + public List<String> httpAttributes() { + String attributes = configs.get(HTTP_ATTRIBUTES); + return (attributes == null) ? new ArrayList<String>() : Arrays + .asList(attributes.split(" ")); + } + + public Set<Integer> secureProxyPorts() { + String ports = configs.get(SECURE_PROXY_PORTS); + Set<Integer> secureProxyPorts = new TreeSet<Integer>(); + + if (ports != null && !ports.isEmpty()) { + for (String port : ports.split(" ")) { + secureProxyPorts.add(Integer.parseInt(port)); + } + } + return secureProxyPorts; + } + +} diff --git a/odl-aaa-moon/aaa/aaa-authn-federation/src/main/java/org/opendaylight/aaa/federation/FederationEndpoint.java b/odl-aaa-moon/aaa/aaa-authn-federation/src/main/java/org/opendaylight/aaa/federation/FederationEndpoint.java new file mode 100644 index 00000000..6ac76c0a --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn-federation/src/main/java/org/opendaylight/aaa/federation/FederationEndpoint.java @@ -0,0 +1,149 @@ +/* + * Copyright (c) 2014, 2015 Hewlett-Packard Development Company, L.P. and others. 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.federation; + +import static javax.servlet.http.HttpServletResponse.SC_CREATED; +import static javax.servlet.http.HttpServletResponse.SC_UNAUTHORIZED; + +import java.io.IOException; +import java.io.PrintWriter; +import java.util.List; +import javax.servlet.ServletConfig; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.apache.oltu.oauth2.as.issuer.OAuthIssuer; +import org.apache.oltu.oauth2.as.issuer.OAuthIssuerImpl; +import org.apache.oltu.oauth2.as.issuer.UUIDValueGenerator; +import org.apache.oltu.oauth2.as.response.OAuthASResponse; +import org.apache.oltu.oauth2.common.exception.OAuthSystemException; +import org.apache.oltu.oauth2.common.message.OAuthResponse; +import org.opendaylight.aaa.AuthenticationBuilder; +import org.opendaylight.aaa.ClaimBuilder; +import org.opendaylight.aaa.api.Authentication; +import org.opendaylight.aaa.api.AuthenticationException; +import org.opendaylight.aaa.api.Claim; + +/** + * An endpoint for claim-based authentication federation (in-bound). + * + * @author liemmn + * + */ +public class FederationEndpoint extends HttpServlet { + + private static final long serialVersionUID = -5553885846238987245L; + + /** An in-bound authentication claim */ + static final String AUTH_CLAIM = "AAA-CLAIM"; + + private static final String UNAUTHORIZED = "unauthorized"; + + private transient OAuthIssuer oi; + + @Override + public void init(ServletConfig config) throws ServletException { + oi = new OAuthIssuerImpl(new UUIDValueGenerator()); + } + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException, + ServletException { + try { + createRefreshToken(req, resp); + } catch (Exception e) { + error(resp, SC_UNAUTHORIZED, e.getMessage()); + } + } + + // Create a refresh token + private void createRefreshToken(HttpServletRequest req, HttpServletResponse resp) + throws OAuthSystemException, IOException { + Claim claim = (Claim) req.getAttribute(AUTH_CLAIM); + oauthRefreshTokenResponse(resp, claim); + } + + // Build OAuth refresh token response from the given claim mapped and + // injected by the external IdP + private void oauthRefreshTokenResponse(HttpServletResponse resp, Claim claim) + throws OAuthSystemException, IOException { + if (claim == null) { + throw new AuthenticationException(UNAUTHORIZED); + } + + String userName = claim.user(); + // Need to have at least a mapped username! + if (userName == null) { + throw new AuthenticationException(UNAUTHORIZED); + } + + String domain = claim.domain(); + // Need to have at least a domain! + if (domain == null) { + throw new AuthenticationException(UNAUTHORIZED); + } + + String userId = userName + "@" + domain; + + // Create an unscoped ODL context from the external claim + Authentication auth = new AuthenticationBuilder(new ClaimBuilder(claim).setUserId(userId) + .build()).setExpiration(tokenExpiration()).build(); + + // Create OAuth response + String token = oi.refreshToken(); + OAuthResponse r = OAuthASResponse + .tokenResponse(SC_CREATED) + .setRefreshToken(token) + .setExpiresIn(Long.toString(auth.expiration())) + .setScope( + // Use mapped domain if there is one, else list + // all the ones that this user has access to + (claim.domain().isEmpty()) ? listToString(ServiceLocator.getInstance() + .getIdmService().listDomains(userId)) : claim.domain()) + .buildJSONMessage(); + // Cache this token... + ServiceLocator.getInstance().getTokenStore().put(token, auth); + write(resp, r); + } + + // Token expiration + private long tokenExpiration() { + return ServiceLocator.getInstance().getTokenStore().tokenExpiration(); + } + + // Space-delimited string from a list of strings + private String listToString(List<String> list) { + StringBuffer sb = new StringBuffer(); + for (String s : list) { + sb.append(s).append(" "); + } + return sb.toString().trim(); + } + + // Emit an error OAuthResponse with the given HTTP code + private void error(HttpServletResponse resp, int httpCode, String error) { + try { + OAuthResponse r = OAuthResponse.errorResponse(httpCode).setError(error) + .buildJSONMessage(); + write(resp, r); + } catch (Exception e1) { + // Nothing to do here + } + } + + // Write out an OAuthResponse + private void write(HttpServletResponse resp, OAuthResponse r) throws IOException { + resp.setStatus(r.getResponseStatus()); + PrintWriter pw = resp.getWriter(); + pw.print(r.getBody()); + pw.flush(); + pw.close(); + } +} diff --git a/odl-aaa-moon/aaa/aaa-authn-federation/src/main/java/org/opendaylight/aaa/federation/ServiceLocator.java b/odl-aaa-moon/aaa/aaa-authn-federation/src/main/java/org/opendaylight/aaa/federation/ServiceLocator.java new file mode 100644 index 00000000..dd861514 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn-federation/src/main/java/org/opendaylight/aaa/federation/ServiceLocator.java @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2014, 2015 Hewlett-Packard Development Company, L.P. and others. 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.federation; + +import java.util.List; +import java.util.Vector; +import org.opendaylight.aaa.api.ClaimAuth; +import org.opendaylight.aaa.api.IdMService; +import org.opendaylight.aaa.api.TokenStore; + +/** + * A service locator to bridge between the web world and OSGi world. + * + * @author liemmn + * + */ +public class ServiceLocator { + + private static final ServiceLocator instance = new ServiceLocator(); + + protected volatile List<ClaimAuth> claimAuthCollection = new Vector<>(); + + protected volatile TokenStore tokenStore; + + protected volatile IdMService idmService; + + private ServiceLocator() { + } + + public static ServiceLocator getInstance() { + return instance; + } + + /** + * Called through reflection from the federation Activator + * + * @see org.opendaylight.aaa.federation.ServiceLocator + * @param ca the injected claims implementation + */ + protected void claimAuthAdded(ClaimAuth ca) { + this.claimAuthCollection.add(ca); + } + + /** + * Called through reflection from the federation Activator + * + * @see org.opendaylight.aaa.federation.Activator + * @param ca the claims implementation to remove + */ + protected void claimAuthRemoved(ClaimAuth ca) { + this.claimAuthCollection.remove(ca); + } + + public List<ClaimAuth> getClaimAuthCollection() { + return claimAuthCollection; + } + + public void setClaimAuthCollection(List<ClaimAuth> claimAuthCollection) { + this.claimAuthCollection = claimAuthCollection; + } + + public TokenStore getTokenStore() { + return tokenStore; + } + + public void setTokenStore(TokenStore tokenStore) { + this.tokenStore = tokenStore; + } + + public IdMService getIdmService() { + return idmService; + } + + public void setIdmService(IdMService idmService) { + this.idmService = idmService; + } +} diff --git a/odl-aaa-moon/aaa/aaa-authn-federation/src/main/java/org/opendaylight/aaa/federation/SssdFilter.java b/odl-aaa-moon/aaa/aaa-authn-federation/src/main/java/org/opendaylight/aaa/federation/SssdFilter.java new file mode 100644 index 00000000..9223c6dd --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn-federation/src/main/java/org/opendaylight/aaa/federation/SssdFilter.java @@ -0,0 +1,151 @@ +/* + * Copyright (c) 2014, 2015 Red Hat, Inc. and others. 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.federation; + +import java.io.IOException; +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletRequestWrapper; + +class SssdHeadersRequest extends HttpServletRequestWrapper { + private static final String headerPrefix = "X-SSSD-"; + + public SssdHeadersRequest(HttpServletRequest request) { + super(request); + } + + public Object getAttribute(String name) { + HttpServletRequest request = (HttpServletRequest) getRequest(); + String headerValue; + + headerValue = request.getHeader(headerPrefix + name); + if (headerValue != null) { + return headerValue; + } else { + return request.getAttribute(name); + } + } + + @Override + public String getRemoteUser() { + HttpServletRequest request = (HttpServletRequest) getRequest(); + String headerValue; + + headerValue = request.getHeader(headerPrefix + "REMOTE_USER"); + if (headerValue != null) { + return headerValue; + } else { + return request.getRemoteUser(); + } + } + + @Override + public String getAuthType() { + HttpServletRequest request = (HttpServletRequest) getRequest(); + String headerValue; + + headerValue = request.getHeader(headerPrefix + "AUTH_TYPE"); + if (headerValue != null) { + return headerValue; + } else { + return request.getAuthType(); + } + } + + @Override + public String getRemoteAddr() { + HttpServletRequest request = (HttpServletRequest) getRequest(); + String headerValue; + + headerValue = request.getHeader(headerPrefix + "REMOTE_ADDR"); + if (headerValue != null) { + return headerValue; + } else { + return request.getRemoteAddr(); + } + } + + @Override + public String getRemoteHost() { + HttpServletRequest request = (HttpServletRequest) getRequest(); + String headerValue; + + headerValue = request.getHeader(headerPrefix + "REMOTE_HOST"); + if (headerValue != null) { + return headerValue; + } else { + return request.getRemoteHost(); + } + } + + @Override + public int getRemotePort() { + HttpServletRequest request = (HttpServletRequest) getRequest(); + String headerValue; + + headerValue = request.getHeader(headerPrefix + "REMOTE_PORT"); + if (headerValue != null) { + return Integer.parseInt(headerValue); + } else { + return request.getRemotePort(); + } + } + +} + +/** + * Populate HttpRequestServlet API data from HTTP extension headers. + * + * When SSSD is used for authentication and identity lookup those actions occur + * in an Apache HTTP server which is fronting the servlet container. After + * successful authentication Apache will proxy the request to the container + * along with additional authentication and identity metadata. + * + * The preferred way to transport the metadata and have it appear seamlessly in + * the servlet API is via the AJP protocol. However AJP may not be available or + * desirable. An alternative method is to transport the metadata in extension + * HTTP headers. However we still want the standard servlet request API methods + * to work. Another way to say this is we do not want upper layers to be aware + * of the transport mechanism. To achieve this we wrap the HttpServletRequest + * class and override specific methods which need to extract the data from the + * extension HTTP headers. (This is roughly equivalent to what happens when AJP + * is implemented natively in the container). + * + * The extension HTTP headers are identified by the prefix "X-SSSD-". The + * overridden methods check for the existence of the appropriate extension + * header and if present returns the value found in the extension header, + * otherwise it returns the value from the method it's wrapping. + * + */ +public class SssdFilter implements Filter { + @Override + public void init(FilterConfig fc) throws ServletException { + } + + @Override + public void destroy() { + } + + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, + FilterChain filterChain) throws IOException, ServletException { + if (servletRequest instanceof HttpServletRequest) { + HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest; + SssdHeadersRequest request = new SssdHeadersRequest(httpServletRequest); + filterChain.doFilter(request, servletResponse); + } else { + filterChain.doFilter(servletRequest, servletResponse); + } + } +} diff --git a/odl-aaa-moon/aaa/aaa-authn-federation/src/main/resources/OSGI-INF/metatype/metatype.properties b/odl-aaa-moon/aaa/aaa-authn-federation/src/main/resources/OSGI-INF/metatype/metatype.properties new file mode 100644 index 00000000..4323c04d --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn-federation/src/main/resources/OSGI-INF/metatype/metatype.properties @@ -0,0 +1,11 @@ +org.opendaylight.aaa.federation.name = Opendaylight AAA Federation Configuration +org.opendaylight.aaa.federation.description = Configuration for AAA federation +org.opendaylight.aaa.federation.httpHeaders.name = Custom HTTP Headers +org.opendaylight.aaa.federation.httpHeaders.description = Space-delimited list of \ +specific HTTP headers to capture for authentication federation. +org.opendaylight.aaa.federation.httpAttributes.name = Custom HTTP Attributes +org.opendaylight.aaa.federation.httpAttributes.description = Space-delimited list of \ +specific HTTP attributes to capture for authentication federation. +org.opendaylight.aaa.federation.secureProxyPorts.name = Secure Proxy Ports +org.opendaylight.aaa.federation.secureProxyPorts.description = Space-delimited list of \ +port numbers on which a trusted HTTP proxy performing authentication forwards pre-authenticated requests. diff --git a/odl-aaa-moon/aaa/aaa-authn-federation/src/main/resources/OSGI-INF/metatype/metatype.xml b/odl-aaa-moon/aaa/aaa-authn-federation/src/main/resources/OSGI-INF/metatype/metatype.xml new file mode 100644 index 00000000..e2efd3d4 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn-federation/src/main/resources/OSGI-INF/metatype/metatype.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<metatype:MetaData xmlns:metatype="http://www.osgi.org/xmlns/metatype/v1.0.0" + localization="OSGI-INF/metatype/metatype"> + <OCD id="org.opendaylight.aaa.federation" name="%org.opendaylight.aaa.federation.name" + description="%org.opendaylight.aaa.federation.description"> + <AD id="httpHeaders" type="String" default="" + name="%org.opendaylight.aaa.federation.httpHeaders.name" + description="%org.opendaylight.aaa.federation.httpHeaders.description" /> + <AD id="httpAttributes" type="String" default="" + name="%org.opendaylight.aaa.federation.httpAttributes.name" + description="%org.opendaylight.aaa.federation.httpAttributes.description" /> + <AD id="secureProxyPorts" type="String" default="" + name="%org.opendaylight.aaa.federation.secureProxyPorts.name" + description="%org.opendaylight.aaa.federation.secureProxyPorts.description" /> + </OCD> + <Designate pid="org.opendaylight.aaa.federation"> + <Object ocdref="org.opendaylight.aaa.federation" /> + </Designate> +</metatype:MetaData> diff --git a/odl-aaa-moon/aaa/aaa-authn-federation/src/main/resources/WEB-INF/web.xml b/odl-aaa-moon/aaa/aaa-authn-federation/src/main/resources/WEB-INF/web.xml new file mode 100644 index 00000000..9fd9751f --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn-federation/src/main/resources/WEB-INF/web.xml @@ -0,0 +1,34 @@ +<?xml version="1.0" encoding="ISO-8859-1"?> +<web-app xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" + version="3.0"> + + <servlet> + <servlet-name>federation</servlet-name> + <servlet-class>org.opendaylight.aaa.federation.FederationEndpoint</servlet-class> + <load-on-startup>1</load-on-startup> + </servlet> + <servlet-mapping> + <servlet-name>federation</servlet-name> + <url-pattern>/*</url-pattern> + </servlet-mapping> + + <!-- Federation Auth filter --> + <filter> + <filter-name>SssdFilter</filter-name> + <filter-class>org.opendaylight.aaa.federation.SssdFilter</filter-class> + </filter> + <filter-mapping> + <filter-name>SssdFilter</filter-name> + <url-pattern>/*</url-pattern> + </filter-mapping> + <filter> + <filter-name>ClaimAuthFilter</filter-name> + <filter-class>org.opendaylight.aaa.federation.ClaimAuthFilter</filter-class> + </filter> + <filter-mapping> + <filter-name>ClaimAuthFilter</filter-name> + <url-pattern>/*</url-pattern> + </filter-mapping> + +</web-app> diff --git a/odl-aaa-moon/aaa/aaa-authn-federation/src/main/resources/federation.cfg b/odl-aaa-moon/aaa/aaa-authn-federation/src/main/resources/federation.cfg new file mode 100644 index 00000000..60ef1c46 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn-federation/src/main/resources/federation.cfg @@ -0,0 +1,3 @@ +httpHeaders= +httpAttributes= +secureProxyPorts= diff --git a/odl-aaa-moon/aaa/aaa-authn-federation/src/test/java/org/opendaylight/aaa/federation/FederationEndpointTest.java b/odl-aaa-moon/aaa/aaa-authn-federation/src/test/java/org/opendaylight/aaa/federation/FederationEndpointTest.java new file mode 100644 index 00000000..ae098652 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn-federation/src/test/java/org/opendaylight/aaa/federation/FederationEndpointTest.java @@ -0,0 +1,121 @@ +/* + * Copyright (c) 2014, 2015 Hewlett-Packard Development Company, L.P. and others. 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.federation; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.anyMap; +import static org.mockito.Matchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.Arrays; +import java.util.TreeSet; +import org.eclipse.jetty.testing.HttpTester; +import org.eclipse.jetty.testing.ServletTester; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.opendaylight.aaa.ClaimBuilder; +import org.opendaylight.aaa.api.Claim; +import org.opendaylight.aaa.api.ClaimAuth; +import org.opendaylight.aaa.api.IdMService; +import org.opendaylight.aaa.api.TokenStore; + +/** + * A unit test for federation endpoint. + * + * @author liemmn + * + */ +public class FederationEndpointTest { + private static final long TOKEN_TIMEOUT_SECS = 10; + private static final String CONTEXT = "/oauth2/federation"; + + private final static ServletTester server = new ServletTester(); + private static final Claim claim = new ClaimBuilder().setUser("bob").setUserId("1234") + .addRole("admin").build(); + + @BeforeClass + public static void init() throws Exception { + // Set up server + server.setContextPath(CONTEXT); + + // Add our servlet under test + server.addServlet(FederationEndpoint.class, "/*"); + + // Add ClaimAuth filter + server.addFilter(ClaimAuthFilter.class, "/*", 0); + + // Let's do dis + server.start(); + } + + @AfterClass + public static void shutdown() throws Exception { + server.stop(); + } + + @Before + public void setup() { + mockServiceLocator(); + when(ServiceLocator.getInstance().getTokenStore().tokenExpiration()).thenReturn( + TOKEN_TIMEOUT_SECS); + } + + @After + public void teardown() { + ServiceLocator.getInstance().getClaimAuthCollection().clear(); + } + + @Test + public void testFederationUnconfiguredProxyPort() throws Exception { + HttpTester req = new HttpTester(); + req.setMethod("POST"); + req.setURI(CONTEXT + "/"); + req.setVersion("HTTP/1.0"); + + HttpTester resp = new HttpTester(); + resp.parse(server.getResponses(req.generate())); + assertEquals(401, resp.getStatus()); + } + + @Test + @SuppressWarnings("unchecked") + public void testFederation() throws Exception { + when(ServiceLocator.getInstance().getClaimAuthCollection().get(0).transform(anyMap())) + .thenReturn(claim); + when(ServiceLocator.getInstance().getIdmService().listDomains(anyString())).thenReturn( + Arrays.asList("pepsi", "coke")); + + // Configure secure port (of zero) + FederationConfiguration.instance = mock(FederationConfiguration.class); + when(FederationConfiguration.instance.secureProxyPorts()).thenReturn( + new TreeSet<Integer>(Arrays.asList(0))); + + HttpTester req = new HttpTester(); + req.setMethod("POST"); + req.setURI(CONTEXT + "/"); + req.setVersion("HTTP/1.0"); + + HttpTester resp = new HttpTester(); + resp.parse(server.getResponses(req.generate())); + assertEquals(201, resp.getStatus()); + String content = resp.getContent(); + assertTrue(content.contains("pepsi coke")); + } + + private static void mockServiceLocator() { + ServiceLocator.getInstance().setIdmService(mock(IdMService.class)); + ServiceLocator.getInstance().setTokenStore(mock(TokenStore.class)); + ServiceLocator.getInstance().getClaimAuthCollection().add(mock(ClaimAuth.class)); + } +} diff --git a/odl-aaa-moon/aaa/aaa-authn-keystone/pom.xml b/odl-aaa-moon/aaa/aaa-authn-keystone/pom.xml new file mode 100644 index 00000000..e85d620d --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn-keystone/pom.xml @@ -0,0 +1,106 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + <parent> + <groupId>org.opendaylight.aaa</groupId> + <artifactId>aaa-parent</artifactId> + <version>0.3.2-Beryllium-SR2</version> + <relativePath>../parent</relativePath> + </parent> + + <artifactId>aaa-authn-keystone</artifactId> + <packaging>bundle</packaging> + + <dependencies> + <dependency> + <groupId>org.opendaylight.aaa</groupId> + <artifactId>aaa-authn</artifactId> + </dependency> + <dependency> + <groupId>org.opendaylight.aaa</groupId> + <artifactId>aaa-authn-api</artifactId> + </dependency> + <dependency> + <groupId>org.slf4j</groupId> + <artifactId>slf4j-api</artifactId> + </dependency> + <dependency> + <groupId>com.sun.jersey</groupId> + <artifactId>jersey-server</artifactId> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>javax.servlet</groupId> + <artifactId>javax.servlet-api</artifactId> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>org.osgi</groupId> + <artifactId>org.osgi.core</artifactId> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>org.apache.felix</groupId> + <artifactId>org.apache.felix.dependencymanager</artifactId> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>com.fasterxml.jackson.core</groupId> + <artifactId>jackson-annotations</artifactId> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>com.fasterxml.jackson.core</groupId> + <artifactId>jackson-core</artifactId> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>com.fasterxml.jackson.core</groupId> + <artifactId>jackson-databind</artifactId> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>org.apache.httpcomponents</groupId> + <artifactId>httpcore-osgi</artifactId> + <version>${httpclient.version}</version> + </dependency> + <dependency> + <groupId>org.apache.httpcomponents</groupId> + <artifactId>httpclient-osgi</artifactId> + <version>${httpclient.version}</version> + </dependency> + <!-- Testing Dependencies --> + <dependency> + <groupId>com.sun.jersey.jersey-test-framework</groupId> + <artifactId>jersey-test-framework-grizzly2</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>junit</groupId> + <artifactId>junit</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.slf4j</groupId> + <artifactId>slf4j-simple</artifactId> + <scope>test</scope> + </dependency> + </dependencies> + + <build> + <plugins> + <plugin> + <groupId>org.apache.felix</groupId> + <artifactId>maven-bundle-plugin</artifactId> + <extensions>true</extensions> + <configuration> + <instructions> + <Bundle-Activator>org.opendaylight.aaa.keystone.Activator</Bundle-Activator> + </instructions> + <manifestLocation>${project.basedir}/META-INF</manifestLocation> + </configuration> + </plugin> + </plugins> + </build> +</project> diff --git a/odl-aaa-moon/aaa/aaa-authn-keystone/src/main/java/org/opendaylight/aaa/keystone/Activator.java b/odl-aaa-moon/aaa/aaa-authn-keystone/src/main/java/org/opendaylight/aaa/keystone/Activator.java new file mode 100644 index 00000000..c3c3bfb1 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn-keystone/src/main/java/org/opendaylight/aaa/keystone/Activator.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2014, 2015 Hewlett-Packard Development Company, L.P. and others. 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.keystone; + +import org.apache.felix.dm.DependencyActivatorBase; +import org.apache.felix.dm.DependencyManager; +import org.opendaylight.aaa.api.TokenAuth; +import org.osgi.framework.BundleContext; + +/** + * An activator for {@link KeystoneTokenAuth}. + * + * @author liemmn + * + */ +public class Activator extends DependencyActivatorBase { + + @Override + public void init(BundleContext context, DependencyManager manager) throws Exception { + manager.add(createComponent().setInterface(new String[] { TokenAuth.class.getName() }, null) + .setImplementation(KeystoneTokenAuth.class)); + } + + @Override + public void destroy(BundleContext context, DependencyManager manager) throws Exception { + } + +} diff --git a/odl-aaa-moon/aaa/aaa-authn-keystone/src/main/java/org/opendaylight/aaa/keystone/KeystoneTokenAuth.java b/odl-aaa-moon/aaa/aaa-authn-keystone/src/main/java/org/opendaylight/aaa/keystone/KeystoneTokenAuth.java new file mode 100644 index 00000000..6f4b4bb1 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn-keystone/src/main/java/org/opendaylight/aaa/keystone/KeystoneTokenAuth.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2014, 2015 Hewlett-Packard Development Company, L.P. and others. 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.keystone; + +import java.util.List; +import java.util.Map; +import org.opendaylight.aaa.api.Authentication; +import org.opendaylight.aaa.api.TokenAuth; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A Keystone {@link TokenAuth} filter. + * + * @author liemmn + */ +public class KeystoneTokenAuth implements TokenAuth { + private static final Logger LOG = LoggerFactory.getLogger(KeystoneTokenAuth.class); + + static final String TOKEN = "X-Auth-Token"; + + @Override + public Authentication validate(Map<String, List<String>> headers) { + if (!headers.containsKey(TOKEN)) { + return null; // Not a Keystone token + } + + // TODO: Call into Keystone to get security context... + LOG.info("Not yet validating token {}", headers.get(TOKEN).get(0)); + return null; + } + +} diff --git a/odl-aaa-moon/aaa/aaa-authn-mdsal-store/aaa-authn-mdsal-api/pom.xml b/odl-aaa-moon/aaa/aaa-authn-mdsal-store/aaa-authn-mdsal-api/pom.xml new file mode 100644 index 00000000..da6f27f1 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn-mdsal-store/aaa-authn-mdsal-api/pom.xml @@ -0,0 +1,99 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + + <parent> + <groupId>org.opendaylight.aaa</groupId> + <artifactId>aaa-parent</artifactId> + <version>0.3.2-Beryllium-SR2</version> + <relativePath>../../parent</relativePath> + </parent> + + <artifactId>aaa-authn-mdsal-api</artifactId> + + <dependencies> + <dependency> + <groupId>org.opendaylight.aaa</groupId> + <artifactId>aaa-authn-api</artifactId> + </dependency> + <dependency> + <groupId>org.opendaylight.mdsal</groupId> + <artifactId>yang-binding</artifactId> + </dependency> + <dependency> + <groupId>org.opendaylight.mdsal.model</groupId> + <artifactId>ietf-inet-types</artifactId> + </dependency> + <dependency> + <groupId>org.opendaylight.mdsal.model</groupId> + <artifactId>ietf-yang-types</artifactId> + </dependency> + <dependency> + <groupId>org.opendaylight.mdsal.model</groupId> + <artifactId>yang-ext</artifactId> + </dependency> + </dependencies> + + <build> + <plugins> + <plugin> + <groupId>org.apache.felix</groupId> + <artifactId>maven-bundle-plugin</artifactId> + <version>${bundle.plugin.version}</version> + <extensions>true</extensions> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-javadoc-plugin</artifactId> + <configuration> + <stylesheet>maven</stylesheet> + </configuration> + <executions> + <execution> + <goals> + <goal>aggregate</goal> + </goals> + <phase>site</phase> + </execution> + </executions> + </plugin> + <plugin> + <groupId>org.opendaylight.yangtools</groupId> + <artifactId>yang-maven-plugin</artifactId> + <version>${yangtools.version}</version> + <executions> + <execution> + <goals> + <goal>generate-sources</goal> + </goals> + <configuration> + <yangFilesRootDir>src/main/yang</yangFilesRootDir> + <codeGenerators> + <generator> + <codeGeneratorClass> + org.opendaylight.yangtools.maven.sal.api.gen.plugin.CodeGeneratorImpl + </codeGeneratorClass> + <outputBaseDir>${salGeneratorPath}</outputBaseDir> + </generator> + </codeGenerators> + <inspectDependencies>true</inspectDependencies> + </configuration> + </execution> + </executions> + + <dependencies> + <dependency> + <groupId>org.opendaylight.mdsal</groupId> + <artifactId>maven-sal-api-gen-plugin</artifactId> + <version>${yangtools.version}</version> + <type>jar</type> + </dependency> + </dependencies> + </plugin> + </plugins> + </build> + <packaging>bundle</packaging> + +</project> diff --git a/odl-aaa-moon/aaa/aaa-authn-mdsal-store/aaa-authn-mdsal-api/src/main/yang/aaa-authn-model.yang b/odl-aaa-moon/aaa/aaa-authn-mdsal-store/aaa-authn-mdsal-api/src/main/yang/aaa-authn-model.yang new file mode 100644 index 00000000..227cb313 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn-mdsal-store/aaa-authn-mdsal-api/src/main/yang/aaa-authn-model.yang @@ -0,0 +1,154 @@ +module aaa-authn-model { + yang-version 1; + namespace "urn:aaa:yang:authn:claims"; + prefix "authn"; + organization "TBD"; + + contact "wdec@cisco.com"; + + revision 2014-10-29 { + description + "Initial revision."; + } + +//Main module begins + +// Following container provides the AuthN Claims data-structure + + container tokencache { + config false; + list claims { + key "token"; + + leaf token { + type string; + description "Token"; + } + leaf clientId { + type string; + description "id of the authorized client, or null if anonymous"; + } + leaf userId { + type string; + description "Unique user-id. User IDs are system-created"; + } + leaf user { + type string; + description "User name"; + } + leaf domain { + type string; + description "Fully-qualified domain name"; + } + leaf-list roles { + type string; + description "Assigned user roles"; + } + } + } + + container token_cache_times { + + list token_list { + key userId; + + leaf userId { + //TODO: Change to instance-ref + type string; + } + + list user_tokens { + key tokenid; + leaf tokenid { + type leafref {path "/tokencache/claims/token";} + } + leaf timestamp { + type uint64; + } + leaf expiration { + type int64; + description "Expiration milliseconds since start of UTC epoch"; + } + } + } + } + + //authentication model is for generating objects to be stores in the + //data store for all the prev idm model objects. + container authentication{ + list domain{ + key domainid; + leaf domainid { + type string; + } + leaf name { + type string; + } + leaf description { + type string; + } + leaf enabled { + type boolean; + } + } + + list user { + key userid; + leaf userid { + type string; + } + leaf name { + type string; + } + leaf description { + type string; + } + leaf enabled { + type boolean; + } + leaf email { + type string; + } + leaf password { + type string; + } + leaf salt { + type string; + } + leaf domainid { + type string; + } + } + list role { + key roleid; + leaf roleid { + type string; + } + leaf name { + type string; + } + leaf description { + type string; + } + leaf domainid { + type string; + } + } + + list grant { + key grantid; + leaf grantid { + type string; + } + leaf domainid { + type string; + } + leaf userid { + type string; + } + leaf roleid { + type string; + } + } + } +} diff --git a/odl-aaa-moon/aaa/aaa-authn-mdsal-store/aaa-authn-mdsal-config/pom.xml b/odl-aaa-moon/aaa/aaa-authn-mdsal-store/aaa-authn-mdsal-config/pom.xml new file mode 100644 index 00000000..3ac6e57f --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn-mdsal-store/aaa-authn-mdsal-config/pom.xml @@ -0,0 +1,40 @@ +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + <parent> + <groupId>org.opendaylight.aaa</groupId> + <artifactId>aaa-parent</artifactId> + <version>0.3.2-Beryllium-SR2</version> + <relativePath>../../parent</relativePath> + </parent> + + <artifactId>aaa-authn-mdsal-config</artifactId> + <description>AuthN Token Store Service Configuration file </description> + <packaging>jar</packaging> + + <build> + <plugins> + <plugin> + <groupId>org.codehaus.mojo</groupId> + <artifactId>build-helper-maven-plugin</artifactId> + <executions> + <execution> + <id>attach-artifacts</id> + <goals> + <goal>attach-artifact</goal> + </goals> + <phase>package</phase> + <configuration> + <artifacts> + <artifact> + <file>${project.build.directory}/classes/initial/${config.authn.store.configfile}</file> + <type>xml</type> + <classifier>config</classifier> + </artifact> + </artifacts> + </configuration> + </execution> + </executions> + </plugin> + </plugins> + </build> +</project> diff --git a/odl-aaa-moon/aaa/aaa-authn-mdsal-store/aaa-authn-mdsal-config/src/main/resources/initial/08-authn-config.xml b/odl-aaa-moon/aaa/aaa-authn-mdsal-store/aaa-authn-mdsal-config/src/main/resources/initial/08-authn-config.xml new file mode 100644 index 00000000..e4a78f4d --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn-mdsal-store/aaa-authn-mdsal-config/src/main/resources/initial/08-authn-config.xml @@ -0,0 +1,43 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- vi: set et smarttab sw=4 tabstop=4: --> +<!-- + Copyright (c) 2015 Cisco Systems, Inc. and others. 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 +--> +<snapshot> + <configuration> + <data xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"> + <modules xmlns="urn:opendaylight:params:xml:ns:yang:controller:config"> + + <!-- defines an implementation module --> + <module> + <type xmlns:authn="config:aaa:authn:mdsal:store">authn:aaa-authn-mdsal-store</type> + <name>aaa-authn-mdsal-store</name> + <dom-broker> + <type xmlns:dom="urn:opendaylight:params:xml:ns:yang:controller:md:sal:dom"> + dom:dom-broker-osgi-registry + </type> + <name>dom-broker</name> + </dom-broker> + <data-broker> + <type xmlns:binding="urn:opendaylight:params:xml:ns:yang:controller:md:sal:binding"> + binding:binding-async-data-broker + </type> + <name>binding-data-broker</name> + </data-broker> + <timeToLive>3600000</timeToLive> + <timeToWait>15</timeToWait> + <password>CHANGE_ME</password> + </module> + </modules> + </data> + + </configuration> + <required-capabilities> + <capability>config:aaa:authn:mdsal:store?module=aaa-authn-mdsal-store-cfg&revision=2014-10-31</capability> + </required-capabilities> + +</snapshot> diff --git a/odl-aaa-moon/aaa/aaa-authn-mdsal-store/aaa-authn-mdsal-store-impl/pom.xml b/odl-aaa-moon/aaa/aaa-authn-mdsal-store/aaa-authn-mdsal-store-impl/pom.xml new file mode 100644 index 00000000..069ec60c --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn-mdsal-store/aaa-authn-mdsal-store-impl/pom.xml @@ -0,0 +1,169 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + ~ Copyright (c) 2015 Cisco Systems, Inc. and others. 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 + ~ + --> + +<project xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + <parent> + <groupId>org.opendaylight.aaa</groupId> + <artifactId>aaa-parent</artifactId> + <version>0.3.2-Beryllium-SR2</version> + <relativePath>../../parent</relativePath> + </parent> + + <artifactId>aaa-authn-mdsal-store-impl</artifactId> + <packaging>bundle</packaging> + + <properties> + <powermock.version>1.5.2</powermock.version> + </properties> + + <dependencies> + <dependency> + <groupId>org.opendaylight.controller</groupId> + <artifactId>sal-binding-util</artifactId> + </dependency> + <dependency> + <groupId>org.opendaylight.controller</groupId> + <artifactId>sal-common-util</artifactId> + </dependency> + <dependency> + <groupId>org.opendaylight.yangtools</groupId> + <artifactId>yang-data-api</artifactId> + </dependency> + <dependency> + <groupId>commons-codec</groupId> + <artifactId>commons-codec</artifactId> + </dependency> + <dependency> + <groupId>org.opendaylight.controller</groupId> + <artifactId>sal-binding-api</artifactId> + </dependency> + <dependency> + <groupId>org.opendaylight.controller</groupId> + <artifactId>config-api</artifactId> + </dependency> + <dependency> + <groupId>org.opendaylight.controller</groupId> + <artifactId>sal-binding-config</artifactId> + </dependency> + <dependency> + <groupId>org.opendaylight.aaa</groupId> + <artifactId>aaa-authn-api</artifactId> + </dependency> + <dependency> + <groupId>org.opendaylight.aaa</groupId> + <artifactId>aaa-authn</artifactId> + </dependency> + <dependency> + <groupId>org.opendaylight.controller</groupId> + <artifactId>sal-core-api</artifactId> + </dependency> + <dependency> + <groupId>org.opendaylight.aaa</groupId> + <artifactId>aaa-authn-mdsal-api</artifactId> + </dependency> + + <!-- Test dependencies --> + <dependency> + <groupId>junit</groupId> + <artifactId>junit</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.mockito</groupId> + <artifactId>mockito-all</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.slf4j</groupId> + <artifactId>slf4j-simple</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.powermock</groupId> + <artifactId>powermock-api-mockito</artifactId> + <version>${powermock.version}</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.powermock</groupId> + <artifactId>powermock-module-junit4</artifactId> + <version>${powermock.version}</version> + <scope>test</scope> + </dependency> + </dependencies> + + <build> + <plugins> + <plugin> + <groupId>org.apache.felix</groupId> + <artifactId>maven-bundle-plugin</artifactId> + <extensions>true</extensions> + <configuration> + <instructions> + <!-- <Bundle-Activator>/Bundle-Activator> --> + <Export-Package>org.opendaylight.yang.gen.v1.config.aaa.authn.mdsal.store.* + </Export-Package> + </instructions> + </configuration> + <!-- <configuration> <Export-Package> </Export-Package> </configuration> --> + </plugin> + <plugin> + <groupId>org.opendaylight.yangtools</groupId> + <artifactId>yang-maven-plugin</artifactId> + <version>${yangtools.version}</version> + <executions> + <execution> + <id>config</id> + <goals> + <goal>generate-sources</goal> + </goals> + <configuration> + <codeGenerators> + <generator> + <codeGeneratorClass> + org.opendaylight.controller.config.yangjmxgenerator.plugin.JMXGenerator + </codeGeneratorClass> + <outputBaseDir>${jmxGeneratorPath}</outputBaseDir> + <additionalConfiguration> + <namespaceToPackage1> + urn:opendaylight:params:xml:ns:yang:controller==org.opendaylight.controller.config.yang + </namespaceToPackage1> + </additionalConfiguration> + </generator> + <generator> + <codeGeneratorClass>org.opendaylight.yangtools.maven.sal.api.gen.plugin.CodeGeneratorImpl</codeGeneratorClass> + <outputBaseDir>${salGeneratorPath}</outputBaseDir> + </generator> + </codeGenerators> + <inspectDependencies>true</inspectDependencies> + </configuration> + </execution> + </executions> + <dependencies> + <dependency> + <groupId>org.opendaylight.controller</groupId> + <artifactId>yang-jmx-generator-plugin</artifactId> + <version>${config.version}</version> + </dependency> + <dependency> + <groupId>org.opendaylight.mdsal</groupId> + <artifactId>maven-sal-api-gen-plugin</artifactId> + <version>${yangtools.version}</version> + </dependency> + </dependencies> + </plugin> + </plugins> + </build> + + +</project> diff --git a/odl-aaa-moon/aaa/aaa-authn-mdsal-store/aaa-authn-mdsal-store-impl/src/main/java/org/opendaylight/aaa/authn/mdsal/store/AuthNStore.java b/odl-aaa-moon/aaa/aaa-authn-mdsal-store/aaa-authn-mdsal-store-impl/src/main/java/org/opendaylight/aaa/authn/mdsal/store/AuthNStore.java new file mode 100644 index 00000000..09170182 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn-mdsal-store/aaa-authn-mdsal-store-impl/src/main/java/org/opendaylight/aaa/authn/mdsal/store/AuthNStore.java @@ -0,0 +1,263 @@ +/* + * Copyright (c) 2015 Cisco Systems, Inc. and others. 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.authn.mdsal.store; + +import com.google.common.base.Optional; +import com.google.common.util.concurrent.CheckedFuture; +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import java.math.BigInteger; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import org.opendaylight.aaa.api.Authentication; +import org.opendaylight.aaa.api.TokenStore; +import org.opendaylight.aaa.authn.mdsal.store.util.AuthNStoreUtil; +import org.opendaylight.controller.md.sal.binding.api.DataBroker; +import org.opendaylight.controller.md.sal.binding.api.ReadTransaction; +import org.opendaylight.controller.md.sal.binding.api.WriteTransaction; +import org.opendaylight.controller.md.sal.common.api.data.LogicalDatastoreType; +import org.opendaylight.controller.md.sal.common.api.data.ReadFailedException; +import org.opendaylight.controller.md.sal.common.api.data.TransactionCommitFailedException; +import org.opendaylight.yang.gen.v1.urn.aaa.yang.authn.claims.rev141029.TokenCacheTimes; +import org.opendaylight.yang.gen.v1.urn.aaa.yang.authn.claims.rev141029.token_cache_times.TokenList; +import org.opendaylight.yang.gen.v1.urn.aaa.yang.authn.claims.rev141029.token_cache_times.TokenListKey; +import org.opendaylight.yang.gen.v1.urn.aaa.yang.authn.claims.rev141029.token_cache_times.token_list.UserTokens; +import org.opendaylight.yang.gen.v1.urn.aaa.yang.authn.claims.rev141029.tokencache.Claims; +import org.opendaylight.yangtools.yang.binding.InstanceIdentifier; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class AuthNStore implements AutoCloseable, TokenStore { + + private static final Logger LOG = LoggerFactory.getLogger(AuthNStore.class); + private DataBroker broker; + private static BigInteger timeToLive; + private static Integer timeToWait; + private final ExecutorService deleteExpiredTokenThread = Executors.newFixedThreadPool(1); + private final DataEncrypter dataEncrypter; + + public AuthNStore(final DataBroker dataBroker, final String config_key) { + this.broker = dataBroker; + this.dataEncrypter = new DataEncrypter(config_key); + LOG.info("Created MD-SAL AAA Token Cache Service..."); + } + + @Override + public void close() throws Exception { + deleteExpiredTokenThread.shutdown(); + LOG.info("MD-SAL AAA Token Cache closed..."); + + } + + @Override + public void put(String token, Authentication auth) { + token = dataEncrypter.encrypt(token); + Claims claims = AuthNStoreUtil.createClaimsRecord(token, auth); + + // create and insert parallel struct + UserTokens userTokens = AuthNStoreUtil.createUserTokens(token, timeToLive.longValue()); + TokenList tokenlist = AuthNStoreUtil.createTokenList(userTokens, auth.userId()); + + writeClaimAndTokenToStore(claims, userTokens, tokenlist); + deleteExpiredTokenThread.execute(deleteOldTokens(claims)); + } + + @Override + public Authentication get(String token) { + token = dataEncrypter.encrypt(token); + Authentication authentication = null; + Claims claims = readClaims(token); + if (claims != null) { + UserTokens userToken = readUserTokensFromDS(claims.getToken(), claims.getUserId()); + authentication = AuthNStoreUtil.convertClaimToAuthentication(claims, + userToken.getExpiration()); + } + deleteExpiredTokenThread.execute(deleteOldTokens(claims)); + return authentication; + } + + @Override + public boolean delete(String token) { + token = dataEncrypter.encrypt(token); + boolean result = false; + Claims claims = readClaims(token); + result = deleteClaims(token); + if (result) { + deleteUserTokenFromDS(token, claims.getUserId()); + } + deleteExpiredTokenThread.execute(deleteOldTokens(claims)); + return result; + } + + @Override + public long tokenExpiration() { + return timeToLive.longValue(); + } + + public void setTimeToLive(BigInteger timeToLive) { + this.timeToLive = timeToLive; + } + + public void setTimeToWait(Integer timeToWait) { + this.timeToWait = timeToWait; + } + + private void writeClaimAndTokenToStore(final Claims claims, UserTokens usertokens, + final TokenList tokenlist) { + + final InstanceIdentifier<Claims> claims_iid = AuthNStoreUtil.createInstIdentifierForTokencache(claims.getToken()); + WriteTransaction tx = broker.newWriteOnlyTransaction(); + tx.put(LogicalDatastoreType.OPERATIONAL, claims_iid, claims, true); + + final InstanceIdentifier<UserTokens> userTokens_iid = AuthNStoreUtil.createInstIdentifierUserTokens( + tokenlist.getUserId(), usertokens.getTokenid()); + tx.put(LogicalDatastoreType.OPERATIONAL, userTokens_iid, usertokens, true); + + CheckedFuture<Void, TransactionCommitFailedException> commitFuture = tx.submit(); + Futures.addCallback(commitFuture, new FutureCallback<Void>() { + + @Override + public void onSuccess(Void result) { + LOG.trace("Token {} was written to datastore.", claims.getToken()); + LOG.trace("Tokenlist for userId {} was written to datastore.", + tokenlist.getUserId()); + } + + @Override + public void onFailure(Throwable t) { + LOG.error("Inserting token {} to datastore failed.", claims.getToken()); + LOG.trace("Inserting for userId {} tokenlist to datastore failed.", + tokenlist.getUserId()); + } + + }); + } + + private Claims readClaims(String token) { + final InstanceIdentifier<Claims> claims_iid = AuthNStoreUtil.createInstIdentifierForTokencache(token); + Claims claims = null; + ReadTransaction rt = broker.newReadOnlyTransaction(); + CheckedFuture<Optional<Claims>, ReadFailedException> claimsFuture = rt.read( + LogicalDatastoreType.OPERATIONAL, claims_iid); + try { + Optional<Claims> maybeClaims = claimsFuture.checkedGet(); + if (maybeClaims.isPresent()) { + claims = maybeClaims.get(); + } + } catch (ReadFailedException e) { + LOG.error( + "Something wrong happened in DataStore. Getting Claim for token {} failed.", + token, e); + } + return claims; + } + + private TokenList readTokenListFromDS(String userId) { + InstanceIdentifier<TokenList> tokenList_iid = InstanceIdentifier.builder( + TokenCacheTimes.class).child(TokenList.class, new TokenListKey(userId)).build(); + TokenList tokenList = null; + ReadTransaction rt = broker.newReadOnlyTransaction(); + CheckedFuture<Optional<TokenList>, ReadFailedException> userTokenListFuture = rt.read( + LogicalDatastoreType.OPERATIONAL, tokenList_iid); + try { + Optional<TokenList> maybeTokenList = userTokenListFuture.checkedGet(); + if (maybeTokenList.isPresent()) { + tokenList = maybeTokenList.get(); + } + } catch (ReadFailedException e) { + LOG.error( + "Something wrong happened in DataStore. Getting TokenList for userId {} failed.", + userId, e); + } + return tokenList; + } + + private UserTokens readUserTokensFromDS(String token, String userId) { + final InstanceIdentifier<UserTokens> userTokens_iid = AuthNStoreUtil.createInstIdentifierUserTokens( + userId, token); + UserTokens userTokens = null; + + ReadTransaction rt = broker.newReadOnlyTransaction(); + CheckedFuture<Optional<UserTokens>, ReadFailedException> userTokensFuture = rt.read( + LogicalDatastoreType.OPERATIONAL, userTokens_iid); + + try { + Optional<UserTokens> maybeUserTokens = userTokensFuture.checkedGet(); + if (maybeUserTokens.isPresent()) { + userTokens = maybeUserTokens.get(); + } + } catch (ReadFailedException e) { + LOG.error( + "Something wrong happened in DataStore. Getting UserTokens for token {} failed.", + token, e); + } + + return userTokens; + } + + private boolean deleteClaims(String token) { + final InstanceIdentifier<Claims> claims_iid = AuthNStoreUtil.createInstIdentifierForTokencache(token); + boolean result = false; + WriteTransaction tx = broker.newWriteOnlyTransaction(); + tx.delete(LogicalDatastoreType.OPERATIONAL, claims_iid); + CheckedFuture<Void, TransactionCommitFailedException> commitFuture = tx.submit(); + + try { + commitFuture.checkedGet(); + result = true; + } catch (TransactionCommitFailedException e) { + LOG.error("Something wrong happened in DataStore. Claim " + + "deletion for token {} from DataStore failed.", token, e); + } + return result; + } + + private void deleteUserTokenFromDS(String token, String userId) { + final InstanceIdentifier<UserTokens> userTokens_iid = AuthNStoreUtil.createInstIdentifierUserTokens( + userId, token); + + WriteTransaction tx = broker.newWriteOnlyTransaction(); + tx.delete(LogicalDatastoreType.OPERATIONAL, userTokens_iid); + CheckedFuture<Void, TransactionCommitFailedException> commitFuture = tx.submit(); + try { + commitFuture.checkedGet(); + } catch (TransactionCommitFailedException e) { + LOG.error("Something wrong happened in DataStore. UserToken " + + "deletion for token {} from DataStore failed.", token, e); + } + } + + private Runnable deleteOldTokens(final Claims claims) { + return new Runnable() { + + @Override + public void run() { + TokenList tokenList = null; + if (claims != null) { + tokenList = readTokenListFromDS(claims.getUserId()); + } + if (tokenList != null) { + for (UserTokens currUserToken : tokenList.getUserTokens()) { + long diff = System.currentTimeMillis() + - currUserToken.getTimestamp().longValue(); + if (diff > currUserToken.getExpiration() + && currUserToken.getExpiration() != 0) { + if (deleteClaims(currUserToken.getTokenid())) { + deleteUserTokenFromDS(currUserToken.getTokenid(), + claims.getUserId()); + LOG.trace("Expired tokens for UserId {} deleted.", + claims.getUserId()); + } + } + } + } + } + }; + } +} diff --git a/odl-aaa-moon/aaa/aaa-authn-mdsal-store/aaa-authn-mdsal-store-impl/src/main/java/org/opendaylight/aaa/authn/mdsal/store/DataEncrypter.java b/odl-aaa-moon/aaa/aaa-authn-mdsal-store/aaa-authn-mdsal-store-impl/src/main/java/org/opendaylight/aaa/authn/mdsal/store/DataEncrypter.java new file mode 100644 index 00000000..ca0a74be --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn-mdsal-store/aaa-authn-mdsal-store-impl/src/main/java/org/opendaylight/aaa/authn/mdsal/store/DataEncrypter.java @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2015 Cisco Systems, Inc. and others. 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.authn.mdsal.store; + +import java.security.spec.KeySpec; +import javax.crypto.Cipher; +import javax.crypto.SecretKey; +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.PBEKeySpec; +import javax.crypto.spec.SecretKeySpec; +import javax.xml.bind.DatatypeConverter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * @author - Sharon Aicler (saichler@cisco.com) + **/ +public class DataEncrypter { + + final protected SecretKey k; + private static final Logger LOG = LoggerFactory.getLogger(DataEncrypter.class); + private static final byte[] iv = { 0, 5, 0, 0, 7, 81, 0, 3, 0, 0, 0, 0, 0, 43, 0, 1 }; + private static final IvParameterSpec ivspec = new IvParameterSpec(iv); + public static final String ENCRYPTED_TAG = "Encrypted:"; + + public DataEncrypter(final String ckey) { + SecretKey tmp = null; + if (ckey != null && !ckey.isEmpty()) { + + try { + SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1"); + KeySpec spec = new PBEKeySpec(ckey.toCharArray(), iv, 32768, 128); + tmp = keyFactory.generateSecret(spec); + } catch (Exception e) { + LOG.error("Couldn't initialize key factory", e); + } + if (tmp != null) { + k = new SecretKeySpec(tmp.getEncoded(), "AES"); + } else { + throw new RuntimeException("Couldn't initalize encryption key"); + } + } else { + k = null; + LOG.warn("Void crypto key passed! AuthN Store Encryption disabled"); + } + + } + + protected String encrypt(String token) { + + if (k == null) { + return token; + } + + String cryptostring = null; + try { + Cipher c = Cipher.getInstance("AES/CBC/PKCS5Padding"); + c.init(Cipher.ENCRYPT_MODE, k, ivspec); + byte[] cryptobytes = c.doFinal(token.getBytes()); + cryptostring = DatatypeConverter.printBase64Binary(cryptobytes); + return ENCRYPTED_TAG + cryptostring; + } catch (Exception e) { + LOG.error("Couldn't encrypt token", e); + return null; + } + } + + protected String decrypt(String eToken) { + if (k == null) { + return eToken; + } + + if (eToken == null || eToken.length() == 0) { + return null; + } + + if (!eToken.startsWith(ENCRYPTED_TAG)) { + return eToken; + } + + try { + Cipher c = Cipher.getInstance("AES/CBC/PKCS5Padding"); + c.init(Cipher.DECRYPT_MODE, k, ivspec); + + byte[] cryptobytes = DatatypeConverter.parseBase64Binary(eToken.substring(ENCRYPTED_TAG.length())); + byte[] clearbytes = c.doFinal(cryptobytes); + return DatatypeConverter.printBase64Binary(clearbytes); + + } catch (Exception e) { + LOG.error("Couldn't decrypt token", e); + return null; + } + } +} diff --git a/odl-aaa-moon/aaa/aaa-authn-mdsal-store/aaa-authn-mdsal-store-impl/src/main/java/org/opendaylight/aaa/authn/mdsal/store/IDMMDSALStore.java b/odl-aaa-moon/aaa/aaa-authn-mdsal-store/aaa-authn-mdsal-store-impl/src/main/java/org/opendaylight/aaa/authn/mdsal/store/IDMMDSALStore.java new file mode 100644 index 00000000..88fba0ba --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn-mdsal-store/aaa-authn-mdsal-store-impl/src/main/java/org/opendaylight/aaa/authn/mdsal/store/IDMMDSALStore.java @@ -0,0 +1,483 @@ +/* + * Copyright (c) 2015 Cisco Systems, Inc. and others. 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.authn.mdsal.store; + +import com.google.common.base.Optional; +import com.google.common.base.Preconditions; +import com.google.common.util.concurrent.CheckedFuture; +import java.util.List; +import java.util.concurrent.ExecutionException; +import org.opendaylight.aaa.api.IDMStoreException; +import org.opendaylight.aaa.api.IDMStoreUtil; +import org.opendaylight.aaa.api.SHA256Calculator; +import org.opendaylight.controller.md.sal.binding.api.DataBroker; +import org.opendaylight.controller.md.sal.binding.api.ReadOnlyTransaction; +import org.opendaylight.controller.md.sal.binding.api.WriteTransaction; +import org.opendaylight.controller.md.sal.common.api.data.LogicalDatastoreType; +import org.opendaylight.controller.md.sal.common.api.data.ReadFailedException; +import org.opendaylight.controller.md.sal.common.api.data.TransactionCommitFailedException; +import org.opendaylight.yang.gen.v1.urn.aaa.yang.authn.claims.rev141029.Authentication; +import org.opendaylight.yang.gen.v1.urn.aaa.yang.authn.claims.rev141029.authentication.Domain; +import org.opendaylight.yang.gen.v1.urn.aaa.yang.authn.claims.rev141029.authentication.DomainBuilder; +import org.opendaylight.yang.gen.v1.urn.aaa.yang.authn.claims.rev141029.authentication.DomainKey; +import org.opendaylight.yang.gen.v1.urn.aaa.yang.authn.claims.rev141029.authentication.Grant; +import org.opendaylight.yang.gen.v1.urn.aaa.yang.authn.claims.rev141029.authentication.GrantBuilder; +import org.opendaylight.yang.gen.v1.urn.aaa.yang.authn.claims.rev141029.authentication.GrantKey; +import org.opendaylight.yang.gen.v1.urn.aaa.yang.authn.claims.rev141029.authentication.Role; +import org.opendaylight.yang.gen.v1.urn.aaa.yang.authn.claims.rev141029.authentication.RoleBuilder; +import org.opendaylight.yang.gen.v1.urn.aaa.yang.authn.claims.rev141029.authentication.RoleKey; +import org.opendaylight.yang.gen.v1.urn.aaa.yang.authn.claims.rev141029.authentication.User; +import org.opendaylight.yang.gen.v1.urn.aaa.yang.authn.claims.rev141029.authentication.UserBuilder; +import org.opendaylight.yang.gen.v1.urn.aaa.yang.authn.claims.rev141029.authentication.UserKey; +import org.opendaylight.yangtools.yang.binding.InstanceIdentifier; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * @author Sharon Aicler - saichler@cisco.com + * + */ +public class IDMMDSALStore { + + private static final Logger LOG = LoggerFactory.getLogger(IDMMDSALStore.class); + private final DataBroker dataBroker; + + public IDMMDSALStore(DataBroker dataBroker) { + this.dataBroker = dataBroker; + } + + public static final String getString(String aValue, String bValue) { + if (aValue != null) + return aValue; + return bValue; + } + + public static final Boolean getBoolean(Boolean aValue, Boolean bValue) { + if (aValue != null) + return aValue; + return bValue; + } + + public static boolean waitForSubmit(CheckedFuture<Void, TransactionCommitFailedException> submit) { + // This can happen only when testing + if (submit == null) + return false; + while (!submit.isDone() && !submit.isCancelled()) { + try { + Thread.sleep(1000); + } catch (Exception err) { + LOG.error("Interrupted", err); + } + } + return submit.isCancelled(); + } + + // Domain methods + public Domain writeDomain(Domain domain) { + Preconditions.checkNotNull(domain); + Preconditions.checkNotNull(domain.getName()); + Preconditions.checkNotNull(domain.isEnabled()); + DomainBuilder b = new DomainBuilder(); + b.setDescription(domain.getDescription()); + b.setDomainid(domain.getName()); + b.setEnabled(domain.isEnabled()); + b.setName(domain.getName()); + b.setKey(new DomainKey(b.getName())); + domain = b.build(); + InstanceIdentifier<Domain> ID = InstanceIdentifier.create(Authentication.class).child( + Domain.class, new DomainKey(domain.getDomainid())); + WriteTransaction wrt = dataBroker.newWriteOnlyTransaction(); + wrt.put(LogicalDatastoreType.CONFIGURATION, ID, domain, true); + CheckedFuture<Void, TransactionCommitFailedException> submit = wrt.submit(); + if (!waitForSubmit(submit)) { + return domain; + } else { + return null; + } + } + + public Domain readDomain(String domainid) { + Preconditions.checkNotNull(domainid); + InstanceIdentifier<Domain> ID = InstanceIdentifier.create(Authentication.class).child( + Domain.class, new DomainKey(domainid)); + ReadOnlyTransaction rot = dataBroker.newReadOnlyTransaction(); + CheckedFuture<Optional<Domain>, ReadFailedException> read = rot.read( + LogicalDatastoreType.CONFIGURATION, ID); + if (read == null) { + LOG.error("Failed to read domain from data store"); + return null; + } + Optional<Domain> optional = null; + try { + optional = read.get(); + } catch (InterruptedException | ExecutionException e1) { + LOG.error("Failed to read domain from data store", e1); + return null; + } + + if (optional == null) + return null; + + if (!optional.isPresent()) + return null; + + return optional.get(); + } + + public Domain deleteDomain(String domainid) { + Preconditions.checkNotNull(domainid); + Domain domain = readDomain(domainid); + if (domain == null) { + LOG.error("Failed to delete domain from data store, unknown domain"); + return null; + } + InstanceIdentifier<Domain> ID = InstanceIdentifier.create(Authentication.class).child( + Domain.class, new DomainKey(domainid)); + WriteTransaction wrt = dataBroker.newWriteOnlyTransaction(); + wrt.delete(LogicalDatastoreType.CONFIGURATION, ID); + wrt.submit(); + return domain; + } + + public Domain updateDomain(Domain domain) throws IDMStoreException { + Preconditions.checkNotNull(domain); + Preconditions.checkNotNull(domain.getDomainid()); + Domain existing = readDomain(domain.getDomainid()); + DomainBuilder b = new DomainBuilder(); + b.setDescription(getString(domain.getDescription(), existing.getDescription())); + b.setName(existing.getName()); + b.setEnabled(getBoolean(domain.isEnabled(), existing.isEnabled())); + return writeDomain(b.build()); + } + + public List<Domain> getAllDomains() { + InstanceIdentifier<Authentication> id = InstanceIdentifier.create(Authentication.class); + ReadOnlyTransaction rot = dataBroker.newReadOnlyTransaction(); + CheckedFuture<Optional<Authentication>, ReadFailedException> read = rot.read( + LogicalDatastoreType.CONFIGURATION, id); + if (read == null) + return null; + + try { + if (read.get() == null) + return null; + if (read.get().isPresent()) { + Authentication auth = read.get().get(); + return auth.getDomain(); + } + } catch (Exception err) { + LOG.error("Failed to read domains", err); + } + return null; + } + + public List<Role> getAllRoles() { + InstanceIdentifier<Authentication> id = InstanceIdentifier.create(Authentication.class); + ReadOnlyTransaction rot = dataBroker.newReadOnlyTransaction(); + CheckedFuture<Optional<Authentication>, ReadFailedException> read = rot.read( + LogicalDatastoreType.CONFIGURATION, id); + if (read == null) + return null; + + try { + if (read.get() == null) + return null; + if (read.get().isPresent()) { + Authentication auth = read.get().get(); + return auth.getRole(); + } + } catch (Exception err) { + LOG.error("Failed to read domains", err); + } + return null; + } + + public List<User> getAllUsers() { + InstanceIdentifier<Authentication> id = InstanceIdentifier.create(Authentication.class); + ReadOnlyTransaction rot = dataBroker.newReadOnlyTransaction(); + CheckedFuture<Optional<Authentication>, ReadFailedException> read = rot.read( + LogicalDatastoreType.CONFIGURATION, id); + if (read == null) + return null; + + try { + if (read.get() == null) + return null; + if (read.get().isPresent()) { + Authentication auth = read.get().get(); + return auth.getUser(); + } + } catch (Exception err) { + LOG.error("Failed to read domains", err); + } + return null; + } + + public List<Grant> getAllGrants() { + InstanceIdentifier<Authentication> id = InstanceIdentifier.create(Authentication.class); + ReadOnlyTransaction rot = dataBroker.newReadOnlyTransaction(); + CheckedFuture<Optional<Authentication>, ReadFailedException> read = rot.read( + LogicalDatastoreType.CONFIGURATION, id); + if (read == null) + return null; + + try { + if (read.get() == null) + return null; + if (read.get().isPresent()) { + Authentication auth = read.get().get(); + return auth.getGrant(); + } + } catch (Exception err) { + LOG.error("Failed to read domains", err); + } + return null; + } + + // Role methods + public Role writeRole(Role role) { + Preconditions.checkNotNull(role); + Preconditions.checkNotNull(role.getName()); + Preconditions.checkNotNull(role.getDomainid()); + Preconditions.checkNotNull(readDomain(role.getDomainid())); + RoleBuilder b = new RoleBuilder(); + b.setDescription(role.getDescription()); + b.setRoleid(IDMStoreUtil.createRoleid(role.getName(), role.getDomainid())); + b.setKey(new RoleKey(b.getRoleid())); + b.setName(role.getName()); + b.setDomainid(role.getDomainid()); + role = b.build(); + InstanceIdentifier<Role> ID = InstanceIdentifier.create(Authentication.class).child( + Role.class, new RoleKey(role.getRoleid())); + WriteTransaction wrt = dataBroker.newWriteOnlyTransaction(); + wrt.put(LogicalDatastoreType.CONFIGURATION, ID, role, true); + CheckedFuture<Void, TransactionCommitFailedException> submit = wrt.submit(); + if (!waitForSubmit(submit)) { + return role; + } else { + return null; + } + } + + public Role readRole(String roleid) { + Preconditions.checkNotNull(roleid); + InstanceIdentifier<Role> ID = InstanceIdentifier.create(Authentication.class).child( + Role.class, new RoleKey(roleid)); + ReadOnlyTransaction rot = dataBroker.newReadOnlyTransaction(); + CheckedFuture<Optional<Role>, ReadFailedException> read = rot.read( + LogicalDatastoreType.CONFIGURATION, ID); + if (read == null) { + LOG.error("Failed to read role from data store"); + return null; + } + Optional<Role> optional = null; + try { + optional = read.get(); + } catch (InterruptedException | ExecutionException e1) { + LOG.error("Failed to read role from data store", e1); + return null; + } + + if (optional == null) + return null; + + if (!optional.isPresent()) + return null; + + return optional.get(); + } + + public Role deleteRole(String roleid) { + Preconditions.checkNotNull(roleid); + Role role = readRole(roleid); + if (role == null) { + LOG.error("Failed to delete role from data store, unknown role"); + return null; + } + InstanceIdentifier<Role> ID = InstanceIdentifier.create(Authentication.class).child( + Role.class, new RoleKey(roleid)); + WriteTransaction wrt = dataBroker.newWriteOnlyTransaction(); + wrt.delete(LogicalDatastoreType.CONFIGURATION, ID); + wrt.submit(); + return role; + } + + public Role updateRole(Role role) { + Preconditions.checkNotNull(role); + Preconditions.checkNotNull(role.getRoleid()); + Role existing = readRole(role.getRoleid()); + RoleBuilder b = new RoleBuilder(); + b.setDescription(getString(role.getDescription(), existing.getDescription())); + b.setName(existing.getName()); + b.setDomainid(existing.getDomainid()); + return writeRole(b.build()); + } + + // User methods + public User writeUser(User user) throws IDMStoreException { + Preconditions.checkNotNull(user); + Preconditions.checkNotNull(user.getName()); + Preconditions.checkNotNull(user.getDomainid()); + Preconditions.checkNotNull(readDomain(user.getDomainid())); + UserBuilder b = new UserBuilder(); + if (user.getSalt() == null) { + b.setSalt(SHA256Calculator.generateSALT()); + } else { + b.setSalt(user.getSalt()); + } + b.setUserid(IDMStoreUtil.createUserid(user.getName(), user.getDomainid())); + b.setDescription(user.getDescription()); + b.setDomainid(user.getDomainid()); + b.setEmail(user.getEmail()); + b.setEnabled(user.isEnabled()); + b.setKey(new UserKey(b.getUserid())); + b.setName(user.getName()); + b.setPassword(SHA256Calculator.getSHA256(user.getPassword(), b.getSalt())); + user = b.build(); + InstanceIdentifier<User> ID = InstanceIdentifier.create(Authentication.class).child( + User.class, new UserKey(user.getUserid())); + WriteTransaction wrt = dataBroker.newWriteOnlyTransaction(); + wrt.put(LogicalDatastoreType.CONFIGURATION, ID, user, true); + CheckedFuture<Void, TransactionCommitFailedException> submit = wrt.submit(); + if (!waitForSubmit(submit)) { + return user; + } else { + return null; + } + } + + public User readUser(String userid) { + Preconditions.checkNotNull(userid); + InstanceIdentifier<User> ID = InstanceIdentifier.create(Authentication.class).child( + User.class, new UserKey(userid)); + ReadOnlyTransaction rot = dataBroker.newReadOnlyTransaction(); + CheckedFuture<Optional<User>, ReadFailedException> read = rot.read( + LogicalDatastoreType.CONFIGURATION, ID); + if (read == null) { + LOG.error("Failed to read user from data store"); + return null; + } + Optional<User> optional = null; + try { + optional = read.get(); + } catch (InterruptedException | ExecutionException e1) { + LOG.error("Failed to read domain from data store", e1); + return null; + } + + if (optional == null) + return null; + + if (!optional.isPresent()) + return null; + + return optional.get(); + } + + public User deleteUser(String userid) { + Preconditions.checkNotNull(userid); + User user = readUser(userid); + if (user == null) { + LOG.error("Failed to delete user from data store, unknown user"); + return null; + } + InstanceIdentifier<User> ID = InstanceIdentifier.create(Authentication.class).child( + User.class, new UserKey(userid)); + WriteTransaction wrt = dataBroker.newWriteOnlyTransaction(); + wrt.delete(LogicalDatastoreType.CONFIGURATION, ID); + wrt.submit(); + return user; + } + + public User updateUser(User user) throws IDMStoreException { + Preconditions.checkNotNull(user); + Preconditions.checkNotNull(user.getUserid()); + User existing = readUser(user.getUserid()); + UserBuilder b = new UserBuilder(); + b.setName(existing.getName()); + b.setDomainid(existing.getDomainid()); + b.setDescription(getString(user.getDescription(), existing.getDescription())); + b.setEmail(getString(user.getEmail(), existing.getEmail())); + b.setEnabled(getBoolean(user.isEnabled(), existing.isEnabled())); + b.setPassword(getString(user.getPassword(), existing.getPassword())); + b.setSalt(getString(user.getSalt(), existing.getSalt())); + return writeUser(b.build()); + } + + // Grant methods + public Grant writeGrant(Grant grant) throws IDMStoreException { + Preconditions.checkNotNull(grant); + Preconditions.checkNotNull(grant.getDomainid()); + Preconditions.checkNotNull(grant.getUserid()); + Preconditions.checkNotNull(grant.getRoleid()); + Preconditions.checkNotNull(readDomain(grant.getDomainid())); + Preconditions.checkNotNull(readUser(grant.getUserid())); + Preconditions.checkNotNull(readRole(grant.getRoleid())); + GrantBuilder b = new GrantBuilder(); + b.setDomainid(grant.getDomainid()); + b.setRoleid(grant.getRoleid()); + b.setUserid(grant.getUserid()); + b.setGrantid(IDMStoreUtil.createGrantid(grant.getUserid(), grant.getDomainid(), + grant.getRoleid())); + b.setKey(new GrantKey(b.getGrantid())); + grant = b.build(); + InstanceIdentifier<Grant> ID = InstanceIdentifier.create(Authentication.class).child( + Grant.class, new GrantKey(grant.getGrantid())); + WriteTransaction wrt = dataBroker.newWriteOnlyTransaction(); + wrt.put(LogicalDatastoreType.CONFIGURATION, ID, grant, true); + CheckedFuture<Void, TransactionCommitFailedException> submit = wrt.submit(); + if (!waitForSubmit(submit)) { + return grant; + } else { + return null; + } + } + + public Grant readGrant(String grantid) { + Preconditions.checkNotNull(grantid); + InstanceIdentifier<Grant> ID = InstanceIdentifier.create(Authentication.class).child( + Grant.class, new GrantKey(grantid)); + ReadOnlyTransaction rot = dataBroker.newReadOnlyTransaction(); + CheckedFuture<Optional<Grant>, ReadFailedException> read = rot.read( + LogicalDatastoreType.CONFIGURATION, ID); + if (read == null) { + LOG.error("Failed to read grant from data store"); + return null; + } + Optional<Grant> optional = null; + try { + optional = read.get(); + } catch (InterruptedException | ExecutionException e1) { + LOG.error("Failed to read domain from data store", e1); + return null; + } + + if (optional == null) + return null; + + if (!optional.isPresent()) + return null; + + return optional.get(); + } + + public Grant deleteGrant(String grantid) { + Preconditions.checkNotNull(grantid); + Grant grant = readGrant(grantid); + if (grant == null) { + LOG.error("Failed to delete grant from data store, unknown grant"); + return null; + } + InstanceIdentifier<Grant> ID = InstanceIdentifier.create(Authentication.class).child( + Grant.class, new GrantKey(grantid)); + WriteTransaction wrt = dataBroker.newWriteOnlyTransaction(); + wrt.delete(LogicalDatastoreType.CONFIGURATION, ID); + wrt.submit(); + return grant; + } +} diff --git a/odl-aaa-moon/aaa/aaa-authn-mdsal-store/aaa-authn-mdsal-store-impl/src/main/java/org/opendaylight/aaa/authn/mdsal/store/IDMObject2MDSAL.java b/odl-aaa-moon/aaa/aaa-authn-mdsal-store/aaa-authn-mdsal-store-impl/src/main/java/org/opendaylight/aaa/authn/mdsal/store/IDMObject2MDSAL.java new file mode 100644 index 00000000..0b58ced7 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn-mdsal-store/aaa-authn-mdsal-store-impl/src/main/java/org/opendaylight/aaa/authn/mdsal/store/IDMObject2MDSAL.java @@ -0,0 +1,224 @@ +/* + * Copyright (c) 2015 Cisco Systems, Inc. and others. 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.authn.mdsal.store; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.opendaylight.aaa.api.model.Domain; +import org.opendaylight.aaa.api.model.Grant; +import org.opendaylight.aaa.api.model.Role; +import org.opendaylight.aaa.api.model.User; +import org.opendaylight.yang.gen.v1.urn.aaa.yang.authn.claims.rev141029.authentication.DomainBuilder; +import org.opendaylight.yang.gen.v1.urn.aaa.yang.authn.claims.rev141029.authentication.GrantBuilder; +import org.opendaylight.yang.gen.v1.urn.aaa.yang.authn.claims.rev141029.authentication.RoleBuilder; +import org.opendaylight.yang.gen.v1.urn.aaa.yang.authn.claims.rev141029.authentication.UserBuilder; +import org.opendaylight.yangtools.yang.binding.DataObject; +/** + * + * @author saichler@gmail.com + * + * This class is a codec to convert between MDSAL objects and IDM model objects. It is doing so via reflection when it assumes that the MDSAL + * Object and the IDM model object has the same method names. + */ +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * @author Sharon Aicler - saichler@cisco.com + * + */ +public abstract class IDMObject2MDSAL { + private static final Logger LOG = LoggerFactory.getLogger(IDMObject2MDSAL.class); + // this is a Map mapping between the class type of the IDM Model object to a + // structure containing the corresponding setters and getter methods + // in MDSAL object + private static Map<Class<?>, ConvertionMethods> typesMethods = new HashMap<Class<?>, ConvertionMethods>(); + + // This method generically via reflection receive a MDSAL object and the + // corresponding IDM model object class type and + // creates an IDM model element from the MDSAL element + private static Object fromMDSALObject(Object mdsalObject, Class<?> type) throws Exception { + if (mdsalObject == null) + return null; + Object result = type.newInstance(); + ConvertionMethods cm = typesMethods.get(type); + if (cm == null) { + cm = new ConvertionMethods(); + typesMethods.put(type, cm); + Method methods[] = type.getMethods(); + for (Method m : methods) { + if (m.getName().startsWith("set")) { + cm.setMethods.add(m); + Method gm = null; + if (m.getParameterTypes()[0].equals(Boolean.class) + || m.getParameterTypes()[0].equals(boolean.class)) + gm = ((DataObject) mdsalObject).getImplementedInterface().getMethod( + "is" + m.getName().substring(3), (Class[]) null); + else { + try { + gm = ((DataObject) mdsalObject).getImplementedInterface().getMethod( + "get" + m.getName().substring(3), (Class[]) null); + } catch (Exception err) { + LOG.error("Error associating get call", err); + } + } + cm.getMethods.put(m.getName(), gm); + } + } + } + for (Method m : cm.setMethods) { + try { + m.invoke( + result, + new Object[] { cm.getMethods.get(m.getName()).invoke(mdsalObject, + (Object[]) null) }); + } catch (Exception err) { + LOG.error("Error invoking reflection method", err); + } + } + return result; + } + + // This method generically use reflection to receive an IDM model object and + // the corresponsing MDSAL object and creates + // a MDSAL object out of the IDM model object + private static Object toMDSALObject(Object object, Class<?> mdSalBuilderType) throws Exception { + if (object == null) + return null; + Object result = mdSalBuilderType.newInstance(); + ConvertionMethods cm = typesMethods.get(mdSalBuilderType); + if (cm == null) { + cm = new ConvertionMethods(); + typesMethods.put(mdSalBuilderType, cm); + Method methods[] = mdSalBuilderType.getMethods(); + for (Method m : methods) { + if (m.getName().startsWith("set")) { + try { + Method gm = null; + if (m.getParameterTypes()[0].equals(Boolean.class) + || m.getParameterTypes()[0].equals(boolean.class)) + gm = object.getClass().getMethod("is" + m.getName().substring(3), + (Class[]) null); + else + gm = object.getClass().getMethod("get" + m.getName().substring(3), + (Class[]) null); + cm.getMethods.put(m.getName(), gm); + cm.setMethods.add(m); + } catch (NoSuchMethodException err) { + } + } + } + cm.builderMethod = mdSalBuilderType.getMethod("build", (Class[]) null); + } + for (Method m : cm.setMethods) { + m.invoke(result, + new Object[] { cm.getMethods.get(m.getName()).invoke(object, (Object[]) null) }); + } + + return cm.builderMethod.invoke(result, (Object[]) null); + } + + // A struccture class to hold the getters & setters of each type to speed + // things up + private static class ConvertionMethods { + private List<Method> setMethods = new ArrayList<Method>(); + private Map<String, Method> getMethods = new HashMap<String, Method>(); + private Method builderMethod = null; + } + + // Convert Domain + public static org.opendaylight.yang.gen.v1.urn.aaa.yang.authn.claims.rev141029.authentication.Domain toMDSALDomain( + Domain domain) { + try { + return (org.opendaylight.yang.gen.v1.urn.aaa.yang.authn.claims.rev141029.authentication.Domain) toMDSALObject( + domain, DomainBuilder.class); + } catch (Exception err) { + LOG.error("Error converting domain to MDSAL object", err); + return null; + } + } + + public static Domain toIDMDomain( + org.opendaylight.yang.gen.v1.urn.aaa.yang.authn.claims.rev141029.authentication.Domain domain) { + try { + return (Domain) fromMDSALObject(domain, Domain.class); + } catch (Exception err) { + LOG.error("Error converting domain from MDSAL to IDM object", err); + return null; + } + } + + // Convert Role + public static org.opendaylight.yang.gen.v1.urn.aaa.yang.authn.claims.rev141029.authentication.Role toMDSALRole( + Role role) { + try { + return (org.opendaylight.yang.gen.v1.urn.aaa.yang.authn.claims.rev141029.authentication.Role) toMDSALObject( + role, RoleBuilder.class); + } catch (Exception err) { + LOG.error("Error converting role to MDSAL object", err); + return null; + } + } + + public static Role toIDMRole( + org.opendaylight.yang.gen.v1.urn.aaa.yang.authn.claims.rev141029.authentication.Role role) { + try { + return (Role) fromMDSALObject(role, Role.class); + } catch (Exception err) { + LOG.error("Error converting role fom MDSAL to IDM object", err); + return null; + } + } + + // Convert User + public static org.opendaylight.yang.gen.v1.urn.aaa.yang.authn.claims.rev141029.authentication.User toMDSALUser( + User user) { + try { + return (org.opendaylight.yang.gen.v1.urn.aaa.yang.authn.claims.rev141029.authentication.User) toMDSALObject( + user, UserBuilder.class); + } catch (Exception err) { + LOG.error("Error converting user to MDSAL object", err); + return null; + } + } + + public static User toIDMUser( + org.opendaylight.yang.gen.v1.urn.aaa.yang.authn.claims.rev141029.authentication.User user) { + try { + return (User) fromMDSALObject(user, User.class); + } catch (Exception err) { + LOG.error("Error converting user from MDSAL to IDM object", err); + return null; + } + } + + // Convert Grant + public static org.opendaylight.yang.gen.v1.urn.aaa.yang.authn.claims.rev141029.authentication.Grant toMDSALGrant( + Grant grant) { + try { + return (org.opendaylight.yang.gen.v1.urn.aaa.yang.authn.claims.rev141029.authentication.Grant) toMDSALObject( + grant, GrantBuilder.class); + } catch (Exception err) { + LOG.error("Error converting grant to MDSAL object", err); + return null; + } + } + + public static Grant toIDMGrant( + org.opendaylight.yang.gen.v1.urn.aaa.yang.authn.claims.rev141029.authentication.Grant grant) { + try { + return (Grant) fromMDSALObject(grant, Grant.class); + } catch (Exception err) { + LOG.error("Error converting grant from MDSAL to IDM object", err); + return null; + } + } +} diff --git a/odl-aaa-moon/aaa/aaa-authn-mdsal-store/aaa-authn-mdsal-store-impl/src/main/java/org/opendaylight/aaa/authn/mdsal/store/IDMStore.java b/odl-aaa-moon/aaa/aaa-authn-mdsal-store/aaa-authn-mdsal-store-impl/src/main/java/org/opendaylight/aaa/authn/mdsal/store/IDMStore.java new file mode 100644 index 00000000..69bc1d52 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn-mdsal-store/aaa-authn-mdsal-store-impl/src/main/java/org/opendaylight/aaa/authn/mdsal/store/IDMStore.java @@ -0,0 +1,182 @@ +/* + * Copyright (c) 2015 Cisco Systems, Inc. and others. 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.authn.mdsal.store; + +import java.util.List; +import org.opendaylight.aaa.api.IDMStoreException; +import org.opendaylight.aaa.api.IDMStoreUtil; +import org.opendaylight.aaa.api.IIDMStore; +import org.opendaylight.aaa.api.model.Domain; +import org.opendaylight.aaa.api.model.Domains; +import org.opendaylight.aaa.api.model.Grant; +import org.opendaylight.aaa.api.model.Grants; +import org.opendaylight.aaa.api.model.Role; +import org.opendaylight.aaa.api.model.Roles; +import org.opendaylight.aaa.api.model.User; +import org.opendaylight.aaa.api.model.Users; + +/** + * @author Sharon Aicler - saichler@cisco.com + * + */ +public class IDMStore implements IIDMStore { + private final IDMMDSALStore mdsalStore; + + public IDMStore(IDMMDSALStore mdsalStore) { + this.mdsalStore = mdsalStore; + } + + @Override + public Domain writeDomain(Domain domain) throws IDMStoreException { + return IDMObject2MDSAL.toIDMDomain(mdsalStore.writeDomain(IDMObject2MDSAL.toMDSALDomain(domain))); + } + + @Override + public Domain readDomain(String domainid) throws IDMStoreException { + return IDMObject2MDSAL.toIDMDomain(mdsalStore.readDomain(domainid)); + } + + @Override + public Domain deleteDomain(String domainid) throws IDMStoreException { + return IDMObject2MDSAL.toIDMDomain(mdsalStore.deleteDomain(domainid)); + } + + @Override + public Domain updateDomain(Domain domain) throws IDMStoreException { + return IDMObject2MDSAL.toIDMDomain(mdsalStore.updateDomain(IDMObject2MDSAL.toMDSALDomain(domain))); + } + + @Override + public Domains getDomains() throws IDMStoreException { + Domains domains = new Domains(); + List<org.opendaylight.yang.gen.v1.urn.aaa.yang.authn.claims.rev141029.authentication.Domain> mdSalDomains = mdsalStore.getAllDomains(); + for (org.opendaylight.yang.gen.v1.urn.aaa.yang.authn.claims.rev141029.authentication.Domain d : mdSalDomains) { + domains.getDomains().add(IDMObject2MDSAL.toIDMDomain(d)); + } + return domains; + } + + @Override + public Role writeRole(Role role) throws IDMStoreException { + return IDMObject2MDSAL.toIDMRole(mdsalStore.writeRole(IDMObject2MDSAL.toMDSALRole(role))); + } + + @Override + public Role readRole(String roleid) throws IDMStoreException { + return IDMObject2MDSAL.toIDMRole(mdsalStore.readRole(roleid)); + } + + @Override + public Role deleteRole(String roleid) throws IDMStoreException { + return IDMObject2MDSAL.toIDMRole(mdsalStore.deleteRole(roleid)); + } + + @Override + public Role updateRole(Role role) throws IDMStoreException { + return IDMObject2MDSAL.toIDMRole(mdsalStore.writeRole(IDMObject2MDSAL.toMDSALRole(role))); + } + + @Override + public User writeUser(User user) throws IDMStoreException { + return IDMObject2MDSAL.toIDMUser(mdsalStore.writeUser(IDMObject2MDSAL.toMDSALUser(user))); + } + + @Override + public User readUser(String userid) throws IDMStoreException { + return IDMObject2MDSAL.toIDMUser(mdsalStore.readUser(userid)); + } + + @Override + public User deleteUser(String userid) throws IDMStoreException { + return IDMObject2MDSAL.toIDMUser(mdsalStore.deleteUser(userid)); + } + + @Override + public User updateUser(User user) throws IDMStoreException { + return IDMObject2MDSAL.toIDMUser(mdsalStore.writeUser(IDMObject2MDSAL.toMDSALUser(user))); + } + + @Override + public Grant writeGrant(Grant grant) throws IDMStoreException { + return IDMObject2MDSAL.toIDMGrant(mdsalStore.writeGrant(IDMObject2MDSAL.toMDSALGrant(grant))); + } + + @Override + public Grant readGrant(String grantid) throws IDMStoreException { + return IDMObject2MDSAL.toIDMGrant(mdsalStore.readGrant(grantid)); + } + + @Override + public Grant deleteGrant(String grantid) throws IDMStoreException { + return IDMObject2MDSAL.toIDMGrant(mdsalStore.readGrant(grantid)); + } + + @Override + public Roles getRoles() throws IDMStoreException { + Roles roles = new Roles(); + List<org.opendaylight.yang.gen.v1.urn.aaa.yang.authn.claims.rev141029.authentication.Role> mdSalRoles = mdsalStore.getAllRoles(); + for (org.opendaylight.yang.gen.v1.urn.aaa.yang.authn.claims.rev141029.authentication.Role r : mdSalRoles) { + roles.getRoles().add(IDMObject2MDSAL.toIDMRole(r)); + } + return roles; + } + + @Override + public Users getUsers() throws IDMStoreException { + Users users = new Users(); + List<org.opendaylight.yang.gen.v1.urn.aaa.yang.authn.claims.rev141029.authentication.User> mdSalUsers = mdsalStore.getAllUsers(); + for (org.opendaylight.yang.gen.v1.urn.aaa.yang.authn.claims.rev141029.authentication.User u : mdSalUsers) { + users.getUsers().add(IDMObject2MDSAL.toIDMUser(u)); + } + return users; + } + + @Override + public Users getUsers(String username, String domain) throws IDMStoreException { + Users users = new Users(); + List<org.opendaylight.yang.gen.v1.urn.aaa.yang.authn.claims.rev141029.authentication.User> mdSalUsers = mdsalStore.getAllUsers(); + for (org.opendaylight.yang.gen.v1.urn.aaa.yang.authn.claims.rev141029.authentication.User u : mdSalUsers) { + if (u.getDomainid().equals(domain) && u.getName().equals(username)) { + users.getUsers().add(IDMObject2MDSAL.toIDMUser(u)); + } + } + return users; + } + + @Override + public Grants getGrants(String domainid, String userid) throws IDMStoreException { + Grants grants = new Grants(); + List<org.opendaylight.yang.gen.v1.urn.aaa.yang.authn.claims.rev141029.authentication.Grant> mdSalGrants = mdsalStore.getAllGrants(); + String currentGrantUserId, currentGrantDomainId; + for (org.opendaylight.yang.gen.v1.urn.aaa.yang.authn.claims.rev141029.authentication.Grant g : mdSalGrants) { + currentGrantUserId = g.getUserid(); + currentGrantDomainId = g.getDomainid(); + if (currentGrantUserId.equals(userid) && currentGrantDomainId.equals(domainid)) { + grants.getGrants().add(IDMObject2MDSAL.toIDMGrant(g)); + } + } + return grants; + } + + @Override + public Grants getGrants(String userid) throws IDMStoreException { + Grants grants = new Grants(); + List<org.opendaylight.yang.gen.v1.urn.aaa.yang.authn.claims.rev141029.authentication.Grant> mdSalGrants = mdsalStore.getAllGrants(); + for (org.opendaylight.yang.gen.v1.urn.aaa.yang.authn.claims.rev141029.authentication.Grant g : mdSalGrants) { + if (g.getUserid().equals(userid)) { + grants.getGrants().add(IDMObject2MDSAL.toIDMGrant(g)); + } + } + return grants; + } + + @Override + public Grant readGrant(String domainid, String userid, String roleid) throws IDMStoreException { + return readGrant(IDMStoreUtil.createGrantid(userid, domainid, roleid)); + } +} diff --git a/odl-aaa-moon/aaa/aaa-authn-mdsal-store/aaa-authn-mdsal-store-impl/src/main/java/org/opendaylight/aaa/authn/mdsal/store/util/AuthNStoreUtil.java b/odl-aaa-moon/aaa/aaa-authn-mdsal-store/aaa-authn-mdsal-store-impl/src/main/java/org/opendaylight/aaa/authn/mdsal/store/util/AuthNStoreUtil.java new file mode 100644 index 00000000..6ef58109 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn-mdsal-store/aaa-authn-mdsal-store-impl/src/main/java/org/opendaylight/aaa/authn/mdsal/store/util/AuthNStoreUtil.java @@ -0,0 +1,140 @@ +/* + * Copyright (c) 2015 Cisco Systems, Inc. and others. 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.authn.mdsal.store.util; + +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import org.opendaylight.aaa.AuthenticationBuilder; +import org.opendaylight.aaa.api.Authentication; +import org.opendaylight.aaa.api.Claim; +import org.opendaylight.yang.gen.v1.urn.aaa.yang.authn.claims.rev141029.TokenCacheTimes; +import org.opendaylight.yang.gen.v1.urn.aaa.yang.authn.claims.rev141029.Tokencache; +import org.opendaylight.yang.gen.v1.urn.aaa.yang.authn.claims.rev141029.token_cache_times.TokenList; +import org.opendaylight.yang.gen.v1.urn.aaa.yang.authn.claims.rev141029.token_cache_times.TokenListBuilder; +import org.opendaylight.yang.gen.v1.urn.aaa.yang.authn.claims.rev141029.token_cache_times.TokenListKey; +import org.opendaylight.yang.gen.v1.urn.aaa.yang.authn.claims.rev141029.token_cache_times.token_list.UserTokens; +import org.opendaylight.yang.gen.v1.urn.aaa.yang.authn.claims.rev141029.token_cache_times.token_list.UserTokensBuilder; +import org.opendaylight.yang.gen.v1.urn.aaa.yang.authn.claims.rev141029.token_cache_times.token_list.UserTokensKey; +import org.opendaylight.yang.gen.v1.urn.aaa.yang.authn.claims.rev141029.tokencache.Claims; +import org.opendaylight.yang.gen.v1.urn.aaa.yang.authn.claims.rev141029.tokencache.ClaimsBuilder; +import org.opendaylight.yang.gen.v1.urn.aaa.yang.authn.claims.rev141029.tokencache.ClaimsKey; +import org.opendaylight.yangtools.yang.binding.InstanceIdentifier; + +public class AuthNStoreUtil { + + public static InstanceIdentifier<Claims> createInstIdentifierForTokencache(String token) { + if (token == null || token.length() == 0) + return null; + + InstanceIdentifier<Claims> claims_iid = InstanceIdentifier.builder(Tokencache.class) + .child(Claims.class, + new ClaimsKey(token)) + .build(); + return claims_iid; + } + + public static InstanceIdentifier<UserTokens> createInstIdentifierUserTokens(String userId, + String token) { + if (userId == null || userId.length() == 0 || token == null || token.length() == 0) + return null; + + InstanceIdentifier<UserTokens> userTokens_iid = InstanceIdentifier.builder( + TokenCacheTimes.class) + .child(TokenList.class, + new TokenListKey( + userId)) + .child(UserTokens.class, + new UserTokensKey( + token)) + .build(); + return userTokens_iid; + } + + public static Claims createClaimsRecord(String token, Authentication auth) { + if (auth == null || token == null || token.length() == 0) + return null; + + ClaimsKey claimsKey = new ClaimsKey(token); + ClaimsBuilder claimsBuilder = new ClaimsBuilder(); + claimsBuilder.setClientId(auth.clientId()); + claimsBuilder.setDomain(auth.domain()); + claimsBuilder.setKey(claimsKey); + List<String> roles = new ArrayList<String>(); + roles.addAll(auth.roles()); + claimsBuilder.setRoles(roles); + claimsBuilder.setToken(token); + claimsBuilder.setUser(auth.user()); + claimsBuilder.setUserId(auth.userId()); + return claimsBuilder.build(); + } + + public static UserTokens createUserTokens(String token, Long expiration) { + if (expiration == null || token == null || token.length() == 0) + return null; + + UserTokensBuilder userTokensBuilder = new UserTokensBuilder(); + userTokensBuilder.setTokenid(token); + BigInteger timestamp = BigInteger.valueOf(System.currentTimeMillis()); + userTokensBuilder.setTimestamp(timestamp); + userTokensBuilder.setExpiration(expiration); + userTokensBuilder.setKey(new UserTokensKey(token)); + return userTokensBuilder.build(); + } + + public static TokenList createTokenList(UserTokens tokens, String userId) { + if (tokens == null || userId == null || userId.length() == 0) + return null; + + TokenListBuilder tokenListBuilder = new TokenListBuilder(); + tokenListBuilder.setUserId(userId); + tokenListBuilder.setKey(new TokenListKey(userId)); + List<UserTokens> userTokens = new ArrayList<UserTokens>(); + userTokens.add(tokens); + tokenListBuilder.setUserTokens(userTokens); + return tokenListBuilder.build(); + } + + public static Authentication convertClaimToAuthentication(final Claims claims, Long expiration) { + if (claims == null) + return null; + + Claim claim = new Claim() { + @Override + public String clientId() { + return claims.getClientId(); + } + + @Override + public String userId() { + return claims.getUserId(); + } + + @Override + public String user() { + return claims.getUser(); + } + + @Override + public String domain() { + return claims.getDomain(); + } + + @Override + public Set<String> roles() { + return new HashSet<>(claims.getRoles()); + } + }; + AuthenticationBuilder authBuilder = new AuthenticationBuilder(claim); + authBuilder.setExpiration(expiration); + return authBuilder.build(); + } +} diff --git a/odl-aaa-moon/aaa/aaa-authn-mdsal-store/aaa-authn-mdsal-store-impl/src/main/java/org/opendaylight/yang/gen/v1/config/aaa/authn/mdsal/store/rev141031/AuthNStoreModule.java b/odl-aaa-moon/aaa/aaa-authn-mdsal-store/aaa-authn-mdsal-store-impl/src/main/java/org/opendaylight/yang/gen/v1/config/aaa/authn/mdsal/store/rev141031/AuthNStoreModule.java new file mode 100644 index 00000000..0631170e --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn-mdsal-store/aaa-authn-mdsal-store-impl/src/main/java/org/opendaylight/yang/gen/v1/config/aaa/authn/mdsal/store/rev141031/AuthNStoreModule.java @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2015 Cisco Systems, Inc. and others. 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.yang.gen.v1.config.aaa.authn.mdsal.store.rev141031; + +import org.opendaylight.aaa.api.IIDMStore; +import org.opendaylight.aaa.api.TokenStore; +import org.opendaylight.aaa.authn.mdsal.store.AuthNStore; +import org.opendaylight.aaa.authn.mdsal.store.IDMMDSALStore; +import org.opendaylight.aaa.authn.mdsal.store.IDMStore; +import org.opendaylight.controller.md.sal.binding.api.DataBroker; +import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceRegistration; + +public class AuthNStoreModule + extends + org.opendaylight.yang.gen.v1.config.aaa.authn.mdsal.store.rev141031.AbstractAuthNStoreModule { + private BundleContext bundleContext; + + public AuthNStoreModule(org.opendaylight.controller.config.api.ModuleIdentifier identifier, + org.opendaylight.controller.config.api.DependencyResolver dependencyResolver) { + super(identifier, dependencyResolver); + } + + public AuthNStoreModule( + org.opendaylight.controller.config.api.ModuleIdentifier identifier, + org.opendaylight.controller.config.api.DependencyResolver dependencyResolver, + org.opendaylight.yang.gen.v1.config.aaa.authn.mdsal.store.rev141031.AuthNStoreModule oldModule, + java.lang.AutoCloseable oldInstance) { + super(identifier, dependencyResolver, oldModule, oldInstance); + } + + @Override + public void customValidation() { + // add custom validation form module attributes here. + } + + @Override + public java.lang.AutoCloseable createInstance() { + + DataBroker dataBrokerService = getDataBrokerDependency(); + final AuthNStore authNStore = new AuthNStore(dataBrokerService, getPassword()); + final IDMMDSALStore mdsalStore = new IDMMDSALStore(dataBrokerService); + final IDMStore idmStore = new IDMStore(mdsalStore); + + authNStore.setTimeToLive(getTimeToLive()); + + // Register the MD-SAL Token store with OSGI + final ServiceRegistration<?> serviceRegistration = bundleContext.registerService( + TokenStore.class.getName(), authNStore, null); + final ServiceRegistration<?> idmServiceRegistration = bundleContext.registerService( + IIDMStore.class.getName(), idmStore, null); + final class AutoCloseableStore implements AutoCloseable { + + @Override + public void close() throws Exception { + serviceRegistration.unregister(); + idmServiceRegistration.unregister(); + authNStore.close(); + } + } + + return new AutoCloseableStore(); + + // return authNStore; + + // throw new java.lang.UnsupportedOperationException(); + } + + /** + * @param bundleContext + */ + public void setBundleContext(BundleContext bundleContext) { + this.bundleContext = bundleContext; + } + + /** + * @return the bundleContext + */ + public BundleContext getBundleContext() { + return bundleContext; + } + +} diff --git a/odl-aaa-moon/aaa/aaa-authn-mdsal-store/aaa-authn-mdsal-store-impl/src/main/java/org/opendaylight/yang/gen/v1/config/aaa/authn/mdsal/store/rev141031/AuthNStoreModuleFactory.java b/odl-aaa-moon/aaa/aaa-authn-mdsal-store/aaa-authn-mdsal-store-impl/src/main/java/org/opendaylight/yang/gen/v1/config/aaa/authn/mdsal/store/rev141031/AuthNStoreModuleFactory.java new file mode 100644 index 00000000..b1e278fa --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn-mdsal-store/aaa-authn-mdsal-store-impl/src/main/java/org/opendaylight/yang/gen/v1/config/aaa/authn/mdsal/store/rev141031/AuthNStoreModuleFactory.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2015 Cisco Systems, Inc. and others. 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 + * + */ + +/* + * Generated file + * + * Generated from: yang module name: aaa-authn-mdsal-store-cfg yang module local name: aaa-authn-mdsal-store + * Generated by: org.opendaylight.controller.config.yangjmxgenerator.plugin.JMXGenerator + * Generated at: Thu Mar 19 18:06:18 CET 2015 + * + * Do not modify this file unless it is present under src/main directory + */ +package org.opendaylight.yang.gen.v1.config.aaa.authn.mdsal.store.rev141031; + +import org.opendaylight.controller.config.api.DependencyResolver; +import org.osgi.framework.BundleContext; + +public class AuthNStoreModuleFactory + extends + org.opendaylight.yang.gen.v1.config.aaa.authn.mdsal.store.rev141031.AbstractAuthNStoreModuleFactory { + + @Override + public AuthNStoreModule instantiateModule(String instanceName, + DependencyResolver dependencyResolver, BundleContext bundleContext) { + AuthNStoreModule module = super.instantiateModule(instanceName, dependencyResolver, + bundleContext); + module.setBundleContext(bundleContext); + return module; + } + + @Override + public AuthNStoreModule instantiateModule(String instanceName, + DependencyResolver dependencyResolver, AuthNStoreModule oldModule, + AutoCloseable oldInstance, BundleContext bundleContext) { + AuthNStoreModule module = super.instantiateModule(instanceName, dependencyResolver, + oldModule, oldInstance, bundleContext); + module.setBundleContext(bundleContext); + return module; + } +} diff --git a/odl-aaa-moon/aaa/aaa-authn-mdsal-store/aaa-authn-mdsal-store-impl/src/main/yang/aaa-authn-mdsal-store-cfg.yang b/odl-aaa-moon/aaa/aaa-authn-mdsal-store/aaa-authn-mdsal-store-impl/src/main/yang/aaa-authn-mdsal-store-cfg.yang new file mode 100644 index 00000000..eac344b8 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn-mdsal-store/aaa-authn-mdsal-store-impl/src/main/yang/aaa-authn-mdsal-store-cfg.yang @@ -0,0 +1,77 @@ +module aaa-authn-mdsal-store-cfg { + + yang-version 1; + namespace "config:aaa:authn:mdsal:store"; + prefix "aaa-authn-store-cfg"; + + import config { prefix config; revision-date 2013-04-05; } + import rpc-context { prefix rpcx; revision-date 2013-06-17; } + import opendaylight-md-sal-binding { prefix mdsal; revision-date 2013-10-28; } + import opendaylight-md-sal-dom {prefix dom;} + + + description + "This module contains the base YANG definitions for + AuthN MD-SAL backed data cache implementation."; + + revision "2014-10-31" { + description + "Initial revision."; + } + + identity token-store-service{ + base config:service-type; + config:java-class "org.opendaylight.aaa.api.TokenStore"; + } + + + // This is the definition of the service implementation as a module identity. + identity aaa-authn-mdsal-store { + base config:module-type; + // Specifies the prefix for generated java classes. + config:java-name-prefix AuthNStore; + config:provided-service token-store-service; + } + + // Augments the 'configuration' choice node under modules/module. + + augment "/config:modules/config:module/config:configuration" { + case aaa-authn-mdsal-store { + when "/config:modules/config:module/config:type = 'aaa-authn-mdsal-store'"; + + //Defines reference to the Bundle context and MD-SAL data broker + container dom-broker { + uses config:service-ref { + refine type { + mandatory true; + config:required-identity dom:dom-broker-osgi-registry; + } + } + } + container data-broker { + uses config:service-ref { + refine type { + mandatory true; + config:required-identity mdsal:binding-async-data-broker; + + } + } + } + + leaf timeToLive { + description "Time to live for tokens. When set to 0 = never expire"; + type uint64; + default 360000; + } + leaf timeToWait { + description "Time to wait for future from data store. 10 by default = never expire"; + type uint16; + default 10; + } + leaf password { + description "Encryption password for the Store"; + type string; + } + } + } +} diff --git a/odl-aaa-moon/aaa/aaa-authn-mdsal-store/aaa-authn-mdsal-store-impl/src/test/java/org/opendaylight/aaa/authn/mdsal/store/DataBrokerReadMocker.java b/odl-aaa-moon/aaa/aaa-authn-mdsal-store/aaa-authn-mdsal-store-impl/src/test/java/org/opendaylight/aaa/authn/mdsal/store/DataBrokerReadMocker.java new file mode 100644 index 00000000..f821cf16 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn-mdsal-store/aaa-authn-mdsal-store-impl/src/test/java/org/opendaylight/aaa/authn/mdsal/store/DataBrokerReadMocker.java @@ -0,0 +1,112 @@ +/* + * Copyright (c) 2016 Cisco Systems, Inc. and others. 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.authn.mdsal.store; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class DataBrokerReadMocker implements InvocationHandler { + private Map<Method, List<StubContainer>> stubs = new HashMap<Method, List<StubContainer>>(); + private Class<?> mokingClass = null; + + @Override + public Object invoke(Object arg0, Method arg1, Object[] arg2) throws Throwable { + List<StubContainer> stList = stubs.get(arg1); + if (stList != null) { + for (StubContainer sc : stList) { + if (sc.fitGeneric(arg2)) { + return sc.returnObject; + } + } + } + return null; + } + + public DataBrokerReadMocker(Class<?> cls) { + this.mokingClass = cls; + } + + public static Object addMock(Class<?> cls) { + return Proxy.newProxyInstance(cls.getClassLoader(), new Class[] { cls }, + new DataBrokerReadMocker(cls)); + } + + public static DataBrokerReadMocker getMocker(Object o) { + return (DataBrokerReadMocker) Proxy.getInvocationHandler(o); + } + + public static Method findMethod(Class<?> cls, String name, Object args[]) { + Method methods[] = cls.getMethods(); + for (Method m : methods) { + if (m.getName().equals(name)) { + if ((m.getParameterTypes() == null || m.getParameterTypes().length == 0) + && args == null) { + return m; + } + boolean match = true; + for (int i = 0; i < m.getParameterTypes().length; i++) { + if (!m.getParameterTypes()[i].isAssignableFrom(args[i].getClass())) { + match = false; + } + } + if (match) + return m; + } + } + return null; + } + + public void addWhen(String methodName, Object[] args, Object returnThis) + throws NoSuchMethodException, SecurityException { + Method m = findMethod(this.mokingClass, methodName, args); + if (m == null) + throw new IllegalArgumentException("Unable to find method"); + StubContainer sc = new StubContainer(args, returnThis); + List<StubContainer> lst = stubs.get(m); + if (lst == null) { + lst = new ArrayList<>(); + } + lst.add(sc); + stubs.put(m, lst); + } + + private class StubContainer { + private Class<?>[] parameters = null; + private Class<?>[] generics = null; + private Object args[] = null; + private Object returnObject; + + public StubContainer(Object[] _args, Object ret) { + this.args = _args; + this.returnObject = ret; + } + + public boolean fitGeneric(Object _args[]) { + if (args == null && _args != null) + return false; + if (args != null && _args == null) + return false; + if (args == null && _args == null) + return true; + if (args.length != _args.length) + return false; + for (int i = 0; i < args.length; i++) { + if (!args[i].equals(_args[i])) { + return false; + } + } + return true; + } + } +}
\ No newline at end of file diff --git a/odl-aaa-moon/aaa/aaa-authn-mdsal-store/aaa-authn-mdsal-store-impl/src/test/java/org/opendaylight/aaa/authn/mdsal/store/DataEncrypterTest.java b/odl-aaa-moon/aaa/aaa-authn-mdsal-store/aaa-authn-mdsal-store-impl/src/test/java/org/opendaylight/aaa/authn/mdsal/store/DataEncrypterTest.java new file mode 100644 index 00000000..eec69bc0 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn-mdsal-store/aaa-authn-mdsal-store-impl/src/test/java/org/opendaylight/aaa/authn/mdsal/store/DataEncrypterTest.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2016 Cisco Systems, Inc. and others. 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.authn.mdsal.store; + +import static org.junit.Assert.assertEquals; + +import javax.xml.bind.DatatypeConverter; +import org.junit.Test; + +public class DataEncrypterTest { + + @Test + public void testEncrypt() { + DataEncrypter dataEncry = new DataEncrypter("foo_key_test"); + String token = "foo_token_test"; + String eToken = dataEncry.encrypt(token); + // check for decryption result + String returnToken = dataEncry.decrypt(eToken); + String tokenBase64 = DatatypeConverter.printBase64Binary(token.getBytes()); + assertEquals(tokenBase64, returnToken); + } + + @Test + public void testDecrypt() { + DataEncrypter dataEncry = new DataEncrypter("foo_key_test"); + String eToken = "foo_etoken_test"; + assertEquals(dataEncry.decrypt(""), null); + // check for encryption Tag + assertEquals(eToken, dataEncry.decrypt(eToken)); + } + +} diff --git a/odl-aaa-moon/aaa/aaa-authn-mdsal-store/aaa-authn-mdsal-store-impl/src/test/java/org/opendaylight/aaa/authn/mdsal/store/IDMStoreTest.java b/odl-aaa-moon/aaa/aaa-authn-mdsal-store/aaa-authn-mdsal-store-impl/src/test/java/org/opendaylight/aaa/authn/mdsal/store/IDMStoreTest.java new file mode 100644 index 00000000..f376dd5f --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn-mdsal-store/aaa-authn-mdsal-store-impl/src/test/java/org/opendaylight/aaa/authn/mdsal/store/IDMStoreTest.java @@ -0,0 +1,175 @@ +/* + * Copyright (c) 2016 Cisco Systems, Inc. and others. 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.authn.mdsal.store; + +import org.junit.Assert; +import org.junit.Test; +import org.opendaylight.aaa.api.IDMStoreUtil; +import org.opendaylight.aaa.api.SHA256Calculator; +import org.opendaylight.yang.gen.v1.urn.aaa.yang.authn.claims.rev141029.authentication.Domain; +import org.opendaylight.yang.gen.v1.urn.aaa.yang.authn.claims.rev141029.authentication.Grant; +import org.opendaylight.yang.gen.v1.urn.aaa.yang.authn.claims.rev141029.authentication.Role; +import org.opendaylight.yang.gen.v1.urn.aaa.yang.authn.claims.rev141029.authentication.User; + +public class IDMStoreTest { + + @Test + public void testWriteDomain() throws Exception { + IDMStoreTestUtil util = new IDMStoreTestUtil(); + IDMMDSALStore testedObject = new IDMMDSALStore(util.dataBroker); + util.addMokitoFordomain(); + Domain domain = testedObject.writeDomain(util.domain); + Assert.assertNotNull(domain); + Assert.assertEquals(domain.getDomainid(), util.domain.getName()); + } + + @Test + public void testReadDomain() throws Exception { + IDMStoreTestUtil util = new IDMStoreTestUtil(); + IDMMDSALStore testedObject = new IDMMDSALStore(util.dataBroker); + util.addMokitoFordomain(); + Domain domain = testedObject.readDomain(util.domain.getDomainid()); + Assert.assertNotNull(domain); + Assert.assertEquals(domain, util.domain); + } + + @Test + public void testDeleteDomain() throws Exception { + IDMStoreTestUtil util = new IDMStoreTestUtil(); + IDMMDSALStore testedObject = new IDMMDSALStore(util.dataBroker); + util.addMokitoFordomain(); + Domain domain = testedObject.deleteDomain(util.domain.getDomainid()); + Assert.assertEquals(domain, util.domain); + } + + @Test + public void testUpdateDomain() throws Exception { + IDMStoreTestUtil util = new IDMStoreTestUtil(); + IDMMDSALStore testedObject = new IDMMDSALStore(util.dataBroker); + util.addMokitoFordomain(); + Domain domain = testedObject.updateDomain(util.domain); + Assert.assertEquals(domain, util.domain); + } + + @Test + public void testWriteRole() throws Exception { + IDMStoreTestUtil util = new IDMStoreTestUtil(); + IDMMDSALStore testedObject = new IDMMDSALStore(util.dataBroker); + util.addMokitoForrole(); + util.addMokitoFordomain(); + Role role = testedObject.writeRole(util.role); + Assert.assertNotNull(role); + Assert.assertEquals(role.getRoleid(), + IDMStoreUtil.createRoleid(role.getName(), role.getDomainid())); + } + + @Test + public void testReadRole() throws Exception { + IDMStoreTestUtil util = new IDMStoreTestUtil(); + IDMMDSALStore testedObject = new IDMMDSALStore(util.dataBroker); + util.addMokitoForrole(); + Role role = testedObject.readRole(util.role.getRoleid()); + Assert.assertNotNull(role); + Assert.assertEquals(role, util.role); + } + + @Test + public void testDeleteRole() throws Exception { + IDMStoreTestUtil util = new IDMStoreTestUtil(); + IDMMDSALStore testedObject = new IDMMDSALStore(util.dataBroker); + util.addMokitoForrole(); + Role role = testedObject.deleteRole(util.role.getRoleid()); + Assert.assertNotNull(role); + Assert.assertEquals(role, util.role); + } + + @Test + public void testUpdateRole() throws Exception { + IDMStoreTestUtil util = new IDMStoreTestUtil(); + IDMMDSALStore testedObject = new IDMMDSALStore(util.dataBroker); + util.addMokitoForrole(); + Role role = testedObject.updateRole(util.role); + Assert.assertNotNull(role); + Assert.assertEquals(role, util.role); + } + + @Test + public void testWriteUser() throws Exception { + IDMStoreTestUtil util = new IDMStoreTestUtil(); + IDMMDSALStore testedObject = new IDMMDSALStore(util.dataBroker); + util.addMokitoForuser(); + User user = testedObject.writeUser(util.user); + Assert.assertNotNull(user); + Assert.assertEquals(user.getUserid(), + IDMStoreUtil.createUserid(user.getName(), util.user.getDomainid())); + } + + @Test + public void testReadUser() throws Exception { + IDMStoreTestUtil util = new IDMStoreTestUtil(); + IDMMDSALStore testedObject = new IDMMDSALStore(util.dataBroker); + util.addMokitoForuser(); + User user = testedObject.readUser(util.user.getUserid()); + Assert.assertNotNull(user); + Assert.assertEquals(user, util.user); + } + + @Test + public void testDeleteUser() throws Exception { + IDMStoreTestUtil util = new IDMStoreTestUtil(); + IDMMDSALStore testedObject = new IDMMDSALStore(util.dataBroker); + util.addMokitoForuser(); + User user = testedObject.deleteUser(util.user.getUserid()); + Assert.assertNotNull(user); + Assert.assertEquals(user, util.user); + } + + @Test + public void testUpdateUser() throws Exception { + IDMStoreTestUtil util = new IDMStoreTestUtil(); + IDMMDSALStore testedObject = new IDMMDSALStore(util.dataBroker); + util.addMokitoForuser(); + User user = testedObject.updateUser(util.user); + Assert.assertNotNull(user); + Assert.assertEquals(user.getPassword(), + SHA256Calculator.getSHA256(util.user.getPassword(), util.user.getSalt())); + } + + @Test + public void testWriteGrant() throws Exception { + IDMStoreTestUtil util = new IDMStoreTestUtil(); + IDMMDSALStore testedObject = new IDMMDSALStore(util.dataBroker); + util.addMokitoFordomain(); + util.addMokitoForrole(); + util.addMokitoForuser(); + util.addMokitoForgrant(); + Grant grant = testedObject.writeGrant(util.grant); + Assert.assertNotNull(grant); + } + + @Test + public void testReadGrant() throws Exception { + IDMStoreTestUtil util = new IDMStoreTestUtil(); + IDMMDSALStore testedObject = new IDMMDSALStore(util.dataBroker); + util.addMokitoForgrant(); + Grant grant = testedObject.readGrant(util.grant.getGrantid()); + Assert.assertNotNull(grant); + Assert.assertEquals(grant, util.grant); + } + + @Test + public void testDeleteGrant() throws Exception { + IDMStoreTestUtil util = new IDMStoreTestUtil(); + IDMMDSALStore testedObject = new IDMMDSALStore(util.dataBroker); + util.addMokitoForgrant(); + Grant grant = testedObject.deleteGrant(util.grant.getGrantid()); + Assert.assertNotNull(grant); + Assert.assertEquals(grant, util.grant); + } +} diff --git a/odl-aaa-moon/aaa/aaa-authn-mdsal-store/aaa-authn-mdsal-store-impl/src/test/java/org/opendaylight/aaa/authn/mdsal/store/IDMStoreTestUtil.java b/odl-aaa-moon/aaa/aaa-authn-mdsal-store/aaa-authn-mdsal-store-impl/src/test/java/org/opendaylight/aaa/authn/mdsal/store/IDMStoreTestUtil.java new file mode 100644 index 00000000..39eeadb4 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn-mdsal-store/aaa-authn-mdsal-store-impl/src/test/java/org/opendaylight/aaa/authn/mdsal/store/IDMStoreTestUtil.java @@ -0,0 +1,181 @@ +/* + * Copyright (c) 2016 Cisco Systems, Inc. and others. 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.authn.mdsal.store; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import com.google.common.base.Optional; +import com.google.common.util.concurrent.CheckedFuture; +import java.util.concurrent.ExecutionException; +import org.opendaylight.aaa.api.IDMStoreUtil; +import org.opendaylight.controller.md.sal.binding.api.DataBroker; +import org.opendaylight.controller.md.sal.binding.api.ReadOnlyTransaction; +import org.opendaylight.controller.md.sal.binding.api.WriteTransaction; +import org.opendaylight.controller.md.sal.common.api.data.LogicalDatastoreType; +import org.opendaylight.controller.md.sal.common.api.data.ReadFailedException; +import org.opendaylight.yang.gen.v1.urn.aaa.yang.authn.claims.rev141029.Authentication; +import org.opendaylight.yang.gen.v1.urn.aaa.yang.authn.claims.rev141029.authentication.Domain; +import org.opendaylight.yang.gen.v1.urn.aaa.yang.authn.claims.rev141029.authentication.DomainBuilder; +import org.opendaylight.yang.gen.v1.urn.aaa.yang.authn.claims.rev141029.authentication.DomainKey; +import org.opendaylight.yang.gen.v1.urn.aaa.yang.authn.claims.rev141029.authentication.Grant; +import org.opendaylight.yang.gen.v1.urn.aaa.yang.authn.claims.rev141029.authentication.GrantBuilder; +import org.opendaylight.yang.gen.v1.urn.aaa.yang.authn.claims.rev141029.authentication.GrantKey; +import org.opendaylight.yang.gen.v1.urn.aaa.yang.authn.claims.rev141029.authentication.Role; +import org.opendaylight.yang.gen.v1.urn.aaa.yang.authn.claims.rev141029.authentication.RoleBuilder; +import org.opendaylight.yang.gen.v1.urn.aaa.yang.authn.claims.rev141029.authentication.RoleKey; +import org.opendaylight.yang.gen.v1.urn.aaa.yang.authn.claims.rev141029.authentication.User; +import org.opendaylight.yang.gen.v1.urn.aaa.yang.authn.claims.rev141029.authentication.UserBuilder; +import org.opendaylight.yang.gen.v1.urn.aaa.yang.authn.claims.rev141029.authentication.UserKey; +import org.opendaylight.yangtools.yang.binding.InstanceIdentifier; + +public class IDMStoreTestUtil { + /* DataBroker mocked with Mokito */ + protected static DataBroker dataBroker = mock(DataBroker.class); + protected static WriteTransaction wrt = mock(WriteTransaction.class); + protected static ReadOnlyTransaction rot = null; + + static { + rot = (ReadOnlyTransaction) DataBrokerReadMocker.addMock(ReadOnlyTransaction.class); + when(dataBroker.newReadOnlyTransaction()).thenReturn(rot); + when(dataBroker.newWriteOnlyTransaction()).thenReturn(wrt); + } + + /* Domain Data Object Instance */ + public Domain domain = createdomain(); + + /* Domain create Method */ + public Domain createdomain() { + /* Start of Domain builder */ + DomainBuilder domainbuilder = new DomainBuilder(); + domainbuilder.setName("SETNAME"); + domainbuilder.setDomainid("SETNAME"); + domainbuilder.setKey(new DomainKey("SETNAME")); + domainbuilder.setDescription("SETDESCRIPTION"); + domainbuilder.setEnabled(true); + /* End of Domain builder */ + return domainbuilder.build(); + } + + /* Role Data Object Instance */ + public Role role = createrole(); + + /* Role create Method */ + public Role createrole() { + /* Start of Role builder */ + RoleBuilder rolebuilder = new RoleBuilder(); + rolebuilder.setRoleid("SETNAME@SETNAME"); + rolebuilder.setName("SETNAME"); + rolebuilder.setKey(new RoleKey(rolebuilder.getRoleid())); + rolebuilder.setDomainid(createdomain().getDomainid()); + rolebuilder.setDescription("SETDESCRIPTION"); + /* End of Role builder */ + return rolebuilder.build(); + } + + /* User Data Object Instance */ + public User user = createuser(); + + /* User create Method */ + public User createuser() { + /* Start of User builder */ + UserBuilder userbuilder = new UserBuilder(); + userbuilder.setUserid("SETNAME@SETNAME"); + userbuilder.setName("SETNAME"); + userbuilder.setKey(new UserKey(userbuilder.getUserid())); + userbuilder.setDomainid(createdomain().getDomainid()); + userbuilder.setEmail("SETEMAIL"); + userbuilder.setPassword("SETPASSWORD"); + userbuilder.setSalt("SETSALT"); + userbuilder.setEnabled(true); + userbuilder.setDescription("SETDESCRIPTION"); + /* End of User builder */ + return userbuilder.build(); + } + + /* Grant Data Object Instance */ + public Grant grant = creategrant(); + + /* Grant create Method */ + public Grant creategrant() { + /* Start of Grant builder */ + GrantBuilder grantbuilder = new GrantBuilder(); + grantbuilder.setDomainid(createdomain().getDomainid()); + grantbuilder.setRoleid(createrole().getRoleid()); + grantbuilder.setUserid(createuser().getUserid()); + grantbuilder.setGrantid(IDMStoreUtil.createGrantid(grantbuilder.getUserid(), + grantbuilder.getDomainid(), grantbuilder.getRoleid())); + grantbuilder.setKey(new GrantKey(grantbuilder.getGrantid())); + /* End of Grant builder */ + return grantbuilder.build(); + } + + /* InstanceIdentifier for Grant instance grant */ + public InstanceIdentifier<Grant> grantID = InstanceIdentifier.create(Authentication.class) + .child(Grant.class, + creategrant().getKey()); + + /* Mokito DataBroker method for grant Data Object */ + public void addMokitoForgrant() throws NoSuchMethodException, SecurityException, InterruptedException, ExecutionException { + CheckedFuture<Optional<Grant>, ReadFailedException> read = mock(CheckedFuture.class); + DataBrokerReadMocker.getMocker(rot).addWhen("read", + new Object[] { LogicalDatastoreType.CONFIGURATION, grantID }, read); + Optional<Grant> optional = mock(Optional.class); + when(read.get()).thenReturn(optional); + when(optional.get()).thenReturn(grant); + when(optional.isPresent()).thenReturn(true); + } + + /* InstanceIdentifier for Domain instance domain */ + public InstanceIdentifier<Domain> domainID = InstanceIdentifier.create(Authentication.class) + .child(Domain.class, + new DomainKey( + new String( + "SETNAME"))); + + /* Mokito DataBroker method for domain Data Object */ + public void addMokitoFordomain() throws NoSuchMethodException, SecurityException, InterruptedException, ExecutionException { + CheckedFuture<Optional<Domain>, ReadFailedException> read = mock(CheckedFuture.class); + DataBrokerReadMocker.getMocker(rot).addWhen("read", + new Object[] { LogicalDatastoreType.CONFIGURATION, domainID }, read); + Optional<Domain> optional = mock(Optional.class); + when(read.get()).thenReturn(optional); + when(optional.get()).thenReturn(domain); + when(optional.isPresent()).thenReturn(true); + } + + /* InstanceIdentifier for Role instance role */ + public InstanceIdentifier<Role> roleID = InstanceIdentifier.create(Authentication.class).child( + Role.class, createrole().getKey()); + + /* Mokito DataBroker method for role Data Object */ + public void addMokitoForrole() throws NoSuchMethodException, SecurityException, InterruptedException, ExecutionException { + CheckedFuture<Optional<Role>, ReadFailedException> read = mock(CheckedFuture.class); + DataBrokerReadMocker.getMocker(rot).addWhen("read", + new Object[] { LogicalDatastoreType.CONFIGURATION, roleID }, read); + Optional<Role> optional = mock(Optional.class); + when(read.get()).thenReturn(optional); + when(optional.get()).thenReturn(role); + when(optional.isPresent()).thenReturn(true); + } + + /* InstanceIdentifier for User instance user */ + public InstanceIdentifier<User> userID = InstanceIdentifier.create(Authentication.class).child( + User.class, createuser().getKey()); + + /* Mokito DataBroker method for user Data Object */ + public void addMokitoForuser() throws NoSuchMethodException, SecurityException, InterruptedException, ExecutionException { + CheckedFuture<Optional<User>, ReadFailedException> read = mock(CheckedFuture.class); + DataBrokerReadMocker.getMocker(rot).addWhen("read", + new Object[] { LogicalDatastoreType.CONFIGURATION, userID }, read); + Optional<User> optional = mock(Optional.class); + when(read.get()).thenReturn(optional); + when(optional.get()).thenReturn(user); + when(optional.isPresent()).thenReturn(true); + } +} diff --git a/odl-aaa-moon/aaa/aaa-authn-mdsal-store/aaa-authn-mdsal-store-impl/src/test/java/org/opendaylight/aaa/authn/mdsal/store/MDSALConvertTest.java b/odl-aaa-moon/aaa/aaa-authn-mdsal-store/aaa-authn-mdsal-store-impl/src/test/java/org/opendaylight/aaa/authn/mdsal/store/MDSALConvertTest.java new file mode 100644 index 00000000..9b7c9712 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn-mdsal-store/aaa-authn-mdsal-store-impl/src/test/java/org/opendaylight/aaa/authn/mdsal/store/MDSALConvertTest.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2016 Cisco Systems, Inc. and others. 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.authn.mdsal.store; + +import org.junit.Assert; +import org.junit.Test; +import org.opendaylight.aaa.api.model.Domain; +import org.opendaylight.aaa.api.model.Grant; +import org.opendaylight.aaa.api.model.Role; +import org.opendaylight.aaa.api.model.User; + +public class MDSALConvertTest { + @Test + public void testConvertDomain() { + Domain d = new Domain(); + d.setDescription("hello"); + d.setDomainid("hello"); + d.setEnabled(true); + d.setName("Hello"); + org.opendaylight.yang.gen.v1.urn.aaa.yang.authn.claims.rev141029.authentication.Domain mdsalDomain = IDMObject2MDSAL.toMDSALDomain(d); + Assert.assertNotNull(mdsalDomain); + Domain d2 = IDMObject2MDSAL.toIDMDomain(mdsalDomain); + Assert.assertNotNull(d2); + Assert.assertEquals(d, d2); + } + + @Test + public void testConvertRole() { + Role r = new Role(); + r.setDescription("hello"); + r.setRoleid("Hello@hello"); + r.setName("Hello"); + r.setDomainid("hello"); + org.opendaylight.yang.gen.v1.urn.aaa.yang.authn.claims.rev141029.authentication.Role mdsalRole = IDMObject2MDSAL.toMDSALRole(r); + Assert.assertNotNull(mdsalRole); + Role r2 = IDMObject2MDSAL.toIDMRole(mdsalRole); + Assert.assertNotNull(r2); + Assert.assertEquals(r, r2); + } + + @Test + public void testConvertUser() { + User u = new User(); + u.setDescription("hello"); + u.setDomainid("hello"); + u.setUserid("hello@hello"); + u.setName("Hello"); + u.setEmail("email"); + u.setEnabled(true); + u.setPassword("pass"); + u.setSalt("salt"); + org.opendaylight.yang.gen.v1.urn.aaa.yang.authn.claims.rev141029.authentication.User mdsalUser = IDMObject2MDSAL.toMDSALUser(u); + Assert.assertNotNull(mdsalUser); + User u2 = IDMObject2MDSAL.toIDMUser(mdsalUser); + Assert.assertNotNull(u2); + Assert.assertEquals(u, u2); + } + + @Test + public void testConvertGrant() { + Grant g = new Grant(); + g.setDomainid("hello"); + g.setUserid("hello@hello"); + g.setRoleid("hello@hello"); + g.setGrantid("hello@hello@Hello"); + org.opendaylight.yang.gen.v1.urn.aaa.yang.authn.claims.rev141029.authentication.Grant mdsalGrant = IDMObject2MDSAL.toMDSALGrant(g); + Assert.assertNotNull(mdsalGrant); + Grant g2 = IDMObject2MDSAL.toIDMGrant(mdsalGrant); + Assert.assertNotNull(g2); + Assert.assertEquals(g, g2); + } +} diff --git a/odl-aaa-moon/aaa/aaa-authn-mdsal-store/aaa-authn-mdsal-store-impl/src/test/java/org/opendaylight/aaa/authn/mdsal/store/util/AuthNStoreUtilTest.java b/odl-aaa-moon/aaa/aaa-authn-mdsal-store/aaa-authn-mdsal-store-impl/src/test/java/org/opendaylight/aaa/authn/mdsal/store/util/AuthNStoreUtilTest.java new file mode 100644 index 00000000..10c18790 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn-mdsal-store/aaa-authn-mdsal-store-impl/src/test/java/org/opendaylight/aaa/authn/mdsal/store/util/AuthNStoreUtilTest.java @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2016 Cisco Systems, Inc. and others. 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.authn.mdsal.store.util; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import java.util.ArrayList; +import java.util.List; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.opendaylight.aaa.api.Authentication; +import org.opendaylight.yang.gen.v1.urn.aaa.yang.authn.claims.rev141029.token_cache_times.token_list.UserTokens; +import org.opendaylight.yang.gen.v1.urn.aaa.yang.authn.claims.rev141029.tokencache.Claims; +import org.opendaylight.yang.gen.v1.urn.aaa.yang.authn.claims.rev141029.tokencache.ClaimsBuilder; +import org.opendaylight.yang.gen.v1.urn.aaa.yang.authn.claims.rev141029.tokencache.ClaimsKey; +import org.powermock.modules.junit4.PowerMockRunner; + +@RunWith(PowerMockRunner.class) +public class AuthNStoreUtilTest { + + private String token = "foo_token_test"; + private String userId = "123"; + private Long expire = new Long(365); + @Mock + private Authentication auth; + @Mock + private UserTokens tokens; + @Mock + private Claims claims; + + @Test + public void testCreateInstIdentifierForTokencache() { + assertTrue(AuthNStoreUtil.createInstIdentifierForTokencache("") == null); + assertNotNull(AuthNStoreUtil.createInstIdentifierForTokencache(token)); + } + + @Test + public void testCreateInstIdentifierUserTokens() { + assertTrue(AuthNStoreUtil.createInstIdentifierUserTokens("", "") == null); + assertNotNull(AuthNStoreUtil.createInstIdentifierUserTokens(userId, token)); + } + + @Test + public void testCreateClaimsRecord() { + assertTrue(AuthNStoreUtil.createClaimsRecord("", null) == null); + assertNotNull(AuthNStoreUtil.createClaimsRecord(token, auth)); + } + + @Test + public void testCreateUserTokens() { + assertTrue(AuthNStoreUtil.createUserTokens("", null) == null); + assertNotNull(AuthNStoreUtil.createUserTokens(token, expire)); + } + + @Test + public void testCreateTokenList() { + assertTrue(AuthNStoreUtil.createTokenList(null, "") == null); + assertNotNull(AuthNStoreUtil.createTokenList(tokens, userId)); + } + + @Test + public void testConvertClaimToAuthentication() { + ClaimsKey claimsKey = new ClaimsKey(token); + ClaimsBuilder claimsBuilder = new ClaimsBuilder(); + claimsBuilder.setClientId("123"); + claimsBuilder.setDomain("foo_domain"); + claimsBuilder.setKey(claimsKey); + List<String> roles = new ArrayList<String>(); + roles.add("foo_role"); + claimsBuilder.setRoles(roles); + claimsBuilder.setToken(token); + claimsBuilder.setUser("foo_usr"); + claimsBuilder.setUserId(userId); + Claims fooClaims = claimsBuilder.build(); + + assertTrue(AuthNStoreUtil.convertClaimToAuthentication(null, expire) == null); + assertNotNull(AuthNStoreUtil.convertClaimToAuthentication(fooClaims, expire)); + } + +} diff --git a/odl-aaa-moon/aaa/aaa-authn-mdsal-store/pom.xml b/odl-aaa-moon/aaa/aaa-authn-mdsal-store/pom.xml new file mode 100644 index 00000000..e5e4f92f --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn-mdsal-store/pom.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <parent> + <groupId>org.opendaylight.aaa</groupId> + <artifactId>aaa-parent</artifactId> + <version>0.3.2-Beryllium-SR2</version> + <relativePath>../parent</relativePath> + </parent> + <modelVersion>4.0.0</modelVersion> + + <artifactId>aaa-authn-mdsal-store</artifactId> + <name>${project.artifactId}</name> + <packaging>pom</packaging> + + <modules> + <module>aaa-authn-mdsal-api</module> + <module>aaa-authn-mdsal-config</module> + <module>aaa-authn-mdsal-store-impl</module> + </modules> +</project> diff --git a/odl-aaa-moon/aaa/aaa-authn-sssd/pom.xml b/odl-aaa-moon/aaa/aaa-authn-sssd/pom.xml new file mode 100644 index 00000000..4dc7eac9 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn-sssd/pom.xml @@ -0,0 +1,88 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + <parent> + <groupId>org.opendaylight.aaa</groupId> + <artifactId>aaa-parent</artifactId> + <version>0.3.2-Beryllium-SR2</version> + <relativePath>../parent</relativePath> + </parent> + + <artifactId>aaa-authn-sssd</artifactId> + <packaging>bundle</packaging> + + <dependencies> + <dependency> + <groupId>org.opendaylight.aaa</groupId> + <artifactId>aaa-authn</artifactId> + </dependency> + <dependency> + <groupId>org.opendaylight.aaa</groupId> + <artifactId>aaa-authn-api</artifactId> + </dependency> + <dependency> + <groupId>org.glassfish</groupId> + <artifactId>javax.json</artifactId> + </dependency> + <dependency> + <groupId>org.opendaylight.aaa</groupId> + <artifactId>aaa-authn-idpmapping</artifactId> + </dependency> + <dependency> + <groupId>org.slf4j</groupId> + <artifactId>slf4j-api</artifactId> + </dependency> + <dependency> + <groupId>com.sun.jersey</groupId> + <artifactId>jersey-server</artifactId> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>javax.servlet</groupId> + <artifactId>javax.servlet-api</artifactId> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>org.osgi</groupId> + <artifactId>org.osgi.core</artifactId> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>org.apache.felix</groupId> + <artifactId>org.apache.felix.dependencymanager</artifactId> + <scope>provided</scope> + </dependency> + <!-- Testing Dependencies --> + <dependency> + <groupId>com.sun.jersey.jersey-test-framework</groupId> + <artifactId>jersey-test-framework-grizzly2</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>junit</groupId> + <artifactId>junit</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.slf4j</groupId> + <artifactId>slf4j-simple</artifactId> + <scope>test</scope> + </dependency> + </dependencies> + + <build> + <plugins> + <plugin> + <groupId>org.apache.felix</groupId> + <artifactId>maven-bundle-plugin</artifactId> + <extensions>true</extensions> + <configuration> + <instructions> + <Bundle-Activator>org.opendaylight.aaa.sssd.Activator</Bundle-Activator> + </instructions> + <manifestLocation>${project.basedir}/META-INF</manifestLocation> + </configuration> + </plugin> + </plugins> + </build> +</project> diff --git a/odl-aaa-moon/aaa/aaa-authn-sssd/src/main/java/org/opendaylight/aaa/sssd/Activator.java b/odl-aaa-moon/aaa/aaa-authn-sssd/src/main/java/org/opendaylight/aaa/sssd/Activator.java new file mode 100644 index 00000000..b6d5259f --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn-sssd/src/main/java/org/opendaylight/aaa/sssd/Activator.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2014, 2015 Hewlett-Packard Development Company, L.P. and others. 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.sssd; + +import org.apache.felix.dm.DependencyActivatorBase; +import org.apache.felix.dm.DependencyManager; +import org.opendaylight.aaa.api.ClaimAuth; +import org.osgi.framework.BundleContext; + +public class Activator extends DependencyActivatorBase { + + @Override + public void init(BundleContext context, DependencyManager manager) throws Exception { + manager.add(createComponent().setInterface(new String[] { ClaimAuth.class.getName() }, null) + .setImplementation(SssdClaimAuth.class)); + } + + @Override + public void destroy(BundleContext context, DependencyManager manager) throws Exception { + } + +} diff --git a/odl-aaa-moon/aaa/aaa-authn-sssd/src/main/java/org/opendaylight/aaa/sssd/SssdClaimAuth.java b/odl-aaa-moon/aaa/aaa-authn-sssd/src/main/java/org/opendaylight/aaa/sssd/SssdClaimAuth.java new file mode 100644 index 00000000..0ae23b48 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn-sssd/src/main/java/org/opendaylight/aaa/sssd/SssdClaimAuth.java @@ -0,0 +1,220 @@ +/* + * Copyright (c) 2014, 2015 Hewlett-Packard Development Company, L.P. and others. 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.sssd; + +import java.io.StringWriter; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import javax.json.Json; +import javax.json.JsonValue; +import javax.json.stream.JsonGenerator; +import javax.json.stream.JsonGeneratorFactory; +import org.apache.felix.dm.Component; +import org.opendaylight.aaa.ClaimBuilder; +import org.opendaylight.aaa.api.Claim; +import org.opendaylight.aaa.api.ClaimAuth; +import org.opendaylight.aaa.idpmapping.RuleProcessor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * An SSSD {@link ClaimAuth} implementation. + * + * @author John Dennis <jdennis@redhat.com> + */ +public class SssdClaimAuth implements ClaimAuth { + private static final Logger LOG = LoggerFactory.getLogger(SssdClaimAuth.class); + + private static final String DEFAULT_MAPPING_RULES_PATHNAME = "etc/idp_mapping_rules.json"; + private JsonGeneratorFactory generatorFactory = null; + private RuleProcessor ruleProcessor = null; + + // Called by DM when all required dependencies are satisfied. + void init(Component c) { + LOG.info("Initializing SSSD Plugin"); + Map<String, Object> properties = new HashMap<String, Object>(1); + properties.put(JsonGenerator.PRETTY_PRINTING, true); + generatorFactory = Json.createGeneratorFactory(properties); + + String mappingRulesFile = DEFAULT_MAPPING_RULES_PATHNAME; + if (mappingRulesFile == null || mappingRulesFile.isEmpty()) { + LOG.warn("mapping rules file is not configured, " + "SssdClaimAuth will be disabled"); + return; + } + + Path mappingRulesPath = Paths.get(mappingRulesFile); + + if (!Files.exists(mappingRulesPath)) { + LOG.warn(String.format("mapping rules file (%s) " + + "does not exist, SssdClaimAuth will be disabled", mappingRulesFile)); + return; + } + + try { + ruleProcessor = new RuleProcessor(mappingRulesPath, null); + } catch (Exception e) { + LOG.error(String.format("mapping rules file (%s) " + + "could not be loaded, SssdClaimAuth will be disabled. " + "error = %s", + mappingRulesFile, e)); + } + } + + /** + * Transform a Map of assertions into a {@link Claim} via a set of mapping + * rules. + * + * A set of mapping rules have been previously loaded. the incoming + * assertion is converted to a JSON document and presented to the + * {@link RuleProcessor}. If the RuleProcessor can successfully transform + * the assertion given the site specific set of rules it will return a Map + * of values which will then be used to build a {@link Claim}. The rule + * should return one or more of the following which will be used to populate + * the Claim. + * + * <dl> + * <dt>ClientId</dt> + * <dd>A string. + * + * @see org.opendaylight.aaa.api.Claim#clientId() </dd> + * + * <dt>UserId</dt> <dd>A string. + * @see org.opendaylight.aaa.api.Claim#userId() </dd> + * + * <dt>User</dt> <dd>A string. + * @see org.opendaylight.aaa.api.Claim#user() </dd> + * + * <dt>Domain</dt> <dd>A string. + * @see org.opendaylight.aaa.api.Claim#domain() </dd> + * + * <dt>Roles</dt> <dd>An array of strings. + * @see org.opendaylight.aaa.api.Claim#roles() </dd> + * + * </dl> + * + * @param assertion + * A Map of name/value assertions provided by an external IdP + * @return A {@link Claim} if successful, null otherwise. + */ + + @Override + public Claim transform(Map<String, Object> assertion) { + String assertionJson; + Map<String, Object> mapped; + assertionJson = claimToJson(assertion); + + if (ruleProcessor == null) { + LOG.debug("ruleProcessor not configured"); + return null; + } + + if (LOG.isDebugEnabled()) { + LOG.debug("assertionJson=\n{}", assertionJson); + } + + mapped = ruleProcessor.process(assertionJson); + if (mapped == null) { + if (LOG.isDebugEnabled()) { + LOG.debug("RuleProcessor returned null"); + } + return null; + } + + if (LOG.isDebugEnabled()) { + LOG.debug("RuleProcessor returned: {}", mapped); + } + + ClaimBuilder cb = new ClaimBuilder(); + if (mapped.containsKey("ClientId")) { + cb.setClientId((String) mapped.get("ClientId")); + } + if (mapped.containsKey("UserId")) { + cb.setUserId((String) mapped.get("UserId")); + } + if (mapped.containsKey("User")) { + cb.setUser((String) mapped.get("User")); + } + if (mapped.containsKey("Domain")) { + cb.setDomain((String) mapped.get("Domain")); + } + if (mapped.containsKey("Roles")) { + @SuppressWarnings("unchecked") + List<String> roles = (List<String>) mapped.get("roles"); + for (String role : roles) { + cb.addRole(role); + } + } + Claim claim = cb.build(); + + if (LOG.isDebugEnabled()) { + LOG.debug("returns claim = {}", claim.toString()); + } + + return claim; + } + + /** + * Convert a Claim Map into a JSON object. + * + * Given a Map of name/value pairs convert it into a JSON object and return + * it as a string. This is not a general purpose routine used to convert any + * Map into JSON because a claim has the restriction that each value must be + * a scalar and those scalars are restricted to the following types: + * + * <ul> + * <li>String</li> + * <li>Integer</li> + * <li>Long</li> + * <li>Double</li> + * <li>Boolean</li> + * <li>null</li> + * </ul> + * + * See also {@link ClaimAuth}. + * + * @param claim + * The Map containing assertion claims to be converted into a + * JSON assertion document. + * @return A string formatted as a JSON object. + */ + + public String claimToJson(Map<String, Object> claim) { + StringWriter stringWriter = new StringWriter(); + JsonGenerator generator = generatorFactory.createGenerator(stringWriter); + + generator.writeStartObject(); + for (Map.Entry<String, Object> entry : claim.entrySet()) { + String name = entry.getKey(); + Object value = entry.getValue(); + + if (value instanceof String) { + generator.write(name, (String) value); + } else if (value instanceof Integer) { + generator.write(name, ((Integer) value).intValue()); + } else if (value instanceof Long) { + generator.write(name, ((Long) value).longValue()); + } else if (value instanceof Double) { + generator.write(name, ((Double) value).doubleValue()); + } else if (value instanceof Boolean) { + generator.write(name, ((Boolean) value).booleanValue()); + } else if (value == null) { + generator.write(name, JsonValue.NULL); + } else { + LOG.warn(String.format("ignoring claim unsupported value type " + + "entry %s has type %s", name, value.getClass().getSimpleName())); + } + } + generator.writeEnd(); + generator.close(); + return stringWriter.toString(); + } +} diff --git a/odl-aaa-moon/aaa/aaa-authn-store/pom.xml b/odl-aaa-moon/aaa/aaa-authn-store/pom.xml new file mode 100644 index 00000000..01fdf252 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn-store/pom.xml @@ -0,0 +1,100 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + <parent> + <groupId>org.opendaylight.aaa</groupId> + <artifactId>aaa-parent</artifactId> + <version>0.3.2-Beryllium-SR2</version> + <relativePath>../parent</relativePath> + </parent> + + <artifactId>aaa-authn-store</artifactId> + <version>0.3.2-Beryllium-SR2</version> + <packaging>bundle</packaging> + + <dependencies> + <dependency> + <groupId>net.sf.ehcache</groupId> + <artifactId>ehcache</artifactId> + </dependency> + <dependency> + <groupId>org.opendaylight.aaa</groupId> + <artifactId>aaa-authn-api</artifactId> + </dependency> + <dependency> + <groupId>org.slf4j</groupId> + <artifactId>slf4j-api</artifactId> + </dependency> + <dependency> + <groupId>org.osgi</groupId> + <artifactId>org.osgi.core</artifactId> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>org.apache.felix</groupId> + <artifactId>org.apache.felix.dependencymanager</artifactId> + <scope>provided</scope> + </dependency> + <!-- Testing Dependencies --> + <dependency> + <groupId>junit</groupId> + <artifactId>junit</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.mockito</groupId> + <artifactId>mockito-all</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.slf4j</groupId> + <artifactId>slf4j-simple</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.opendaylight.aaa</groupId> + <artifactId>aaa-authn</artifactId> + <scope>test</scope> + </dependency> + </dependencies> + + <build> + <plugins> + <plugin> + <groupId>org.apache.felix</groupId> + <artifactId>maven-bundle-plugin</artifactId> + <extensions>true</extensions> + <configuration> + <instructions> + <Bundle-Activator>org.opendaylight.aaa.store.Activator</Bundle-Activator> + </instructions> + <manifestLocation>${project.basedir}/META-INF</manifestLocation> + </configuration> + </plugin> + <plugin> + <groupId>org.codehaus.mojo</groupId> + <artifactId>build-helper-maven-plugin</artifactId> + <executions> + <execution> + <id>attach-artifacts</id> + <phase>package</phase> + <goals> + <goal>attach-artifact</goal> + </goals> + <configuration> + <artifacts> + <artifact> + <file>${project.build.directory}/classes/tokens.cfg</file> + <type>cfg</type> + <classifier>config</classifier> + </artifact> + </artifacts> + </configuration> + </execution> + </executions> + </plugin> + </plugins> + </build> + +</project> diff --git a/odl-aaa-moon/aaa/aaa-authn-store/src/main/java/org/opendaylight/aaa/store/Activator.java b/odl-aaa-moon/aaa/aaa-authn-store/src/main/java/org/opendaylight/aaa/store/Activator.java new file mode 100644 index 00000000..f3299723 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn-store/src/main/java/org/opendaylight/aaa/store/Activator.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2014, 2015 Hewlett-Packard Development Company, L.P. and others. 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.store; + +import java.util.Dictionary; +import org.apache.felix.dm.DependencyActivatorBase; +import org.apache.felix.dm.DependencyManager; +import org.opendaylight.aaa.api.TokenStore; +import org.osgi.framework.BundleContext; +import org.osgi.framework.Constants; +import org.osgi.service.cm.ManagedService; + +/** + * An activator for the default datastore implementation of {@link TokenStore}. + * + * @author liemmn + */ +public class Activator extends DependencyActivatorBase { + + private static final String TOKEN_PID = "org.opendaylight.aaa.tokens"; + + @Override + public void init(BundleContext context, DependencyManager manager) throws Exception { + DefaultTokenStore ts = new DefaultTokenStore(); + manager.add(createComponent().setInterface(new String[] { TokenStore.class.getName() }, + null).setImplementation(ts)); + context.registerService(ManagedService.class.getName(), ts, + addPid(DefaultTokenStore.defaults)); + } + + @Override + public void destroy(BundleContext context, DependencyManager manager) throws Exception { + } + + private Dictionary<String, ?> addPid(Dictionary<String, String> dict) { + dict.put(Constants.SERVICE_PID, TOKEN_PID); + return dict; + } +} diff --git a/odl-aaa-moon/aaa/aaa-authn-store/src/main/java/org/opendaylight/aaa/store/DefaultTokenStore.java b/odl-aaa-moon/aaa/aaa-authn-store/src/main/java/org/opendaylight/aaa/store/DefaultTokenStore.java new file mode 100644 index 00000000..df65be32 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn-store/src/main/java/org/opendaylight/aaa/store/DefaultTokenStore.java @@ -0,0 +1,154 @@ +/* + * Copyright (c) 2014, 2015 Hewlett-Packard Development Company, L.P. and others. 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.store; + +import java.io.File; +import java.lang.management.ManagementFactory; +import java.util.Dictionary; +import java.util.Hashtable; +import java.util.concurrent.locks.ReentrantLock; +import javax.management.MBeanServer; +import net.sf.ehcache.Cache; +import net.sf.ehcache.CacheManager; +import net.sf.ehcache.Element; +import net.sf.ehcache.config.CacheConfiguration; +import net.sf.ehcache.management.ManagementService; +import org.apache.felix.dm.Component; +import org.opendaylight.aaa.api.Authentication; +import org.opendaylight.aaa.api.TokenStore; +import org.osgi.service.cm.ConfigurationException; +import org.osgi.service.cm.ManagedService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A default token store for STS. + * + * @author liemmn + * + */ +public class DefaultTokenStore implements TokenStore, ManagedService { + private static final Logger LOG = LoggerFactory.getLogger(DefaultTokenStore.class); + private static final String TOKEN_STORE_CONFIG_ERR = "Token store configuration error"; + + private static final String TOKEN_CACHE_MANAGER = "org.opendaylight.aaa"; + private static final String TOKEN_CACHE = "tokens"; + private static final String EHCACHE_XML = "etc/ehcache.xml"; + + static final String MAX_CACHED_MEMORY = "maxCachedTokensInMemory"; + static final String MAX_CACHED_DISK = "maxCachedTokensOnDisk"; + static final String SECS_TO_LIVE = "secondsToLive"; + static final String SECS_TO_IDLE = "secondsToIdle"; + + // Defaults (needed only for non-Karaf deployments) + static final Dictionary<String, String> defaults = new Hashtable<>(); + static { + defaults.put(MAX_CACHED_MEMORY, Long.toString(10000)); + defaults.put(MAX_CACHED_DISK, Long.toString(1000000)); + defaults.put(SECS_TO_IDLE, Long.toString(3600)); + defaults.put(SECS_TO_LIVE, Long.toString(3600)); + } + + // Token cache lock + private static final ReentrantLock cacheLock = new ReentrantLock(); + + // Token cache + private Cache tokens; + + // This should be a singleton + DefaultTokenStore() { + } + + // Called by DM when all required dependencies are satisfied. + void init(Component c) { + File ehcache = new File(EHCACHE_XML); + CacheManager cm; + if (ehcache.exists()) { + cm = CacheManager.create(ehcache.getAbsolutePath()); + tokens = cm.getCache(TOKEN_CACHE); + LOG.info("Initialized token store with custom cache config"); + } else { + cm = CacheManager.getInstance(); + tokens = new Cache( + new CacheConfiguration(TOKEN_CACHE, + Integer.parseInt(defaults.get(MAX_CACHED_MEMORY))).maxEntriesLocalDisk( + Integer.parseInt(defaults.get(MAX_CACHED_DISK))) + .timeToLiveSeconds( + Long.parseLong(defaults.get(SECS_TO_LIVE))) + .timeToIdleSeconds( + Long.parseLong(defaults.get(SECS_TO_IDLE)))); + cm.addCache(tokens); + LOG.info("Initialized token store with default cache config"); + } + cm.setName(TOKEN_CACHE_MANAGER); + + // JMX for cache management + MBeanServer mBeanServer = ManagementFactory.getPlatformMBeanServer(); + ManagementService.registerMBeans(cm, mBeanServer, false, false, false, true); + } + + // Called on shutdown + void destroy() { + LOG.info("Shutting down token store..."); + CacheManager.getInstance().shutdown(); + } + + @Override + public Authentication get(String token) { + Element elem = tokens.get(token); + return (Authentication) ((elem != null) ? elem.getObjectValue() : null); + } + + @Override + public void put(String token, Authentication auth) { + tokens.put(new Element(token, auth)); + } + + @Override + public boolean delete(String token) { + return tokens.remove(token); + } + + @Override + public long tokenExpiration() { + return tokens.getCacheConfiguration().getTimeToLiveSeconds(); + } + + @Override + public void updated(@SuppressWarnings("rawtypes") Dictionary props) + throws ConfigurationException { + LOG.info("Updating token store configuration..."); + if (props == null) { + // Someone deleted the configuration, use defaults + props = defaults; + } + reconfig(props); + } + + // Refresh cache configuration... + private void reconfig(@SuppressWarnings("rawtypes") Dictionary props) + throws ConfigurationException { + cacheLock.lock(); + try { + long secsToIdle = Long.parseLong(props.get(SECS_TO_IDLE).toString()); + long secsToLive = Long.parseLong(props.get(SECS_TO_LIVE).toString()); + int maxMem = Integer.parseInt(props.get(MAX_CACHED_MEMORY).toString()); + int maxDisk = Integer.parseInt(props.get(MAX_CACHED_DISK).toString()); + CacheConfiguration config = tokens.getCacheConfiguration(); + config.setTimeToIdleSeconds(secsToIdle); + config.setTimeToLiveSeconds(secsToLive); + config.maxEntriesLocalHeap(maxMem); + config.maxEntriesLocalDisk(maxDisk); + } catch (Throwable t) { + throw new ConfigurationException(null, TOKEN_STORE_CONFIG_ERR, t); + } finally { + cacheLock.unlock(); + } + } +} diff --git a/odl-aaa-moon/aaa/aaa-authn-store/src/main/resources/OSGI-INF/metatype/metatype.properties b/odl-aaa-moon/aaa/aaa-authn-store/src/main/resources/OSGI-INF/metatype/metatype.properties new file mode 100644 index 00000000..b88d5c10 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn-store/src/main/resources/OSGI-INF/metatype/metatype.properties @@ -0,0 +1,14 @@ +org.opendaylight.aaa.tokens.name = Opendaylight AAA Token Configuration +org.opendaylight.aaa.tokens.description = Configuration for AAA tokens +org.opendaylight.aaa.tokens.maxCachedTokensInMemory.name = Memory Configuration +org.opendaylight.aaa.tokens.maxCachedTokensInMemory.description = Maximum number of \ +tokens in memory +org.opendaylight.aaa.tokens.maxCachedTokensOnDisk.name = Disk Configuration +org.opendaylight.aaa.tokens.maxCachedTokensOnDisk.description = Maximum number of \ +tokens in memory +org.opendaylight.aaa.tokens.secondsToLive.name = Token Expiration +org.opendaylight.aaa.tokens.secondsToLive.description = Maximum number of \ +seconds a token can exist regardless of use. Zero (0) means never expires. +org.opendaylight.aaa.tokens.secondsToIdle.name = Unused Token Expiration +org.opendaylight.aaa.tokens.secondsToIdle.description = Maximum number of \ +seconds a token can exist without being accessed. Zero (0) means never expires.
\ No newline at end of file diff --git a/odl-aaa-moon/aaa/aaa-authn-store/src/main/resources/OSGI-INF/metatype/metatype.xml b/odl-aaa-moon/aaa/aaa-authn-store/src/main/resources/OSGI-INF/metatype/metatype.xml new file mode 100644 index 00000000..d04874f4 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn-store/src/main/resources/OSGI-INF/metatype/metatype.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<metatype:MetaData xmlns:metatype="http://www.osgi.org/xmlns/metatype/v1.0.0" + localization="OSGI-INF/metatype/metatype"> + <OCD id="org.opendaylight.aaa.tokens" name="%org.opendaylight.aaa.tokens.name" + description="%org.opendaylight.aaa.tokens.description"> + <AD id="maxCachedTokensInMemory" type="Long" default="10000" + name="%org.opendaylight.aaa.tokens.maxCachedTokensInMemory.name" + description="%org.opendaylight.aaa.tokens.maxCachedTokensInMemory.description" /> + <AD id="maxCachedTokensOnDisk" type="Long" default="1000000" + name="%org.opendaylight.aaa.tokens.maxCachedTokensOnDisk.name" + description="%org.opendaylight.aaa.tokens.maxCachedTokensOnDisk.description" /> + <AD id="secondsToLive" type="Long" default="3600" + name="%org.opendaylight.aaa.tokens.secondsToLive.name" + description="%org.opendaylight.aaa.tokens.secondsToLive.description" /> + <AD id="secondsToIdle" type="Long" default="3600" + name="%org.opendaylight.aaa.tokens.secondsToIdle.name" + description="%org.opendaylight.aaa.tokens.secondsToIdle.description" /> + </OCD> + <Designate pid="org.opendaylight.aaa.tokens"> + <Object ocdref="org.opendaylight.aaa.tokens" /> + </Designate> +</metatype:MetaData>
\ No newline at end of file diff --git a/odl-aaa-moon/aaa/aaa-authn-store/src/main/resources/tokens.cfg b/odl-aaa-moon/aaa/aaa-authn-store/src/main/resources/tokens.cfg new file mode 100644 index 00000000..d3dda90e --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn-store/src/main/resources/tokens.cfg @@ -0,0 +1,4 @@ +maxCachedTokensInMemory=10000 +maxCachedTokensOnDisk=1000000 +secondsToLive=3600 +secondsToIdle=3600
\ No newline at end of file diff --git a/odl-aaa-moon/aaa/aaa-authn-store/src/test/java/org/opendaylight/aaa/store/DefaultTokenStoreTest.java b/odl-aaa-moon/aaa/aaa-authn-store/src/test/java/org/opendaylight/aaa/store/DefaultTokenStoreTest.java new file mode 100644 index 00000000..e5c837bf --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn-store/src/test/java/org/opendaylight/aaa/store/DefaultTokenStoreTest.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2014, 2015 Hewlett-Packard Development Company, L.P. and others. 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.store; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.mockito.Mockito.mock; +import static org.opendaylight.aaa.store.DefaultTokenStore.MAX_CACHED_DISK; +import static org.opendaylight.aaa.store.DefaultTokenStore.MAX_CACHED_MEMORY; +import static org.opendaylight.aaa.store.DefaultTokenStore.SECS_TO_IDLE; +import static org.opendaylight.aaa.store.DefaultTokenStore.SECS_TO_LIVE; + +import java.util.Dictionary; +import java.util.Hashtable; +import org.apache.felix.dm.Component; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.opendaylight.aaa.AuthenticationBuilder; +import org.opendaylight.aaa.ClaimBuilder; +import org.opendaylight.aaa.api.Authentication; +import org.osgi.service.cm.ConfigurationException; + +public class DefaultTokenStoreTest { + private static final String FOO_TOKEN = "foo_token"; + private final DefaultTokenStore dts = new DefaultTokenStore(); + private static final Dictionary<String, String> config = new Hashtable<>(); + static { + config.put(MAX_CACHED_MEMORY, Long.toString(3)); + config.put(MAX_CACHED_DISK, Long.toString(3)); + config.put(SECS_TO_IDLE, Long.toString(1)); + config.put(SECS_TO_LIVE, Long.toString(1)); + } + + @Before + public void setup() throws ConfigurationException { + dts.init(mock(Component.class)); + dts.updated(config); + } + + @After + public void teardown() { + dts.destroy(); + } + + @Test + public void testCache() throws InterruptedException { + Authentication auth = new AuthenticationBuilder(new ClaimBuilder().setUser("foo") + .setUserId("1234") + .addRole("admin").build()).build(); + dts.put(FOO_TOKEN, auth); + assertEquals(auth, dts.get(FOO_TOKEN)); + dts.delete(FOO_TOKEN); + assertNull(dts.get(FOO_TOKEN)); + dts.put(FOO_TOKEN, auth); + Thread.sleep(1200); + assertNull(dts.get(FOO_TOKEN)); + } + +} diff --git a/odl-aaa-moon/aaa/aaa-authn-sts/pom.xml b/odl-aaa-moon/aaa/aaa-authn-sts/pom.xml new file mode 100644 index 00000000..7dbf86ab --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn-sts/pom.xml @@ -0,0 +1,112 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + <parent> + <groupId>org.opendaylight.aaa</groupId> + <artifactId>aaa-parent</artifactId> + <version>0.3.2-Beryllium-SR2</version> + <relativePath>../parent</relativePath> + </parent> + + <artifactId>aaa-authn-sts</artifactId> + <packaging>bundle</packaging> + + <dependencies> + <dependency> + <groupId>org.opendaylight.aaa</groupId> + <artifactId>aaa-authn</artifactId> + </dependency> + <dependency> + <groupId>org.opendaylight.aaa</groupId> + <artifactId>aaa-authn-api</artifactId> + </dependency> + <dependency> + <groupId>org.slf4j</groupId> + <artifactId>slf4j-api</artifactId> + </dependency> + <dependency> + <groupId>com.sun.jersey</groupId> + <artifactId>jersey-server</artifactId> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>javax.servlet</groupId> + <artifactId>javax.servlet-api</artifactId> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>org.apache.oltu.oauth2</groupId> + <artifactId>org.apache.oltu.oauth2.authzserver</artifactId> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>org.apache.oltu.oauth2</groupId> + <artifactId>org.apache.oltu.oauth2.common</artifactId> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>org.apache.oltu.oauth2</groupId> + <artifactId>org.apache.oltu.oauth2.resourceserver</artifactId> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>org.osgi</groupId> + <artifactId>org.osgi.core</artifactId> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>org.apache.felix</groupId> + <artifactId>org.apache.felix.dependencymanager</artifactId> + <scope>provided</scope> + </dependency> + <!-- Testing Dependencies --> + <dependency> + <groupId>com.sun.jersey.jersey-test-framework</groupId> + <artifactId>jersey-test-framework-grizzly2</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.eclipse.jetty</groupId> + <artifactId>jetty-servlet-tester</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>junit</groupId> + <artifactId>junit</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.mockito</groupId> + <artifactId>mockito-all</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.slf4j</groupId> + <artifactId>slf4j-simple</artifactId> + <scope>test</scope> + </dependency> + </dependencies> + + <build> + <plugins> + <plugin> + <groupId>org.apache.felix</groupId> + <artifactId>maven-bundle-plugin</artifactId> + <extensions>true</extensions> + <configuration> + <instructions> + <Import-Package> + *, + com.sun.jersey.spi.container.servlet + </Import-Package> + <Web-ContextPath>/oauth2</Web-ContextPath> + <Bundle-Activator>org.opendaylight.aaa.sts.Activator</Bundle-Activator> + <manifestLocation>${project.basedir}/META-INF</manifestLocation> + </instructions> + </configuration> + </plugin> + </plugins> + </build> + +</project> diff --git a/odl-aaa-moon/aaa/aaa-authn-sts/src/main/java/org/opendaylight/aaa/sts/Activator.java b/odl-aaa-moon/aaa/aaa-authn-sts/src/main/java/org/opendaylight/aaa/sts/Activator.java new file mode 100644 index 00000000..1bf4591d --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn-sts/src/main/java/org/opendaylight/aaa/sts/Activator.java @@ -0,0 +1,207 @@ +/* + * Copyright (c) 2014, 2015 Hewlett-Packard Development Company, L.P. and others. 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.sts; + +import com.google.common.base.Function; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableList.Builder; +import com.google.common.collect.Lists; +import java.util.List; +import org.apache.felix.dm.DependencyActivatorBase; +import org.apache.felix.dm.DependencyManager; +import org.opendaylight.aaa.api.AuthenticationService; +import org.opendaylight.aaa.api.ClaimAuth; +import org.opendaylight.aaa.api.ClientService; +import org.opendaylight.aaa.api.CredentialAuth; +import org.opendaylight.aaa.api.IdMService; +import org.opendaylight.aaa.api.TokenAuth; +import org.opendaylight.aaa.api.TokenStore; +import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceReference; +import org.osgi.util.tracker.ServiceTracker; +import org.osgi.util.tracker.ServiceTrackerCustomizer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * An activator for the secure token server to inject in a + * {@link CredentialAuth} implementation. + * + * @author liemmn + * @author Ryan Goulding (ryandgoulding@gmail.com) + */ +public class Activator extends DependencyActivatorBase { + + private static final Logger LOG = LoggerFactory.getLogger(Activator.class); + + // Definition of several methods called in the ServiceLocator through + // Reflection + private static final String AUTHENTICATION_SERVICE_REMOVED = "authenticationServiceRemoved"; + private static final String AUTHENTICATION_SERVICE_ADDED = "authenticationServiceAdded"; + private static final String TOKEN_STORE_REMOVED = "tokenStoreRemoved"; + private static final String TOKEN_STORE_ADDED = "tokenStoreAdded"; + private static final String TOKEN_AUTH_REMOVED = "tokenAuthRemoved"; + private static final String TOKEN_AUTH_ADDED = "tokenAuthAdded"; + private static final String CLAIM_AUTH_REMOVED = "claimAuthRemoved"; + private static final String CLAIM_AUTH_ADDED = "claimAuthAdded"; + private static final String CREDENTIAL_AUTH_REMOVED = "credentialAuthRemoved"; + private static final String CREDENTIAL_AUTH_ADDED = "credentialAuthAdded"; + + // A collection of all services, which is used for closing ServiceTrackers + private ImmutableList<ServiceTracker<?, ?>> services; + + @Override + public void init(BundleContext context, DependencyManager manager) throws Exception { + + LOG.info("STS Activator initializing"); + manager.add(createComponent().setImplementation(ServiceLocator.getInstance()) + .add(createServiceDependency().setService(CredentialAuth.class) + .setRequired(true) + .setCallbacks( + CREDENTIAL_AUTH_ADDED, + CREDENTIAL_AUTH_REMOVED)) + .add(createServiceDependency().setService(ClaimAuth.class) + .setRequired(false) + .setCallbacks(CLAIM_AUTH_ADDED, + CLAIM_AUTH_REMOVED)) + .add(createServiceDependency().setService(TokenAuth.class) + .setRequired(false) + .setCallbacks(TOKEN_AUTH_ADDED, + TOKEN_AUTH_REMOVED)) + .add(createServiceDependency().setService(TokenStore.class) + .setRequired(true) + .setCallbacks(TOKEN_STORE_ADDED, + TOKEN_STORE_REMOVED)) + .add(createServiceDependency().setService(TokenStore.class) + .setRequired(true)) + .add(createServiceDependency().setService( + AuthenticationService.class) + .setRequired(true) + .setCallbacks( + AUTHENTICATION_SERVICE_ADDED, + AUTHENTICATION_SERVICE_REMOVED)) + .add(createServiceDependency().setService(IdMService.class) + .setRequired(true)) + .add(createServiceDependency().setService(ClientService.class) + .setRequired(true))); + + final Builder<ServiceTracker<?, ?>> servicesBuilder = new ImmutableList.Builder<ServiceTracker<?, ?>>(); + + // Async ServiceTrackers to track and load AAA STS bundles + final ServiceTracker<AuthenticationService, AuthenticationService> authenticationService = new ServiceTracker<>( + context, AuthenticationService.class, + new AAAServiceTrackerCustomizer<AuthenticationService>( + new Function<AuthenticationService, Void>() { + @Override + public Void apply(AuthenticationService authenticationService) { + ServiceLocator.getInstance().setAuthenticationService( + authenticationService); + return null; + } + })); + servicesBuilder.add(authenticationService); + authenticationService.open(); + + final ServiceTracker<IdMService, IdMService> idmService = new ServiceTracker<>(context, + IdMService.class, new AAAServiceTrackerCustomizer<IdMService>( + new Function<IdMService, Void>() { + @Override + public Void apply(IdMService idmService) { + ServiceLocator.getInstance().setIdmService(idmService); + return null; + } + })); + servicesBuilder.add(idmService); + idmService.open(); + + final ServiceTracker<TokenAuth, TokenAuth> tokenAuthService = new ServiceTracker<>(context, + TokenAuth.class, new AAAServiceTrackerCustomizer<TokenAuth>( + new Function<TokenAuth, Void>() { + @Override + public Void apply(TokenAuth tokenAuth) { + final List<TokenAuth> tokenAuthCollection = (List<TokenAuth>) Lists.newArrayList(tokenAuth); + ServiceLocator.getInstance().setTokenAuthCollection( + tokenAuthCollection); + return null; + } + })); + servicesBuilder.add(tokenAuthService); + tokenAuthService.open(); + + final ServiceTracker<TokenStore, TokenStore> tokenStoreService = new ServiceTracker<>( + context, TokenStore.class, new AAAServiceTrackerCustomizer<TokenStore>( + new Function<TokenStore, Void>() { + @Override + public Void apply(TokenStore tokenStore) { + ServiceLocator.getInstance().setTokenStore(tokenStore); + return null; + } + })); + servicesBuilder.add(tokenStoreService); + tokenStoreService.open(); + + final ServiceTracker<ClientService, ClientService> clientService = new ServiceTracker<>( + context, ClientService.class, new AAAServiceTrackerCustomizer<ClientService>( + new Function<ClientService, Void>() { + @Override + public Void apply(ClientService clientService) { + ServiceLocator.getInstance().setClientService(clientService); + return null; + } + })); + servicesBuilder.add(clientService); + clientService.open(); + + services = servicesBuilder.build(); + + LOG.info("STS Activator initialized; ServiceTracker may still be processing"); + } + + /** + * Wrapper for AAA generic service loading. + * + * @param <S> + */ + static final class AAAServiceTrackerCustomizer<S> implements ServiceTrackerCustomizer<S, S> { + + private Function<S, Void> callback; + + public AAAServiceTrackerCustomizer(final Function<S, Void> callback) { + this.callback = callback; + } + + @Override + public S addingService(ServiceReference<S> reference) { + S service = reference.getBundle().getBundleContext().getService(reference); + LOG.info("Unable to resolve {}", service.getClass()); + try { + callback.apply(service); + } catch (Exception e) { + LOG.error("Unable to resolve {}", service.getClass(), e); + } + return service; + } + + @Override + public void modifiedService(ServiceReference<S> reference, S service) { + } + + @Override + public void removedService(ServiceReference<S> reference, S service) { + } + } + + @Override + public void destroy(BundleContext context, DependencyManager manager) throws Exception { + + for (ServiceTracker<?, ?> serviceTracker : services) { + serviceTracker.close(); + } + } +} diff --git a/odl-aaa-moon/aaa/aaa-authn-sts/src/main/java/org/opendaylight/aaa/sts/AnonymousPasswordValidator.java b/odl-aaa-moon/aaa/aaa-authn-sts/src/main/java/org/opendaylight/aaa/sts/AnonymousPasswordValidator.java new file mode 100644 index 00000000..55b5b61f --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn-sts/src/main/java/org/opendaylight/aaa/sts/AnonymousPasswordValidator.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2014, 2015 Hewlett-Packard Development Company, L.P. and others. 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.sts; + +import javax.servlet.http.HttpServletRequest; +import org.apache.oltu.oauth2.common.OAuth; +import org.apache.oltu.oauth2.common.validators.AbstractValidator; + +/** + * A password validator that does not enforce client identification. + * + * @author liemmn + * + */ +public class AnonymousPasswordValidator extends AbstractValidator<HttpServletRequest> { + + public AnonymousPasswordValidator() { + requiredParams.add(OAuth.OAUTH_GRANT_TYPE); + requiredParams.add(OAuth.OAUTH_USERNAME); + requiredParams.add(OAuth.OAUTH_PASSWORD); + + enforceClientAuthentication = false; + } +} diff --git a/odl-aaa-moon/aaa/aaa-authn-sts/src/main/java/org/opendaylight/aaa/sts/AnonymousRefreshTokenValidator.java b/odl-aaa-moon/aaa/aaa-authn-sts/src/main/java/org/opendaylight/aaa/sts/AnonymousRefreshTokenValidator.java new file mode 100644 index 00000000..5b50c7da --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn-sts/src/main/java/org/opendaylight/aaa/sts/AnonymousRefreshTokenValidator.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2014, 2015 Hewlett-Packard Development Company, L.P. and others. 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.sts; + +import javax.servlet.http.HttpServletRequest; +import org.apache.oltu.oauth2.common.OAuth; +import org.apache.oltu.oauth2.common.validators.AbstractValidator; + +/** + * A refresh token validator that does not enforce client identification. + * + * @author liemmn + * + */ +public class AnonymousRefreshTokenValidator extends AbstractValidator<HttpServletRequest> { + + public AnonymousRefreshTokenValidator() { + requiredParams.add(OAuth.OAUTH_GRANT_TYPE); + requiredParams.add(OAuth.OAUTH_REFRESH_TOKEN); + + enforceClientAuthentication = false; + } +} diff --git a/odl-aaa-moon/aaa/aaa-authn-sts/src/main/java/org/opendaylight/aaa/sts/OAuthRequest.java b/odl-aaa-moon/aaa/aaa-authn-sts/src/main/java/org/opendaylight/aaa/sts/OAuthRequest.java new file mode 100644 index 00000000..2a2b34b6 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn-sts/src/main/java/org/opendaylight/aaa/sts/OAuthRequest.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2014, 2015 Hewlett-Packard Development Company, L.P. and others. 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.sts; + +import javax.servlet.http.HttpServletRequest; +import org.apache.oltu.oauth2.as.request.AbstractOAuthTokenRequest; +import org.apache.oltu.oauth2.as.validator.UnauthenticatedAuthorizationCodeValidator; +import org.apache.oltu.oauth2.common.exception.OAuthProblemException; +import org.apache.oltu.oauth2.common.exception.OAuthSystemException; +import org.apache.oltu.oauth2.common.message.types.GrantType; +import org.apache.oltu.oauth2.common.validators.OAuthValidator; + +/** + * OAuth request wrapper. + * + * @author liemmn + * + */ +public class OAuthRequest extends AbstractOAuthTokenRequest { + + public OAuthRequest(HttpServletRequest request) throws OAuthSystemException, + OAuthProblemException { + super(request); + } + + @Override + public OAuthValidator<HttpServletRequest> initValidator() throws OAuthProblemException, + OAuthSystemException { + validators.put(GrantType.PASSWORD.toString(), AnonymousPasswordValidator.class); + validators.put(GrantType.REFRESH_TOKEN.toString(), AnonymousRefreshTokenValidator.class); + validators.put(GrantType.AUTHORIZATION_CODE.toString(), + UnauthenticatedAuthorizationCodeValidator.class); + return super.initValidator(); + } + +} diff --git a/odl-aaa-moon/aaa/aaa-authn-sts/src/main/java/org/opendaylight/aaa/sts/ServiceLocator.java b/odl-aaa-moon/aaa/aaa-authn-sts/src/main/java/org/opendaylight/aaa/sts/ServiceLocator.java new file mode 100644 index 00000000..2c1f84c3 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn-sts/src/main/java/org/opendaylight/aaa/sts/ServiceLocator.java @@ -0,0 +1,141 @@ +/* + * Copyright (c) 2014, 2015 Hewlett-Packard Development Company, L.P. and others. 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.sts; + +import java.util.List; +import java.util.Vector; +import org.opendaylight.aaa.api.AuthenticationService; +import org.opendaylight.aaa.api.ClientService; +import org.opendaylight.aaa.api.CredentialAuth; +import org.opendaylight.aaa.api.IdMService; +import org.opendaylight.aaa.api.PasswordCredentials; +import org.opendaylight.aaa.api.TokenAuth; +import org.opendaylight.aaa.api.TokenStore; + +/** + * A service locator to bridge between the web world and OSGi world. + * + * @author liemmn + * + */ +public class ServiceLocator { + + private static final ServiceLocator instance = new ServiceLocator(); + + protected volatile List<TokenAuth> tokenAuthCollection = new Vector<>(); + + protected volatile CredentialAuth<PasswordCredentials> credentialAuth; + + protected volatile TokenStore tokenStore; + + protected volatile AuthenticationService authenticationService; + + protected volatile IdMService idmService; + + protected volatile ClientService clientService; + + private ServiceLocator() { + } + + public static ServiceLocator getInstance() { + return instance; + } + + /** + * Called through reflection by the sts activator. + * + * @see org.opendaylight.aaa.sts.Activator + * @param ta + */ + protected void tokenAuthAdded(TokenAuth ta) { + this.tokenAuthCollection.add(ta); + } + + /** + * Called through reflection by the sts activator. + * + * @see org.opendaylight.aaa.sts.Activator + * @param ta + */ + protected void tokenAuthRemoved(TokenAuth ta) { + this.tokenAuthCollection.remove(ta); + } + + protected void tokenStoreAdded(TokenStore ts) { + this.tokenStore = ts; + } + + protected void tokenStoreRemoved(TokenStore ts) { + this.tokenStore = null; + } + + protected void authenticationServiceAdded(AuthenticationService as) { + this.authenticationService = as; + } + + protected void authenticationServiceRemoved(AuthenticationService as) { + this.authenticationService = null; + } + + protected void credentialAuthAdded(CredentialAuth<PasswordCredentials> da) { + this.credentialAuth = da; + } + + protected void credentialAuthAddedRemoved(CredentialAuth<PasswordCredentials> da) { + this.credentialAuth = null; + } + + public List<TokenAuth> getTokenAuthCollection() { + return tokenAuthCollection; + } + + public void setTokenAuthCollection(List<TokenAuth> tokenAuthCollection) { + this.tokenAuthCollection = tokenAuthCollection; + } + + public CredentialAuth<PasswordCredentials> getCredentialAuth() { + return credentialAuth; + } + + public synchronized void setCredentialAuth(CredentialAuth<PasswordCredentials> credentialAuth) { + this.credentialAuth = credentialAuth; + } + + public TokenStore getTokenStore() { + return tokenStore; + } + + public void setTokenStore(TokenStore tokenStore) { + this.tokenStore = tokenStore; + } + + public AuthenticationService getAuthenticationService() { + return authenticationService; + } + + public void setAuthenticationService(AuthenticationService authenticationService) { + this.authenticationService = authenticationService; + } + + public IdMService getIdmService() { + return idmService; + } + + public void setIdmService(IdMService idmService) { + this.idmService = idmService; + } + + public ClientService getClientService() { + return clientService; + } + + public void setClientService(ClientService clientService) { + this.clientService = clientService; + } +} diff --git a/odl-aaa-moon/aaa/aaa-authn-sts/src/main/java/org/opendaylight/aaa/sts/TokenAuthFilter.java b/odl-aaa-moon/aaa/aaa-authn-sts/src/main/java/org/opendaylight/aaa/sts/TokenAuthFilter.java new file mode 100644 index 00000000..3fa7a66c --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn-sts/src/main/java/org/opendaylight/aaa/sts/TokenAuthFilter.java @@ -0,0 +1,148 @@ +/* + * Copyright (c) 2014, 2015 Hewlett-Packard Development Company, L.P. and others. 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.sts; + +import com.sun.jersey.spi.container.ContainerRequest; +import com.sun.jersey.spi.container.ContainerRequestFilter; +import java.util.List; +import java.util.Map; +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status; +import org.apache.oltu.oauth2.common.exception.OAuthProblemException; +import org.apache.oltu.oauth2.common.exception.OAuthSystemException; +import org.apache.oltu.oauth2.common.message.types.ParameterStyle; +import org.apache.oltu.oauth2.rs.request.OAuthAccessResourceRequest; +import org.opendaylight.aaa.api.Authentication; +import org.opendaylight.aaa.api.AuthenticationException; +import org.opendaylight.aaa.api.TokenAuth; + +/** + * A token-based authentication filter for resource providers. + * + * Deprecated: Use <code>AAAFilter</code> instead. + * + * @author liemmn + * + */ +@Deprecated +public class TokenAuthFilter implements ContainerRequestFilter { + + private final String OPTIONS = "OPTIONS"; + private final String ACCESS_CONTROL_REQUEST_HEADERS = "Access-Control-Request-Headers"; + private final String AUTHORIZATION = "authorization"; + + @Context + private HttpServletRequest httpRequest; + + @Override + public ContainerRequest filter(ContainerRequest request) { + + // Do the CORS check first + if (checkCORSOptionRequest(request)) { + return request; + } + + // Are we up yet? + if (ServiceLocator.getInstance().getAuthenticationService() == null) { + throw new WebApplicationException( + Response.status(Status.SERVICE_UNAVAILABLE).type(MediaType.APPLICATION_JSON) + .entity("{\"error\":\"Authentication service unavailable\"}").build()); + } + + // Are we doing authentication or not? + if (ServiceLocator.getInstance().getAuthenticationService().isAuthEnabled()) { + Map<String, List<String>> headers = request.getRequestHeaders(); + + // Go through and invoke other TokenAuth first... + List<TokenAuth> tokenAuthCollection = ServiceLocator.getInstance() + .getTokenAuthCollection(); + for (TokenAuth ta : tokenAuthCollection) { + try { + Authentication auth = ta.validate(headers); + if (auth != null) { + ServiceLocator.getInstance().getAuthenticationService().set(auth); + return request; + } + } catch (AuthenticationException ae) { + throw unauthorized(); + } + } + + // OK, last chance to validate token... + try { + OAuthAccessResourceRequest or = new OAuthAccessResourceRequest(httpRequest, + ParameterStyle.HEADER); + validate(or.getAccessToken()); + } catch (OAuthSystemException | OAuthProblemException e) { + throw unauthorized(); + } + } + + return request; + } + + /** + * CORS access control : when browser sends cross-origin request, it first + * sends the OPTIONS method with a list of access control request headers, + * which has a list of custom headers and access control method such as GET. + * POST etc. You custom header "Authorization will not be present in request + * header, instead it will be present as a value inside + * Access-Control-Request-Headers. We should not do any authorization + * against such request. for more details : + * https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS + */ + + private boolean checkCORSOptionRequest(ContainerRequest request) { + if (OPTIONS.equals(request.getMethod())) { + List<String> headerList = request.getRequestHeader(ACCESS_CONTROL_REQUEST_HEADERS); + if (headerList != null && !headerList.isEmpty()) { + String header = headerList.get(0); + if (header != null && header.toLowerCase().contains(AUTHORIZATION)) { + return true; + } + } + } + return false; + } + + // Validate an ODL token... + private Authentication validate(final String token) { + Authentication auth = ServiceLocator.getInstance().getTokenStore().get(token); + if (auth == null) { + throw unauthorized(); + } else { + ServiceLocator.getInstance().getAuthenticationService().set(auth); + } + return auth; + } + + // Houston, we got a problem! + private static final WebApplicationException unauthorized() { + ServiceLocator.getInstance().getAuthenticationService().clear(); + return new UnauthorizedException(); + } + + // A custom 401 web exception that handles http basic response as well + static final class UnauthorizedException extends WebApplicationException { + private static final long serialVersionUID = -1732363804773027793L; + static final String WWW_AUTHENTICATE = "WWW-Authenticate"; + static final Object OPENDAYLIGHT = "Basic realm=\"opendaylight\""; + private static final Response response = Response.status(Status.UNAUTHORIZED) + .header(WWW_AUTHENTICATE, OPENDAYLIGHT) + .build(); + + public UnauthorizedException() { + super(response); + } + } +} diff --git a/odl-aaa-moon/aaa/aaa-authn-sts/src/main/java/org/opendaylight/aaa/sts/TokenEndpoint.java b/odl-aaa-moon/aaa/aaa-authn-sts/src/main/java/org/opendaylight/aaa/sts/TokenEndpoint.java new file mode 100644 index 00000000..a456d702 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn-sts/src/main/java/org/opendaylight/aaa/sts/TokenEndpoint.java @@ -0,0 +1,242 @@ +/* + * Copyright (c) 2014, 2015 Hewlett-Packard Development Company, L.P. and others. 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.sts; + +import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST; +import static javax.servlet.http.HttpServletResponse.SC_CREATED; +import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR; +import static javax.servlet.http.HttpServletResponse.SC_NOT_IMPLEMENTED; +import static javax.servlet.http.HttpServletResponse.SC_NO_CONTENT; +import static javax.servlet.http.HttpServletResponse.SC_OK; +import static javax.servlet.http.HttpServletResponse.SC_UNAUTHORIZED; + +import java.io.IOException; +import java.io.PrintWriter; +import java.util.List; +import javax.servlet.ServletConfig; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.apache.oltu.oauth2.as.issuer.OAuthIssuer; +import org.apache.oltu.oauth2.as.issuer.OAuthIssuerImpl; +import org.apache.oltu.oauth2.as.issuer.UUIDValueGenerator; +import org.apache.oltu.oauth2.as.response.OAuthASResponse; +import org.apache.oltu.oauth2.common.OAuth; +import org.apache.oltu.oauth2.common.exception.OAuthProblemException; +import org.apache.oltu.oauth2.common.exception.OAuthSystemException; +import org.apache.oltu.oauth2.common.message.OAuthResponse; +import org.apache.oltu.oauth2.common.message.types.GrantType; +import org.apache.oltu.oauth2.common.message.types.TokenType; +import org.opendaylight.aaa.AuthenticationBuilder; +import org.opendaylight.aaa.ClaimBuilder; +import org.opendaylight.aaa.PasswordCredentialBuilder; +import org.opendaylight.aaa.api.Authentication; +import org.opendaylight.aaa.api.AuthenticationException; +import org.opendaylight.aaa.api.Claim; +import org.opendaylight.aaa.api.PasswordCredentials; + +/** + * Secure Token Service (STS) endpoint. + * + * @author liemmn + * + */ +public class TokenEndpoint extends HttpServlet { + private static final long serialVersionUID = 8272453849539659999L; + + private static final String DOMAIN_SCOPE_REQUIRED = "Domain scope required"; + private static final String NOT_IMPLEMENTED = "not_implemented"; + private static final String UNAUTHORIZED = "unauthorized"; + + static final String TOKEN_GRANT_ENDPOINT = "/token"; + static final String TOKEN_REVOKE_ENDPOINT = "/revoke"; + static final String TOKEN_VALIDATE_ENDPOINT = "/validate"; + + private transient OAuthIssuer oi; + + @Override + public void init(ServletConfig config) throws ServletException { + oi = new OAuthIssuerImpl(new UUIDValueGenerator()); + } + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException { + try { + if (req.getServletPath().equals(TOKEN_GRANT_ENDPOINT)) { + createAccessToken(req, resp); + } else if (req.getServletPath().equals(TOKEN_REVOKE_ENDPOINT)) { + deleteAccessToken(req, resp); + } else if (req.getServletPath().equals(TOKEN_VALIDATE_ENDPOINT)) { + validateToken(req, resp); + } + } catch (AuthenticationException e) { + error(resp, SC_UNAUTHORIZED, e.getMessage()); + } catch (OAuthProblemException oe) { + error(resp, oe); + } catch (Exception e) { + error(resp, e); + } + } + + private void validateToken(HttpServletRequest req, HttpServletResponse resp) + throws IOException, OAuthSystemException { + String token = req.getReader().readLine(); + if (token != null) { + Authentication authn = ServiceLocator.getInstance().getTokenStore().get(token.trim()); + if (authn == null) { + throw new AuthenticationException(UNAUTHORIZED); + } else { + ServiceLocator.getInstance().getAuthenticationService().set(authn); + resp.setStatus(SC_OK); + } + } else { + throw new AuthenticationException(UNAUTHORIZED); + } + } + + // Delete an access token + private void deleteAccessToken(HttpServletRequest req, HttpServletResponse resp) + throws IOException { + String token = req.getReader().readLine(); + if (token != null) { + if (ServiceLocator.getInstance().getTokenStore().delete(token.trim())) { + resp.setStatus(SC_NO_CONTENT); + } else { + throw new AuthenticationException(UNAUTHORIZED); + } + } else { + throw new AuthenticationException(UNAUTHORIZED); + } + } + + // Create an access token + private void createAccessToken(HttpServletRequest req, HttpServletResponse resp) + throws OAuthSystemException, OAuthProblemException, IOException { + Claim claim = null; + String clientId = null; + + OAuthRequest oauthRequest = new OAuthRequest(req); + // Any client credentials? + clientId = oauthRequest.getClientId(); + if (clientId != null) { + ServiceLocator.getInstance().getClientService() + .validate(clientId, oauthRequest.getClientSecret()); + } + + // Credential request... + if (oauthRequest.getParam(OAuth.OAUTH_GRANT_TYPE).equals(GrantType.PASSWORD.toString())) { + String domain = oauthRequest.getScopes().iterator().next(); + PasswordCredentials pc = new PasswordCredentialBuilder().setUserName( + oauthRequest.getUsername()).setPassword(oauthRequest.getPassword()) + .setDomain(domain).build(); + if (!oauthRequest.getScopes().isEmpty()) { + claim = ServiceLocator.getInstance().getCredentialAuth().authenticate(pc); + } + } else if (oauthRequest.getParam(OAuth.OAUTH_GRANT_TYPE).equals( + GrantType.REFRESH_TOKEN.toString())) { + // Refresh token... + String token = oauthRequest.getRefreshToken(); + if (!oauthRequest.getScopes().isEmpty()) { + String domain = oauthRequest.getScopes().iterator().next(); + // Authenticate... + Authentication auth = ServiceLocator.getInstance().getTokenStore().get(token); + if (auth != null && domain != null) { + List<String> roles = ServiceLocator.getInstance().getIdmService() + .listRoles(auth.userId(), domain); + if (!roles.isEmpty()) { + ClaimBuilder cb = new ClaimBuilder(auth); + cb.setDomain(domain); // scope domain + // Add roles for the scoped domain + for (String role : roles) { + cb.addRole(role); + } + claim = cb.build(); + } + } + } else { + error(resp, SC_BAD_REQUEST, DOMAIN_SCOPE_REQUIRED); + } + } else { + // Support authorization code later... + error(resp, SC_NOT_IMPLEMENTED, NOT_IMPLEMENTED); + } + + // Respond with OAuth token + oauthAccessTokenResponse(resp, claim, clientId); + } + + // Build OAuth access token response from the given claim + private void oauthAccessTokenResponse(HttpServletResponse resp, Claim claim, String clientId) + throws OAuthSystemException, IOException { + if (claim == null) { + throw new AuthenticationException(UNAUTHORIZED); + } + String token = oi.accessToken(); + + // Cache this token... + Authentication auth = new AuthenticationBuilder(new ClaimBuilder(claim).setClientId( + clientId).build()).setExpiration(tokenExpiration()).build(); + ServiceLocator.getInstance().getTokenStore().put(token, auth); + + OAuthResponse r = OAuthASResponse.tokenResponse(SC_CREATED).setAccessToken(token) + .setTokenType(TokenType.BEARER.toString()) + .setExpiresIn(Long.toString(auth.expiration())) + .buildJSONMessage(); + write(resp, r); + } + + // Token expiration + private long tokenExpiration() { + return ServiceLocator.getInstance().getTokenStore().tokenExpiration(); + } + + // Emit an error OAuthResponse with the given HTTP code + private void error(HttpServletResponse resp, int httpCode, String error) { + try { + OAuthResponse r = OAuthResponse.errorResponse(httpCode).setError(error) + .buildJSONMessage(); + write(resp, r); + } catch (Exception e1) { + // Nothing to do here + } + } + + // Emit an error OAuthResponse for the given OAuth-related exception + private void error(HttpServletResponse resp, OAuthProblemException e) { + try { + OAuthResponse r = OAuthResponse.errorResponse(SC_BAD_REQUEST).error(e) + .buildJSONMessage(); + write(resp, r); + } catch (Exception e1) { + // Nothing to do here + } + } + + // Emit an error OAuthResponse for the given generic exception + private void error(HttpServletResponse resp, Exception e) { + try { + OAuthResponse r = OAuthResponse.errorResponse(SC_INTERNAL_SERVER_ERROR) + .setError(e.getClass().getName()) + .setErrorDescription(e.getMessage()).buildJSONMessage(); + write(resp, r); + } catch (Exception e1) { + // Nothing to do here + } + } + + // Write out an OAuthResponse + private void write(HttpServletResponse resp, OAuthResponse r) throws IOException { + resp.setStatus(r.getResponseStatus()); + PrintWriter pw = resp.getWriter(); + pw.print(r.getBody()); + pw.flush(); + pw.close(); + } +} diff --git a/odl-aaa-moon/aaa/aaa-authn-sts/src/main/resources/WEB-INF/web.xml b/odl-aaa-moon/aaa/aaa-authn-sts/src/main/resources/WEB-INF/web.xml new file mode 100644 index 00000000..83a9fa51 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn-sts/src/main/resources/WEB-INF/web.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="ISO-8859-1"?> +<web-app xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" + version="3.0"> + + <servlet> + <servlet-name>STS</servlet-name> + <servlet-class>org.opendaylight.aaa.sts.TokenEndpoint</servlet-class> + <load-on-startup>1</load-on-startup> + </servlet> + <servlet-mapping> + <servlet-name>STS</servlet-name> + <url-pattern>/token</url-pattern> + </servlet-mapping> + <servlet-mapping> + <servlet-name>STS</servlet-name> + <url-pattern>/revoke</url-pattern> + </servlet-mapping> + <servlet-mapping> + <servlet-name>STS</servlet-name> + <url-pattern>/validate</url-pattern> + </servlet-mapping> +</web-app> diff --git a/odl-aaa-moon/aaa/aaa-authn-sts/src/test/java/org/opendaylight/aaa/sts/RestFixture.java b/odl-aaa-moon/aaa/aaa-authn-sts/src/test/java/org/opendaylight/aaa/sts/RestFixture.java new file mode 100644 index 00000000..0f806d91 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn-sts/src/test/java/org/opendaylight/aaa/sts/RestFixture.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2014, 2015 Hewlett-Packard Development Company, L.P. and others. 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.sts; + +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Context; + +/** + * Fixture for testing RESTful stuff. + * + * @author liemmn + * + */ +@Path("test") +public class RestFixture { + + @Context + private HttpServletRequest httpRequest; + + @GET + @Produces("text/plain") + public String msg() { + return "ok"; + } +} diff --git a/odl-aaa-moon/aaa/aaa-authn-sts/src/test/java/org/opendaylight/aaa/sts/TokenAuthTest.java b/odl-aaa-moon/aaa/aaa-authn-sts/src/test/java/org/opendaylight/aaa/sts/TokenAuthTest.java new file mode 100644 index 00000000..7f888455 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn-sts/src/test/java/org/opendaylight/aaa/sts/TokenAuthTest.java @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2014, 2015 Hewlett-Packard Development Company, L.P. and others. 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.sts; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.Matchers.anyMap; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.sun.jersey.api.client.ClientResponse; +import com.sun.jersey.api.client.UniformInterfaceException; +import com.sun.jersey.test.framework.JerseyTest; +import com.sun.jersey.test.framework.WebAppDescriptor; +import org.junit.BeforeClass; +import org.junit.Test; +import org.opendaylight.aaa.AuthenticationBuilder; +import org.opendaylight.aaa.ClaimBuilder; +import org.opendaylight.aaa.api.Authentication; +import org.opendaylight.aaa.api.AuthenticationService; +import org.opendaylight.aaa.api.TokenAuth; +import org.opendaylight.aaa.api.TokenStore; +import org.opendaylight.aaa.sts.TokenAuthFilter.UnauthorizedException; + +public class TokenAuthTest extends JerseyTest { + + private static final String RS_PACKAGES = "org.opendaylight.aaa.sts"; + private static final String JERSEY_FILTERS = "com.sun.jersey.spi.container.ContainerRequestFilters"; + private static final String AUTH_FILTERS = TokenAuthFilter.class.getName(); + + private static Authentication auth = new AuthenticationBuilder(new ClaimBuilder().setUserId( + "1234").setUser("Bob").addRole("admin").addRole("user").setDomain("tenantX").build()).setExpiration( + System.currentTimeMillis() + 1000).build(); + + private static final String GOOD_TOKEN = "9b01b7cf-8a49-346d-8c47-6a61193e2b60"; + private static final String BAD_TOKEN = "9b01b7cf-8a49-346d-8c47-6a611badbeef"; + + public TokenAuthTest() throws Exception { + super(new WebAppDescriptor.Builder(RS_PACKAGES).initParam(JERSEY_FILTERS, AUTH_FILTERS) + .build()); + } + + @BeforeClass + public static void init() { + ServiceLocator.getInstance().setAuthenticationService(mock(AuthenticationService.class)); + ServiceLocator.getInstance().setTokenStore(mock(TokenStore.class)); + when(ServiceLocator.getInstance().getTokenStore().get(GOOD_TOKEN)).thenReturn(auth); + when(ServiceLocator.getInstance().getTokenStore().get(BAD_TOKEN)).thenReturn(null); + when(ServiceLocator.getInstance().getAuthenticationService().isAuthEnabled()).thenReturn( + Boolean.TRUE); + } + + @Test() + public void testGetUnauthorized() { + try { + resource().path("test").get(String.class); + fail("Shoulda failed with 401!"); + } catch (UniformInterfaceException e) { + ClientResponse resp = e.getResponse(); + assertEquals(401, resp.getStatus()); + assertTrue(resp.getHeaders().get(UnauthorizedException.WWW_AUTHENTICATE) + .contains(UnauthorizedException.OPENDAYLIGHT)); + } + } + + @Test + public void testGet() { + String resp = resource().path("test").header("Authorization", "Bearer " + GOOD_TOKEN) + .get(String.class); + assertEquals("ok", resp); + } + + @SuppressWarnings("unchecked") + @Test + public void testGetWithValidator() { + try { + // Mock a laxed tokenauth... + TokenAuth ta = mock(TokenAuth.class); + when(ta.validate(anyMap())).thenReturn(auth); + ServiceLocator.getInstance().getTokenAuthCollection().add(ta); + testGet(); + } finally { + ServiceLocator.getInstance().getTokenAuthCollection().clear(); + } + } + +} diff --git a/odl-aaa-moon/aaa/aaa-authn-sts/src/test/java/org/opendaylight/aaa/sts/TokenEndpointTest.java b/odl-aaa-moon/aaa/aaa-authn-sts/src/test/java/org/opendaylight/aaa/sts/TokenEndpointTest.java new file mode 100644 index 00000000..06dd6302 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn-sts/src/test/java/org/opendaylight/aaa/sts/TokenEndpointTest.java @@ -0,0 +1,164 @@ +/* + * Copyright (c) 2014, 2015 Hewlett-Packard Development Company, L.P. and others. 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.sts; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.Arrays; +import org.eclipse.jetty.testing.HttpTester; +import org.eclipse.jetty.testing.ServletTester; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.opendaylight.aaa.AuthenticationBuilder; +import org.opendaylight.aaa.ClaimBuilder; +import org.opendaylight.aaa.api.AuthenticationService; +import org.opendaylight.aaa.api.Claim; +import org.opendaylight.aaa.api.ClientService; +import org.opendaylight.aaa.api.CredentialAuth; +import org.opendaylight.aaa.api.IdMService; +import org.opendaylight.aaa.api.PasswordCredentials; +import org.opendaylight.aaa.api.TokenAuth; +import org.opendaylight.aaa.api.TokenStore; + +/** + * A unit test for token endpoint. + * + * @author liemmn + * + */ +public class TokenEndpointTest { + private static final long TOKEN_TIMEOUT_SECS = 10; + private static final String CONTEXT = "/oauth2"; + private static final String DIRECT_AUTH = "grant_type=password&username=admin&password=admin&scope=pepsi&client_id=dlux&client_secret=secrete"; + private static final String REFRESH_TOKEN = "grant_type=refresh_token&refresh_token=whateverisgood&scope=pepsi"; + + private static final Claim claim = new ClaimBuilder().setUser("bob").setUserId("1234") + .addRole("admin").build(); + private final static ServletTester server = new ServletTester(); + + @BeforeClass + public static void init() throws Exception { + // Set up server + server.setContextPath(CONTEXT); + + // Add our servlet under test + server.addServlet(TokenEndpoint.class, "/revoke"); + server.addServlet(TokenEndpoint.class, "/token"); + + // Let's do dis + server.start(); + } + + @AfterClass + public static void shutdown() throws Exception { + server.stop(); + } + + @Before + public void setup() { + mockServiceLocator(); + when(ServiceLocator.getInstance().getTokenStore().tokenExpiration()).thenReturn( + TOKEN_TIMEOUT_SECS); + } + + @After + public void teardown() { + ServiceLocator.getInstance().getTokenAuthCollection().clear(); + } + + @Test + public void testCreateToken401() throws Exception { + HttpTester req = new HttpTester(); + req.setMethod("POST"); + req.setHeader("Content-Type", "application/x-www-form-urlencoded"); + req.setContent(DIRECT_AUTH); + req.setURI(CONTEXT + TokenEndpoint.TOKEN_GRANT_ENDPOINT); + req.setVersion("HTTP/1.0"); + + HttpTester resp = new HttpTester(); + resp.parse(server.getResponses(req.generate())); + assertEquals(401, resp.getStatus()); + } + + @Test + public void testCreateTokenWithPassword() throws Exception { + when( + ServiceLocator.getInstance().getCredentialAuth() + .authenticate(any(PasswordCredentials.class))).thenReturn(claim); + + HttpTester req = new HttpTester(); + req.setMethod("POST"); + req.setHeader("Content-Type", "application/x-www-form-urlencoded"); + req.setContent(DIRECT_AUTH); + req.setURI(CONTEXT + TokenEndpoint.TOKEN_GRANT_ENDPOINT); + req.setVersion("HTTP/1.0"); + + HttpTester resp = new HttpTester(); + resp.parse(server.getResponses(req.generate())); + assertEquals(201, resp.getStatus()); + assertTrue(resp.getContent().contains("expires_in\":10")); + assertTrue(resp.getContent().contains("Bearer")); + } + + @Test + public void testCreateTokenWithRefreshToken() throws Exception { + when(ServiceLocator.getInstance().getTokenStore().get(anyString())).thenReturn( + new AuthenticationBuilder(claim).build()); + when(ServiceLocator.getInstance().getIdmService().listRoles(anyString(), anyString())).thenReturn( + Arrays.asList("admin", "user")); + + HttpTester req = new HttpTester(); + req.setMethod("POST"); + req.setHeader("Content-Type", "application/x-www-form-urlencoded"); + req.setContent(REFRESH_TOKEN); + req.setURI(CONTEXT + TokenEndpoint.TOKEN_GRANT_ENDPOINT); + req.setVersion("HTTP/1.0"); + + HttpTester resp = new HttpTester(); + resp.parse(server.getResponses(req.generate())); + assertEquals(201, resp.getStatus()); + assertTrue(resp.getContent().contains("expires_in\":10")); + assertTrue(resp.getContent().contains("Bearer")); + } + + @Test + public void testDeleteToken() throws Exception { + when(ServiceLocator.getInstance().getTokenStore().delete("token_to_be_deleted")).thenReturn( + true); + + HttpTester req = new HttpTester(); + req.setMethod("POST"); + req.setHeader("Content-Type", "application/x-www-form-urlencoded"); + req.setContent("token_to_be_deleted"); + req.setURI(CONTEXT + TokenEndpoint.TOKEN_REVOKE_ENDPOINT); + req.setVersion("HTTP/1.0"); + + HttpTester resp = new HttpTester(); + resp.parse(server.getResponses(req.generate())); + assertEquals(204, resp.getStatus()); + } + + @SuppressWarnings("unchecked") + private static void mockServiceLocator() { + ServiceLocator.getInstance().setClientService(mock(ClientService.class)); + ServiceLocator.getInstance().setIdmService(mock(IdMService.class)); + ServiceLocator.getInstance().setAuthenticationService(mock(AuthenticationService.class)); + ServiceLocator.getInstance().setTokenStore(mock(TokenStore.class)); + ServiceLocator.getInstance().setCredentialAuth(mock(CredentialAuth.class)); + ServiceLocator.getInstance().getTokenAuthCollection().add(mock(TokenAuth.class)); + } +} diff --git a/odl-aaa-moon/aaa/aaa-authn/pom.xml b/odl-aaa-moon/aaa/aaa-authn/pom.xml new file mode 100644 index 00000000..01f1c99c --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn/pom.xml @@ -0,0 +1,103 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Copyright (c) 2014-2015 Hewlett-Packard Development Company, L.P. and others. + 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 --> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + <parent> + <groupId>org.opendaylight.aaa</groupId> + <artifactId>aaa-parent</artifactId> + <version>0.3.2-Beryllium-SR2</version> + <relativePath>../parent</relativePath> + </parent> + + <artifactId>aaa-authn</artifactId> + <packaging>bundle</packaging> + + <dependencies> + <dependency> + <groupId>org.opendaylight.aaa</groupId> + <artifactId>aaa-authn-api</artifactId> + </dependency> + <dependency> + <groupId>com.google.guava</groupId> + <artifactId>guava</artifactId> + </dependency> + <dependency> + <groupId>org.slf4j</groupId> + <artifactId>slf4j-api</artifactId> + </dependency> + <dependency> + <groupId>com.sun.jersey</groupId> + <artifactId>jersey-server</artifactId> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>org.osgi</groupId> + <artifactId>org.osgi.core</artifactId> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>org.osgi</groupId> + <artifactId>org.osgi.compendium</artifactId> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>org.apache.felix</groupId> + <artifactId>org.apache.felix.dependencymanager</artifactId> + <scope>provided</scope> + </dependency> + <!-- Testing Dependencies --> + <dependency> + <groupId>junit</groupId> + <artifactId>junit</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.slf4j</groupId> + <artifactId>slf4j-simple</artifactId> + <scope>test</scope> + </dependency> + </dependencies> + + <build> + <plugins> + <plugin> + <groupId>org.apache.felix</groupId> + <artifactId>maven-bundle-plugin</artifactId> + <extensions>true</extensions> + <configuration> + <instructions> + <Bundle-Activator>org.opendaylight.aaa.Activator</Bundle-Activator> + </instructions> + <manifestLocation>${project.basedir}/META-INF</manifestLocation> + </configuration> + </plugin> + <plugin> + <groupId>org.codehaus.mojo</groupId> + <artifactId>build-helper-maven-plugin</artifactId> + <executions> + <execution> + <id>attach-artifacts</id> + <phase>package</phase> + <goals> + <goal>attach-artifact</goal> + </goals> + <configuration> + <artifacts> + <artifact> + <file>${project.build.directory}/classes/authn.cfg</file> + <type>cfg</type> + <classifier>config</classifier> + </artifact> + </artifacts> + </configuration> + </execution> + </executions> + </plugin> + </plugins> + </build> + +</project> diff --git a/odl-aaa-moon/aaa/aaa-authn/src/main/java/org/opendaylight/aaa/Activator.java b/odl-aaa-moon/aaa/aaa-authn/src/main/java/org/opendaylight/aaa/Activator.java new file mode 100644 index 00000000..cfe27ef0 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn/src/main/java/org/opendaylight/aaa/Activator.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2014 Hewlett-Packard Development Company, L.P. and others. 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; + +import java.util.Dictionary; +import org.apache.felix.dm.DependencyActivatorBase; +import org.apache.felix.dm.DependencyManager; +import org.opendaylight.aaa.api.AuthenticationService; +import org.opendaylight.aaa.api.ClientService; +import org.osgi.framework.BundleContext; +import org.osgi.framework.Constants; +import org.osgi.service.cm.ManagedService; + +/** + * Activator to register {@link AuthenticationService} with OSGi. + * + * @author liemmn + * + */ +public class Activator extends DependencyActivatorBase { + + private static final String AUTHN_PID = "org.opendaylight.aaa.authn"; + + @Override + public void init(BundleContext context, DependencyManager manager) throws Exception { + manager.add(createComponent().setInterface( + new String[] { AuthenticationService.class.getName() }, null).setImplementation( + AuthenticationManager.instance())); + + ClientManager cm = new ClientManager(); + manager.add(createComponent().setInterface(new String[] { ClientService.class.getName() }, + null).setImplementation(cm)); + context.registerService(ManagedService.class.getName(), cm, addPid(ClientManager.defaults)); + context.registerService(ManagedService.class.getName(), AuthenticationManager.instance(), + addPid(AuthenticationManager.defaults)); + } + + @Override + public void destroy(BundleContext context, DependencyManager manager) throws Exception { + } + + private Dictionary<String, ?> addPid(Dictionary<String, String> dict) { + dict.put(Constants.SERVICE_PID, AUTHN_PID); + return dict; + } +} diff --git a/odl-aaa-moon/aaa/aaa-authn/src/main/java/org/opendaylight/aaa/AuthenticationBuilder.java b/odl-aaa-moon/aaa/aaa-authn/src/main/java/org/opendaylight/aaa/AuthenticationBuilder.java new file mode 100644 index 00000000..948cbac6 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn/src/main/java/org/opendaylight/aaa/AuthenticationBuilder.java @@ -0,0 +1,122 @@ +/* + * Copyright (c) 2014, 2015 Hewlett-Packard Development Company, L.P. and others. 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; + +import static org.opendaylight.aaa.EqualUtil.areEqual; +import static org.opendaylight.aaa.HashCodeUtil.hash; + +import java.io.Serializable; +import java.util.Set; +import org.opendaylight.aaa.api.Authentication; +import org.opendaylight.aaa.api.Claim; + +/** + * A builder for the authentication context. + * + * The expiration defaults to 0. + * + * @author liemmn + * + */ +public class AuthenticationBuilder { + + private long expiration = 0L; + private Claim claim; + + public AuthenticationBuilder(Claim claim) { + this.claim = claim; + } + + public AuthenticationBuilder setExpiration(long expiration) { + this.expiration = expiration; + return this; + } + + public Authentication build() { + return new ImmutableAuthentication(this); + } + + private static final class ImmutableAuthentication implements Authentication, Serializable { + private static final long serialVersionUID = 4919078164955609987L; + private int hashCode = 0; + long expiration = 0L; + Claim claim; + + private ImmutableAuthentication(AuthenticationBuilder base) { + if (base.claim == null) { + throw new IllegalStateException("The Claim is null."); + } + claim = new ClaimBuilder(base.claim).build(); + expiration = base.expiration; + + if (base.expiration < 0) { + throw new IllegalStateException("The expiration is less than 0."); + } + } + + @Override + public long expiration() { + return expiration; + } + + @Override + public String clientId() { + return claim.clientId(); + } + + @Override + public String userId() { + return claim.userId(); + } + + @Override + public String user() { + return claim.user(); + } + + @Override + public String domain() { + return claim.domain(); + } + + @Override + public Set<String> roles() { + return claim.roles(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof Authentication)) { + return false; + } + Authentication a = (Authentication) o; + return areEqual(expiration, a.expiration()) && areEqual(claim.roles(), a.roles()) + && areEqual(claim.domain(), a.domain()) && areEqual(claim.userId(), a.userId()) + && areEqual(claim.user(), a.user()) && areEqual(claim.clientId(), a.clientId()); + } + + @Override + public int hashCode() { + if (hashCode == 0) { + int result = HashCodeUtil.SEED; + result = hash(result, expiration); + result = hash(result, claim.hashCode()); + hashCode = result; + } + return hashCode; + } + + @Override + public String toString() { + return "expiration:" + expiration + "," + claim.toString(); + } + } +}
\ No newline at end of file diff --git a/odl-aaa-moon/aaa/aaa-authn/src/main/java/org/opendaylight/aaa/AuthenticationManager.java b/odl-aaa-moon/aaa/aaa-authn/src/main/java/org/opendaylight/aaa/AuthenticationManager.java new file mode 100644 index 00000000..5f6420a3 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn/src/main/java/org/opendaylight/aaa/AuthenticationManager.java @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2014 Hewlett-Packard Development Company, L.P. and others. 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; + +import java.util.Dictionary; +import java.util.Hashtable; +import org.opendaylight.aaa.api.Authentication; +import org.opendaylight.aaa.api.AuthenticationService; +import org.osgi.service.cm.ConfigurationException; +import org.osgi.service.cm.ManagedService; + +/** + * An {@link InheritableThreadLocal}-based {@link AuthenticationService}. + * + * @author liemmn + */ +public class AuthenticationManager implements AuthenticationService, ManagedService { + private static final String AUTH_ENABLED_ERR = "Error setting authEnabled"; + + static final String AUTH_ENABLED = "authEnabled"; + static final Dictionary<String, String> defaults = new Hashtable<>(); + static { + defaults.put(AUTH_ENABLED, Boolean.FALSE.toString()); + } + + // In non-Karaf environments, authEnabled is set to false by default + private static volatile boolean authEnabled = false; + + private final static AuthenticationManager am = new AuthenticationManager(); + private final ThreadLocal<Authentication> auth = new InheritableThreadLocal<>(); + + private AuthenticationManager() { + } + + static AuthenticationManager instance() { + return am; + } + + @Override + public Authentication get() { + return auth.get(); + } + + @Override + public void set(Authentication a) { + auth.set(a); + } + + @Override + public void clear() { + auth.remove(); + } + + @Override + public boolean isAuthEnabled() { + return authEnabled; + } + + @Override + public void updated(Dictionary<String, ?> properties) throws ConfigurationException { + if (properties == null) { + return; + } + + String propertyValue = (String) properties.get(AUTH_ENABLED); + boolean isTrueString = Boolean.parseBoolean(propertyValue); + if (!isTrueString && !"false".equalsIgnoreCase(propertyValue)) { + throw new ConfigurationException(AUTH_ENABLED, AUTH_ENABLED_ERR); + } + authEnabled = isTrueString; + } +} diff --git a/odl-aaa-moon/aaa/aaa-authn/src/main/java/org/opendaylight/aaa/ClaimBuilder.java b/odl-aaa-moon/aaa/aaa-authn/src/main/java/org/opendaylight/aaa/ClaimBuilder.java new file mode 100644 index 00000000..4e4a8ef3 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn/src/main/java/org/opendaylight/aaa/ClaimBuilder.java @@ -0,0 +1,160 @@ +/* + * Copyright (c) 2014, 2015 Hewlett-Packard Development Company, L.P. and others. 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; + +import static org.opendaylight.aaa.EqualUtil.areEqual; +import static org.opendaylight.aaa.HashCodeUtil.hash; + +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableSet; +import java.io.Serializable; +import java.util.LinkedHashSet; +import java.util.Set; +import org.opendaylight.aaa.api.Claim; + +/** + * Builder for a {@link Claim}. The userId, user, and roles information is + * mandatory. + * + * @author liemmn + * + */ +public class ClaimBuilder { + private String userId = ""; + private String user = ""; + private Set<String> roles = new LinkedHashSet<>(); + private String clientId = ""; + private String domain = ""; + + public ClaimBuilder() { + } + + public ClaimBuilder(Claim claim) { + clientId = claim.clientId(); + userId = claim.userId(); + user = claim.user(); + domain = claim.domain(); + roles.addAll(claim.roles()); + } + + public ClaimBuilder setClientId(String clientId) { + this.clientId = Strings.nullToEmpty(clientId).trim(); + return this; + } + + public ClaimBuilder setUserId(String userId) { + this.userId = Strings.nullToEmpty(userId).trim(); + return this; + } + + public ClaimBuilder setUser(String userName) { + user = Strings.nullToEmpty(userName).trim(); + return this; + } + + public ClaimBuilder setDomain(String domain) { + this.domain = Strings.nullToEmpty(domain).trim(); + return this; + } + + public ClaimBuilder addRoles(Set<String> roles) { + for (String role : roles) { + addRole(role); + } + return this; + } + + public ClaimBuilder addRole(String role) { + roles.add(Strings.nullToEmpty(role).trim()); + return this; + } + + public Claim build() { + return new ImmutableClaim(this); + } + + protected static class ImmutableClaim implements Claim, Serializable { + private static final long serialVersionUID = -8115027645190209129L; + private int hashCode = 0; + protected String clientId; + protected String userId; + protected String user; + protected String domain; + protected ImmutableSet<String> roles; + + protected ImmutableClaim(ClaimBuilder base) { + clientId = base.clientId; + userId = base.userId; + user = base.user; + domain = base.domain; + roles = ImmutableSet.<String> builder().addAll(base.roles).build(); + + if (userId.isEmpty() || user.isEmpty() || roles.isEmpty() || roles.contains("")) { + throw new IllegalStateException( + "The Claim is missing one or more of the required fields."); + } + } + + @Override + public String clientId() { + return clientId; + } + + @Override + public String userId() { + return userId; + } + + @Override + public String user() { + return user; + } + + @Override + public String domain() { + return domain; + } + + @Override + public Set<String> roles() { + return roles; + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (!(o instanceof Claim)) + return false; + Claim a = (Claim) o; + return areEqual(roles, a.roles()) && areEqual(domain, a.domain()) + && areEqual(userId, a.userId()) && areEqual(user, a.user()) + && areEqual(clientId, a.clientId()); + } + + @Override + public int hashCode() { + if (hashCode == 0) { + int result = HashCodeUtil.SEED; + result = hash(result, clientId); + result = hash(result, userId); + result = hash(result, user); + result = hash(result, domain); + result = hash(result, roles); + hashCode = result; + } + return hashCode; + } + + @Override + public String toString() { + return "clientId:" + clientId + "," + "userId:" + userId + "," + "userName:" + user + + "," + "domain:" + domain + "," + "roles:" + roles; + } + } +} diff --git a/odl-aaa-moon/aaa/aaa-authn/src/main/java/org/opendaylight/aaa/ClientManager.java b/odl-aaa-moon/aaa/aaa-authn/src/main/java/org/opendaylight/aaa/ClientManager.java new file mode 100644 index 00000000..e7e51424 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn/src/main/java/org/opendaylight/aaa/ClientManager.java @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2014 Hewlett-Packard Development Company, L.P. and others. 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; + +import java.util.Dictionary; +import java.util.HashMap; +import java.util.Hashtable; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import org.apache.felix.dm.Component; +import org.opendaylight.aaa.api.AuthenticationException; +import org.opendaylight.aaa.api.ClientService; +import org.osgi.service.cm.ConfigurationException; +import org.osgi.service.cm.ManagedService; + +/** + * A configuration-based client manager. + * + * @author liemmn + * + */ +public class ClientManager implements ClientService, ManagedService { + static final String CLIENTS = "authorizedClients"; + private static final String CLIENTS_FORMAT_ERR = "Clients are space-delimited in the form of <client_id>:<client_secret>"; + private static final String UNAUTHORIZED_CLIENT_ERR = "Unauthorized client"; + + // Defaults (needed only for non-Karaf deployments) + static final Dictionary<String, String> defaults = new Hashtable<>(); + static { + defaults.put(CLIENTS, "dlux:secrete"); + } + + private final Map<String, String> clients = new ConcurrentHashMap<>(); + + // This should be a singleton + ClientManager() { + } + + // Called by DM when all required dependencies are satisfied. + void init(Component c) throws ConfigurationException { + reconfig(defaults); + } + + @Override + public void validate(String clientId, String clientSecret) throws AuthenticationException { + // TODO: Post-Helium, we will support a CRUD API + if (!clients.containsKey(clientId)) { + throw new AuthenticationException(UNAUTHORIZED_CLIENT_ERR); + } + if (!clients.get(clientId).equals(clientSecret)) { + throw new AuthenticationException(UNAUTHORIZED_CLIENT_ERR); + } + } + + @Override + public void updated(Dictionary<String, ?> props) throws ConfigurationException { + if (props == null) { + props = defaults; + } + reconfig(props); + } + + // Reconfigure the client map... + private void reconfig(@SuppressWarnings("rawtypes") Dictionary props) + throws ConfigurationException { + try { + String authorizedClients = (String) props.get(CLIENTS); + Map<String, String> newClients = new HashMap<>(); + if (authorizedClients != null) { + for (String client : authorizedClients.split(" ")) { + String[] aClient = client.split(":"); + newClients.put(aClient[0], aClient[1]); + } + } + clients.clear(); + clients.putAll(newClients); + } catch (Throwable t) { + throw new ConfigurationException(null, CLIENTS_FORMAT_ERR); + } + } + +} diff --git a/odl-aaa-moon/aaa/aaa-authn/src/main/java/org/opendaylight/aaa/EqualUtil.java b/odl-aaa-moon/aaa/aaa-authn/src/main/java/org/opendaylight/aaa/EqualUtil.java new file mode 100644 index 00000000..17204d0e --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn/src/main/java/org/opendaylight/aaa/EqualUtil.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2014 Hewlett-Packard Development Company, L.P. and others. 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; + +/** + * Simple class to aide in implementing equals. + * <p> + * + * <em>Arrays are not handled by this class</em>. This is because the + * <code>Arrays.equals</code> methods should be used for array fields. + */ +public final class EqualUtil { + static public boolean areEqual(boolean aThis, boolean aThat) { + return aThis == aThat; + } + + static public boolean areEqual(char aThis, char aThat) { + return aThis == aThat; + } + + static public boolean areEqual(long aThis, long aThat) { + return aThis == aThat; + } + + static public boolean areEqual(float aThis, float aThat) { + return Float.floatToIntBits(aThis) == Float.floatToIntBits(aThat); + } + + static public boolean areEqual(double aThis, double aThat) { + return Double.doubleToLongBits(aThis) == Double.doubleToLongBits(aThat); + } + + static public boolean areEqual(Object aThis, Object aThat) { + return aThis == null ? aThat == null : aThis.equals(aThat); + } +} diff --git a/odl-aaa-moon/aaa/aaa-authn/src/main/java/org/opendaylight/aaa/HashCodeUtil.java b/odl-aaa-moon/aaa/aaa-authn/src/main/java/org/opendaylight/aaa/HashCodeUtil.java new file mode 100644 index 00000000..c295b3ed --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn/src/main/java/org/opendaylight/aaa/HashCodeUtil.java @@ -0,0 +1,104 @@ +/***************************************************************************** + * Copyright (c) 2014 Hewlett-Packard Development Company, L.P. and others. 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; + +import java.lang.reflect.Array; + +/** + * Collected methods which allow easy implementation of <tt>hashCode</tt>. + * + * Example use case: + * + * <pre> + * public int hashCode() { + * int result = HashCodeUtil.SEED; + * // collect the contributions of various fields + * result = HashCodeUtil.hash(result, fPrimitive); + * result = HashCodeUtil.hash(result, fObject); + * result = HashCodeUtil.hash(result, fArray); + * return result; + * } + * </pre> + */ +public final class HashCodeUtil { + + /** + * An initial value for a <tt>hashCode</tt>, to which is added contributions + * from fields. Using a non-zero value decreases collisions of + * <tt>hashCode</tt> values. + */ + public static final int SEED = 23; + + /** booleans. */ + public static int hash(int aSeed, boolean aBoolean) { + return firstTerm(aSeed) + (aBoolean ? 1 : 0); + } + + /*** chars. */ + public static int hash(int aSeed, char aChar) { + return firstTerm(aSeed) + aChar; + } + + /** ints. */ + public static int hash(int aSeed, int aInt) { + return firstTerm(aSeed) + aInt; + } + + /** longs. */ + public static int hash(int aSeed, long aLong) { + return firstTerm(aSeed) + (int) (aLong ^ (aLong >>> 32)); + } + + /** floats. */ + public static int hash(int aSeed, float aFloat) { + return hash(aSeed, Float.floatToIntBits(aFloat)); + } + + /** doubles. */ + public static int hash(int aSeed, double aDouble) { + return hash(aSeed, Double.doubleToLongBits(aDouble)); + } + + /** + * <tt>aObject</tt> is a possibly-null object field, and possibly an array. + * + * If <tt>aObject</tt> is an array, then each element may be a primitive or + * a possibly-null object. + */ + public static int hash(int aSeed, Object aObject) { + int result = aSeed; + if (aObject == null) { + result = hash(result, 0); + } else if (!isArray(aObject)) { + result = hash(result, aObject.hashCode()); + } else { + int length = Array.getLength(aObject); + for (int idx = 0; idx < length; ++idx) { + Object item = Array.get(aObject, idx); + // if an item in the array references the array itself, prevent + // infinite looping + if (!(item == aObject)) { + result = hash(result, item); + } + } + } + return result; + } + + // PRIVATE + private static final int fODD_PRIME_NUMBER = 37; + + private static int firstTerm(int aSeed) { + return fODD_PRIME_NUMBER * aSeed; + } + + private static boolean isArray(Object aObject) { + return aObject.getClass().isArray(); + } +}
\ No newline at end of file diff --git a/odl-aaa-moon/aaa/aaa-authn/src/main/java/org/opendaylight/aaa/PasswordCredentialBuilder.java b/odl-aaa-moon/aaa/aaa-authn/src/main/java/org/opendaylight/aaa/PasswordCredentialBuilder.java new file mode 100644 index 00000000..d8a2e87a --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn/src/main/java/org/opendaylight/aaa/PasswordCredentialBuilder.java @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2014 Hewlett-Packard Development Company, L.P. and others. 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; + +import static org.opendaylight.aaa.EqualUtil.areEqual; +import static org.opendaylight.aaa.HashCodeUtil.hash; + +import org.opendaylight.aaa.api.PasswordCredentials; + +/** + * {@link PasswordCredentials} builder. + * + * @author liemmn + * + */ +public class PasswordCredentialBuilder { + private final MutablePasswordCredentials pc = new MutablePasswordCredentials(); + + public PasswordCredentialBuilder setUserName(String username) { + pc.username = username; + return this; + } + + public PasswordCredentialBuilder setPassword(String password) { + pc.password = password; + return this; + } + + public PasswordCredentialBuilder setDomain(String domain) { + pc.domain = domain; + return this; + } + + public PasswordCredentials build() { + return pc; + } + + private static class MutablePasswordCredentials implements PasswordCredentials { + private int hashCode = 0; + private String username; + private String password; + private String domain; + + @Override + public String username() { + return username; + } + + @Override + public String password() { + return password; + } + + @Override + public String domain() { + return domain; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof PasswordCredentials)) { + return false; + } + PasswordCredentials p = (PasswordCredentials) o; + return areEqual(username, p.username()) && areEqual(password, p.password()); + } + + @Override + public int hashCode() { + if (hashCode == 0) { + int result = HashCodeUtil.SEED; + result = hash(result, username); + result = hash(result, password); + hashCode = result; + } + return hashCode; + } + } +} diff --git a/odl-aaa-moon/aaa/aaa-authn/src/main/java/org/opendaylight/aaa/SecureBlockingQueue.java b/odl-aaa-moon/aaa/aaa-authn/src/main/java/org/opendaylight/aaa/SecureBlockingQueue.java new file mode 100644 index 00000000..3ded52da --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn/src/main/java/org/opendaylight/aaa/SecureBlockingQueue.java @@ -0,0 +1,258 @@ +/* + * Copyright (c) 2014 Hewlett-Packard Development Company, L.P. and others. 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; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Iterator; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.TimeUnit; +import org.opendaylight.aaa.api.Authentication; + +/** + * A {@link BlockingQueue} decorator with injected security context. + * + * @author liemmn + * + * @param <T> + * queue element type + */ +public class SecureBlockingQueue<T> implements BlockingQueue<T> { + private final BlockingQueue<SecureData<T>> queue; + + /** + * Constructor. + * + * @param queue + * blocking queue implementation to use + */ + public SecureBlockingQueue(BlockingQueue<SecureData<T>> queue) { + this.queue = queue; + } + + @Override + public T remove() { + return setAuth(queue.remove()); + } + + @Override + public T poll() { + return setAuth(queue.poll()); + } + + @Override + public T element() { + return setAuth(queue.element()); + } + + @Override + public T peek() { + return setAuth(queue.peek()); + } + + @Override + public int size() { + return queue.size(); + } + + @Override + public boolean isEmpty() { + return queue.isEmpty(); + } + + @Override + public Iterator<T> iterator() { + return new Iterator<T>() { + Iterator<SecureData<T>> it = queue.iterator(); + + @Override + public boolean hasNext() { + return it.hasNext(); + } + + @Override + public T next() { + return it.next().data; + } + + @Override + public void remove() { + it.remove(); + } + }; + } + + @Override + public Object[] toArray() { + return toData().toArray(); + } + + @SuppressWarnings("hiding") + @Override + public <T> T[] toArray(T[] a) { + return toData().toArray(a); + } + + @Override + public boolean containsAll(Collection<?> c) { + return toData().containsAll(c); + } + + @Override + public boolean addAll(Collection<? extends T> c) { + return queue.addAll(fromData(c)); + } + + @Override + public boolean removeAll(Collection<?> c) { + return queue.removeAll(fromData(c)); + } + + @Override + public boolean retainAll(Collection<?> c) { + return queue.retainAll(fromData(c)); + } + + @Override + public void clear() { + queue.clear(); + } + + @Override + public boolean add(T e) { + return queue.add(new SecureData<>(e)); + } + + @Override + public boolean offer(T e) { + return queue.offer(new SecureData<>(e)); + } + + @Override + public void put(T e) throws InterruptedException { + queue.put(new SecureData<T>(e)); + } + + @Override + public boolean offer(T e, long timeout, TimeUnit unit) throws InterruptedException { + return queue.offer(new SecureData<>(e), timeout, unit); + } + + @Override + public T take() throws InterruptedException { + return setAuth(queue.take()); + } + + @Override + public T poll(long timeout, TimeUnit unit) throws InterruptedException { + return setAuth(queue.poll(timeout, unit)); + } + + @Override + public int remainingCapacity() { + return queue.remainingCapacity(); + } + + @Override + public boolean remove(Object o) { + Iterator<SecureData<T>> it = queue.iterator(); + while (it.hasNext()) { + SecureData<T> sd = it.next(); + if (sd.data.equals(o)) { + return queue.remove(sd); + } + } + return false; + } + + @Override + public boolean contains(Object o) { + Iterator<SecureData<T>> it = queue.iterator(); + while (it.hasNext()) { + SecureData<T> sd = it.next(); + if (sd.data.equals(o)) { + return true; + } + } + return false; + } + + @Override + public int drainTo(Collection<? super T> c) { + Collection<SecureData<T>> sd = new ArrayList<>(); + int n = queue.drainTo(sd); + c.addAll(toData(sd)); + return n; + } + + @Override + public int drainTo(Collection<? super T> c, int maxElements) { + Collection<SecureData<T>> sd = new ArrayList<>(); + int n = queue.drainTo(sd, maxElements); + c.addAll(toData(sd)); + return n; + } + + // Rehydrate security context + private T setAuth(SecureData<T> i) { + AuthenticationManager.instance().set(i.auth); + return i.data; + } + + // Construct secure data collection from a plain old data collection + @SuppressWarnings("unchecked") + private Collection<SecureData<T>> fromData(Collection<?> c) { + Collection<SecureData<T>> sd = new ArrayList<>(c.size()); + for (Object d : c) { + sd.add((SecureData<T>) new SecureData<>(d)); + } + return sd; + } + + // Extract the data portion out from the secure data + @SuppressWarnings("unchecked") + private Collection<T> toData() { + return toData(Arrays.<SecureData<T>> asList(queue.toArray(new SecureData[0]))); + } + + // Extract the data portion out from the secure data + private Collection<T> toData(Collection<SecureData<T>> secureData) { + Collection<T> data = new ArrayList<>(secureData.size()); + Iterator<SecureData<T>> it = secureData.iterator(); + while (it.hasNext()) { + data.add(it.next().data); + } + return data; + } + + // Inject security context + public static final class SecureData<T> { + private final T data; + private final Authentication auth; + + private SecureData(T data) { + this.data = data; + this.auth = AuthenticationManager.instance().get(); + } + + @SuppressWarnings("rawtypes") + @Override + public boolean equals(Object o) { + if (o == null) { + return false; + } + return (o instanceof SecureData) ? data.equals(((SecureData) o).data) : false; + } + + @Override + public int hashCode() { + return data.hashCode(); + } + } +} diff --git a/odl-aaa-moon/aaa/aaa-authn/src/main/resources/OSGI-INF/metatype/metatype.properties b/odl-aaa-moon/aaa/aaa-authn/src/main/resources/OSGI-INF/metatype/metatype.properties new file mode 100644 index 00000000..75537f6b --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn/src/main/resources/OSGI-INF/metatype/metatype.properties @@ -0,0 +1,12 @@ +org.opendaylight.aaa.authn.name = Opendaylight AAA Authentication Configuration +org.opendaylight.aaa.authn.description = Configuration for AAA authorized clients +org.opendaylight.aaa.authn.authorizedClients.name = Authorized Clients +org.opendaylight.aaa.authn.authorizedClients.description = Space-delimited list of authorized \ + clients, with client id and client password separated by a ':'. \ + Example: dlux:secrete <client_id:client_secret> +org.opendaylight.aaa.authn.authEnabled.name = Enable authentication +org.opendaylight.aaa.authn.authEnabled.description = Enable authentication by setting it \ +to the value 'true', or 'false' if bypassing authentication. \ +Note that bypassing authentication may result in your controller being more \ +vulnerable to unauthorized accesses. Authorization, if enabled, will not work if \ +authentication is disabled.
\ No newline at end of file diff --git a/odl-aaa-moon/aaa/aaa-authn/src/main/resources/OSGI-INF/metatype/metatype.xml b/odl-aaa-moon/aaa/aaa-authn/src/main/resources/OSGI-INF/metatype/metatype.xml new file mode 100644 index 00000000..10150587 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn/src/main/resources/OSGI-INF/metatype/metatype.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<metatype:MetaData xmlns:metatype="http://www.osgi.org/xmlns/metatype/v1.0.0" + localization="OSGI-INF/metatype/metatype"> + <OCD id="org.opendaylight.aaa.authn" name="%org.opendaylight.aaa.authn.name" + description="%org.opendaylight.aaa.authn.description"> + <AD id="authorizedClients" type="String" default="dlux:secrete" + name="%org.opendaylight.aaa.authn.authorizedClients.name" + description="%org.opendaylight.aaa.authn.authorizedClients.description" /> + <AD id="authEnabled" type="String" default="true" + name="%org.opendaylight.aaa.authn.authEnabled.name" + description="%org.opendaylight.aaa.authn.authEnabled.description" /> + </OCD> + <Designate pid="org.opendaylight.aaa.authn"> + <Object ocdref="org.opendaylight.aaa.authn" /> + </Designate> +</metatype:MetaData>
\ No newline at end of file diff --git a/odl-aaa-moon/aaa/aaa-authn/src/main/resources/authn.cfg b/odl-aaa-moon/aaa/aaa-authn/src/main/resources/authn.cfg new file mode 100644 index 00000000..e7326f86 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn/src/main/resources/authn.cfg @@ -0,0 +1,2 @@ +authorizedClients=dlux:secrete +authEnabled=true
\ No newline at end of file diff --git a/odl-aaa-moon/aaa/aaa-authn/src/test/java/org/opendaylight/aaa/AuthenticationBuilderTest.java b/odl-aaa-moon/aaa/aaa-authn/src/test/java/org/opendaylight/aaa/AuthenticationBuilderTest.java new file mode 100644 index 00000000..2f69fe5b --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn/src/test/java/org/opendaylight/aaa/AuthenticationBuilderTest.java @@ -0,0 +1,129 @@ +/* + * Copyright (c) 2015 Hewlett-Packard Development Company, L.P. and others. 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; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertTrue; + +import java.util.Arrays; +import java.util.LinkedHashSet; +import java.util.Set; +import org.junit.Test; +import org.opendaylight.aaa.api.Authentication; +import org.opendaylight.aaa.api.Claim; + +public class AuthenticationBuilderTest { + private Set<String> roles = new LinkedHashSet<>(Arrays.asList("role1", "role2")); + private Claim validClaim = new ClaimBuilder().setDomain("aName").setUserId("1") + .setClientId("2222").setUser("bob").addRole("foo").addRoles(roles).build(); + + @Test + public void testBuildWithExpiration() { + Authentication a1 = new AuthenticationBuilder(validClaim).setExpiration(1).build(); + assertEquals(1, a1.expiration()); + assertEquals("aName", a1.domain()); + assertEquals("1", a1.userId()); + assertEquals("2222", a1.clientId()); + assertEquals("bob", a1.user()); + assertTrue(a1.roles().contains("foo")); + assertTrue(a1.roles().containsAll(roles)); + assertEquals(3, a1.roles().size()); + Authentication a2 = new AuthenticationBuilder(a1).build(); + assertNotEquals(a1, a2); + Authentication a3 = new AuthenticationBuilder(a1).setExpiration(1).build(); + assertEquals(a1, a3); + } + + @Test + public void testBuildWithoutExpiration() { + Authentication a1 = new AuthenticationBuilder(validClaim).build(); + assertEquals(0, a1.expiration()); + assertEquals("aName", a1.domain()); + assertEquals("1", a1.userId()); + assertEquals("2222", a1.clientId()); + assertEquals("bob", a1.user()); + assertTrue(a1.roles().contains("foo")); + assertTrue(a1.roles().containsAll(roles)); + assertEquals(3, a1.roles().size()); + } + + @Test(expected = IllegalStateException.class) + public void testBuildWithNegativeExpiration() { + AuthenticationBuilder a1 = new AuthenticationBuilder(validClaim).setExpiration(-1); + a1.build(); + } + + @Test(expected = IllegalStateException.class) + public void testBuildWithNullClaim() { + AuthenticationBuilder a1 = new AuthenticationBuilder(null); + a1.build(); + } + + @Test + public void testToString() { + Authentication a1 = new AuthenticationBuilder(validClaim).setExpiration(1).build(); + assertEquals( + "expiration:1,clientId:2222,userId:1,userName:bob,domain:aName,roles:[foo, role1, role2]", + a1.toString()); + } + + @Test + public void testEquals() { + Authentication a1 = new AuthenticationBuilder(validClaim).setExpiration(1).build(); + assertTrue(a1.equals(a1)); + Authentication a2 = new AuthenticationBuilder(a1).setExpiration(1).build(); + assertTrue(a1.equals(a2)); + assertTrue(a2.equals(a1)); + Authentication a3 = new AuthenticationBuilder(validClaim).setExpiration(1).build(); + assertTrue(a1.equals(a3)); + assertTrue(a3.equals(a2)); + assertTrue(a1.equals(a2)); + } + + @Test + public void testNotEquals() { + Authentication a1 = new AuthenticationBuilder(validClaim).setExpiration(1).build(); + assertFalse(a1.equals(null)); + assertFalse(a1.equals("wrong object")); + Authentication a2 = new AuthenticationBuilder(a1).build(); + assertFalse(a1.equals(a2)); + assertFalse(a2.equals(a1)); + Authentication a3 = new AuthenticationBuilder(validClaim).setExpiration(1).build(); + assertFalse(a1.equals(a2)); + assertTrue(a1.equals(a3)); + assertFalse(a2.equals(a3)); + Authentication a4 = new AuthenticationBuilder(validClaim).setExpiration(9).build(); + assertFalse(a1.equals(a4)); + assertFalse(a4.equals(a1)); + Authentication a5 = new AuthenticationBuilder(a1).setExpiration(9).build(); + assertFalse(a1.equals(a5)); + assertFalse(a5.equals(a1)); + } + + @Test + public void testHashCode() { + Authentication a1 = new AuthenticationBuilder(validClaim).setExpiration(1).build(); + assertEquals(a1.hashCode(), a1.hashCode()); + Authentication a2 = new AuthenticationBuilder(a1).setExpiration(1).build(); + assertTrue(a1.equals(a2)); + assertEquals(a1.hashCode(), a2.hashCode()); + Authentication a3 = new AuthenticationBuilder(validClaim).setExpiration(1).build(); + assertTrue(a1.equals(a3)); + assertEquals(a1.hashCode(), a3.hashCode()); + assertEquals(a2.hashCode(), a3.hashCode()); + Authentication a4 = new AuthenticationBuilder(a1).setExpiration(9).build(); + assertFalse(a1.equals(a4)); + assertNotEquals(a1.hashCode(), a4.hashCode()); + Authentication a5 = new AuthenticationBuilder(a1).build(); + assertFalse(a1.equals(a5)); + assertNotEquals(a1.hashCode(), a5.hashCode()); + } +} diff --git a/odl-aaa-moon/aaa/aaa-authn/src/test/java/org/opendaylight/aaa/AuthenticationManagerTest.java b/odl-aaa-moon/aaa/aaa-authn/src/test/java/org/opendaylight/aaa/AuthenticationManagerTest.java new file mode 100644 index 00000000..540df287 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn/src/test/java/org/opendaylight/aaa/AuthenticationManagerTest.java @@ -0,0 +1,133 @@ +/* + * Copyright (c) 2014, 2015 Hewlett-Packard Development Company, L.P. and others. 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; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import java.util.Arrays; +import java.util.Dictionary; +import java.util.Hashtable; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import org.junit.Test; +import org.opendaylight.aaa.api.Authentication; +import org.opendaylight.aaa.api.AuthenticationService; +import org.osgi.service.cm.ConfigurationException; + +public class AuthenticationManagerTest { + @Test + public void testAuthenticationCrudSameThread() { + Authentication auth = new AuthenticationBuilder(new ClaimBuilder().setUser("Bob") + .setUserId("1234").addRole("admin").addRole("guest").build()).build(); + AuthenticationService as = AuthenticationManager.instance(); + + assertNotNull(as); + + as.set(auth); + assertEquals(auth, as.get()); + + as.clear(); + assertNull(as.get()); + } + + @Test + public void testAuthenticationCrudSpawnedThread() throws InterruptedException, + ExecutionException { + AuthenticationService as = AuthenticationManager.instance(); + Authentication auth = new AuthenticationBuilder(new ClaimBuilder().setUser("Bob") + .setUserId("1234").addRole("admin").addRole("guest").build()).build(); + + as.set(auth); + Future<Authentication> f = Executors.newSingleThreadExecutor().submit(new Worker()); + assertEquals(auth, f.get()); + + as.clear(); + f = Executors.newSingleThreadExecutor().submit(new Worker()); + assertNull(f.get()); + } + + @Test + public void testAuthenticationCrudSpawnedThreadPool() throws InterruptedException, + ExecutionException { + AuthenticationService as = AuthenticationManager.instance(); + Authentication auth = new AuthenticationBuilder(new ClaimBuilder().setUser("Bob") + .setUserId("1234").addRole("admin").addRole("guest").build()).build(); + + as.set(auth); + List<Future<Authentication>> fs = Executors.newFixedThreadPool(2).invokeAll( + Arrays.asList(new Worker(), new Worker())); + for (Future<Authentication> f : fs) { + assertEquals(auth, f.get()); + } + + as.clear(); + fs = Executors.newFixedThreadPool(2).invokeAll(Arrays.asList(new Worker(), new Worker())); + for (Future<Authentication> f : fs) { + assertNull(f.get()); + } + } + + @Test + public void testUpdatedValid() throws ConfigurationException { + Dictionary<String, String> props = new Hashtable<>(); + AuthenticationManager as = AuthenticationManager.instance(); + + assertFalse(as.isAuthEnabled()); + + props.put(AuthenticationManager.AUTH_ENABLED, "TrUe"); + as.updated(props); + assertTrue(as.isAuthEnabled()); + + props.put(AuthenticationManager.AUTH_ENABLED, "FaLsE"); + as.updated(props); + assertFalse(as.isAuthEnabled()); + } + + @Test + public void testUpdatedNullProperty() throws ConfigurationException { + AuthenticationManager as = AuthenticationManager.instance(); + + assertFalse(as.isAuthEnabled()); + as.updated(null); + assertFalse(as.isAuthEnabled()); + } + + @Test(expected = ConfigurationException.class) + public void testUpdatedInvalidValue() throws ConfigurationException { + AuthenticationManager as = AuthenticationManager.instance(); + Dictionary<String, String> props = new Hashtable<>(); + + props.put(AuthenticationManager.AUTH_ENABLED, "yes"); + as.updated(props); + } + + @Test(expected = ConfigurationException.class) + public void testUpdatedInvalidKey() throws ConfigurationException { + AuthenticationManager as = AuthenticationManager.instance(); + Dictionary<String, String> props = new Hashtable<>(); + + props.put("Invalid Key", "true"); + as.updated(props); + } + + private class Worker implements Callable<Authentication> { + @Override + public Authentication call() throws Exception { + AuthenticationService as = AuthenticationManager.instance(); + return as.get(); + } + } +}
\ No newline at end of file diff --git a/odl-aaa-moon/aaa/aaa-authn/src/test/java/org/opendaylight/aaa/ClaimBuilderTest.java b/odl-aaa-moon/aaa/aaa-authn/src/test/java/org/opendaylight/aaa/ClaimBuilderTest.java new file mode 100644 index 00000000..372eb6d2 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn/src/test/java/org/opendaylight/aaa/ClaimBuilderTest.java @@ -0,0 +1,208 @@ +/* + * Copyright (c) 2014, 2015 Hewlett-Packard Development Company, L.P. and others. 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; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertTrue; + +import java.util.Arrays; +import java.util.HashSet; +import org.junit.Test; +import org.opendaylight.aaa.api.Claim; + +/** + * + * @author liemmn + * + */ +public class ClaimBuilderTest { + @Test + public void testBuildWithAll() { + Claim c1 = new ClaimBuilder().setClientId("dlux").setDomain("pepsi").setUser("john") + .setUserId("1234").addRole("foo").addRole("foo2") + .addRoles(new HashSet<>(Arrays.asList("foo", "bar"))).build(); + assertEquals("dlux", c1.clientId()); + assertEquals("pepsi", c1.domain()); + assertEquals("john", c1.user()); + assertEquals("1234", c1.userId()); + assertTrue(c1.roles().contains("foo")); + assertTrue(c1.roles().contains("foo2")); + assertTrue(c1.roles().contains("bar")); + assertEquals(3, c1.roles().size()); + Claim c2 = new ClaimBuilder(c1).build(); + assertEquals(c1, c2); + } + + @Test + public void testBuildWithRequired() { + Claim c1 = new ClaimBuilder().setUser("john").setUserId("1234").addRole("foo").build(); + assertEquals("john", c1.user()); + assertEquals("1234", c1.userId()); + assertTrue(c1.roles().contains("foo")); + assertEquals(1, c1.roles().size()); + assertEquals("", c1.domain()); + assertEquals("", c1.clientId()); + } + + @Test + public void testBuildWithEmptyOptional() { + Claim c1 = new ClaimBuilder().setDomain(" ").setClientId(" ").setUser("john") + .setUserId("1234").addRole("foo").build(); + assertEquals("", c1.domain()); + assertEquals("", c1.clientId()); + assertEquals("john", c1.user()); + assertEquals("1234", c1.userId()); + assertTrue(c1.roles().contains("foo")); + assertEquals(1, c1.roles().size()); + } + + @Test + public void testBuildWithNullOptional() { + Claim c1 = new ClaimBuilder().setDomain(null).setClientId(null).setUser("john") + .setUserId("1234").addRole("foo").build(); + assertEquals("", c1.domain()); + assertEquals("", c1.clientId()); + assertEquals("john", c1.user()); + assertEquals("1234", c1.userId()); + assertTrue(c1.roles().contains("foo")); + assertEquals(1, c1.roles().size()); + } + + @Test(expected = IllegalStateException.class) + public void testBuildWithDefault() { + ClaimBuilder c1 = new ClaimBuilder(); + c1.build(); + } + + @Test(expected = IllegalStateException.class) + public void testBuildWithoutUser() { + ClaimBuilder c1 = new ClaimBuilder().setUserId("1234").addRole("foo"); + c1.build(); + } + + @Test(expected = IllegalStateException.class) + public void testBuildWithNullUser() { + ClaimBuilder c1 = new ClaimBuilder().setUser(null).setUserId("1234").addRole("foo"); + c1.build(); + } + + @Test(expected = IllegalStateException.class) + public void testBuildWithEmptyUser() { + ClaimBuilder c1 = new ClaimBuilder().setUser(" ").setUserId("1234").addRole("foo"); + c1.build(); + } + + @Test(expected = IllegalStateException.class) + public void testBuildWithoutUserId() { + ClaimBuilder c1 = new ClaimBuilder().setUser("john").addRole("foo"); + c1.build(); + } + + @Test(expected = IllegalStateException.class) + public void testBuildWithNullUserId() { + ClaimBuilder c1 = new ClaimBuilder().setUser("john").setUserId(null).addRole("foo"); + c1.build(); + } + + @Test(expected = IllegalStateException.class) + public void testBuildWithEmptyUserId() { + ClaimBuilder c1 = new ClaimBuilder().setUser("john").setUserId(" ").addRole("foo"); + c1.build(); + } + + @Test(expected = IllegalStateException.class) + public void testBuildWithoutRole() { + ClaimBuilder c1 = new ClaimBuilder().setUser("john").setUserId("1234"); + c1.build(); + } + + @Test(expected = IllegalStateException.class) + public void testBuildWithNullRole() { + ClaimBuilder c1 = new ClaimBuilder().setUser("john").setUserId("1234").addRole(null); + c1.build(); + } + + @Test(expected = IllegalStateException.class) + public void testBuildWithEmptyRole() { + ClaimBuilder c1 = new ClaimBuilder().setUser("john").setUserId("1234").addRole(" "); + c1.build(); + } + + @Test + public void testEquals() { + Claim c1 = new ClaimBuilder().setClientId("dlux").setDomain("pepsi").setUser("john") + .setUserId("1234").addRole("foo").build(); + assertTrue(c1.equals(c1)); + Claim c2 = new ClaimBuilder(c1).addRole("foo").build(); + assertTrue(c1.equals(c2)); + assertTrue(c2.equals(c1)); + Claim c3 = new ClaimBuilder().setClientId("dlux").setDomain("pepsi").setUser("john") + .setUserId("1234").addRole("foo").build(); + assertTrue(c1.equals(c3)); + assertTrue(c3.equals(c2)); + assertTrue(c1.equals(c2)); + } + + @Test + public void testNotEquals() { + Claim c1 = new ClaimBuilder().setClientId("dlux").setDomain("pepsi").setUser("john") + .setUserId("1234").addRole("foo").build(); + assertFalse(c1.equals(null)); + assertFalse(c1.equals("wrong object")); + Claim c2 = new ClaimBuilder(c1).addRoles(new HashSet<>(Arrays.asList("foo", "bar"))) + .build(); + assertEquals(2, c2.roles().size()); + assertFalse(c1.equals(c2)); + assertFalse(c2.equals(c1)); + Claim c3 = new ClaimBuilder().setClientId("dlux").setDomain("pepsi").setUser("john") + .setUserId("1234").addRole("foo").build(); + assertFalse(c1.equals(c2)); + assertTrue(c1.equals(c3)); + assertFalse(c2.equals(c3)); + Claim c5 = new ClaimBuilder().setUser("john").setUserId("1234").addRole("foo").build(); + assertFalse(c1.equals(c5)); + assertFalse(c5.equals(c1)); + } + + @Test + public void testHash() { + Claim c1 = new ClaimBuilder().setClientId("dlux").setDomain("pepsi").setUser("john") + .setUserId("1234").addRole("foo").build(); + assertEquals(c1.hashCode(), c1.hashCode()); + Claim c2 = new ClaimBuilder(c1).addRole("foo").build(); + assertTrue(c1.equals(c2)); + assertEquals(c1.hashCode(), c2.hashCode()); + Claim c3 = new ClaimBuilder(c1).addRoles(new HashSet<>(Arrays.asList("foo", "bar"))) + .build(); + assertFalse(c1.equals(c3)); + assertNotEquals(c1.hashCode(), c3.hashCode()); + Claim c4 = new ClaimBuilder().setClientId("dlux").setDomain("pepsi").setUser("john") + .setUserId("1234").addRole("foo").build(); + assertTrue(c1.equals(c4)); + assertEquals(c1.hashCode(), c4.hashCode()); + assertEquals(c2.hashCode(), c4.hashCode()); + Claim c5 = new ClaimBuilder().setUser("john").setUserId("1234").addRole("foo").build(); + assertFalse(c1.equals(c5)); + assertNotEquals(c1.hashCode(), c5.hashCode()); + } + + @Test + public void testToString() { + Claim c1 = new ClaimBuilder().setUser("john").setUserId("1234").addRole("foo").build(); + assertEquals("clientId:,userId:1234,userName:john,domain:,roles:[foo]", c1.toString()); + c1 = new ClaimBuilder(c1).setClientId("dlux").setDomain("pepsi").build(); + assertEquals("clientId:dlux,userId:1234,userName:john,domain:pepsi,roles:[foo]", + c1.toString()); + c1 = new ClaimBuilder(c1).addRole("bar").build(); + assertEquals("clientId:dlux,userId:1234,userName:john,domain:pepsi,roles:[foo, bar]", + c1.toString()); + } +}
\ No newline at end of file diff --git a/odl-aaa-moon/aaa/aaa-authn/src/test/java/org/opendaylight/aaa/ClientManagerTest.java b/odl-aaa-moon/aaa/aaa-authn/src/test/java/org/opendaylight/aaa/ClientManagerTest.java new file mode 100644 index 00000000..059ba9a3 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn/src/test/java/org/opendaylight/aaa/ClientManagerTest.java @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2014, 2015 Hewlett-Packard Development Company, L.P. and others. 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; + +import static org.junit.Assert.fail; + +import java.util.Dictionary; +import java.util.Hashtable; +import org.junit.Before; +import org.junit.Test; +import org.opendaylight.aaa.api.AuthenticationException; +import org.osgi.service.cm.ConfigurationException; + +/** + * + * @author liemmn + * + */ +public class ClientManagerTest { + private static final ClientManager cm = new ClientManager(); + + @Before + public void setup() throws ConfigurationException { + cm.init(null); + } + + @Test + public void testValidate() { + cm.validate("dlux", "secrete"); + } + + @Test(expected = AuthenticationException.class) + public void testFailValidate() { + cm.validate("dlux", "what?"); + } + + @Test + public void testUpdate() throws ConfigurationException { + Dictionary<String, String> configs = new Hashtable<>(); + configs.put(ClientManager.CLIENTS, "aws:amazon dlux:xxx"); + cm.updated(configs); + cm.validate("aws", "amazon"); + cm.validate("dlux", "xxx"); + } + + @Test + public void testFailUpdate() { + Dictionary<String, String> configs = new Hashtable<>(); + configs.put(ClientManager.CLIENTS, "aws:amazon dlux"); + try { + cm.updated(configs); + fail("Shoulda failed updating bad configuration"); + } catch (ConfigurationException ce) { + // Expected + } + cm.validate("dlux", "secrete"); + try { + cm.validate("aws", "amazon"); + fail("Shoulda failed updating bad configuration"); + } catch (AuthenticationException ae) { + // Expected + } + } +} diff --git a/odl-aaa-moon/aaa/aaa-authn/src/test/java/org/opendaylight/aaa/PasswordCredentialTest.java b/odl-aaa-moon/aaa/aaa-authn/src/test/java/org/opendaylight/aaa/PasswordCredentialTest.java new file mode 100644 index 00000000..2dabb77b --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn/src/test/java/org/opendaylight/aaa/PasswordCredentialTest.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2014, 2015 Hewlett-Packard Development Company, L.P. and others. 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; + +import static org.junit.Assert.assertEquals; + +import java.util.HashSet; +import org.junit.Test; +import org.opendaylight.aaa.api.PasswordCredentials; + +public class PasswordCredentialTest { + + @Test + public void testBuilder() { + PasswordCredentials pc1 = new PasswordCredentialBuilder().setUserName("bob") + .setPassword("secrete").build(); + assertEquals("bob", pc1.username()); + assertEquals("secrete", pc1.password()); + + PasswordCredentials pc2 = new PasswordCredentialBuilder().setUserName("bob") + .setPassword("secrete").build(); + assertEquals(pc1, pc2); + + PasswordCredentials pc3 = new PasswordCredentialBuilder().setUserName("bob") + .setPassword("secret").build(); + HashSet<PasswordCredentials> pcs = new HashSet<>(); + pcs.add(pc1); + pcs.add(pc2); + pcs.add(pc3); + assertEquals(2, pcs.size()); + } + +} diff --git a/odl-aaa-moon/aaa/aaa-authn/src/test/java/org/opendaylight/aaa/SecureBlockingQueueTest.java b/odl-aaa-moon/aaa/aaa-authn/src/test/java/org/opendaylight/aaa/SecureBlockingQueueTest.java new file mode 100644 index 00000000..16627d9f --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authn/src/test/java/org/opendaylight/aaa/SecureBlockingQueueTest.java @@ -0,0 +1,191 @@ +/* + * Copyright (c) 2014, 2015 Hewlett-Packard Development Company, L.P. and others. 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; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import java.util.Arrays; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import org.junit.Before; +import org.junit.Test; +import org.opendaylight.aaa.SecureBlockingQueue.SecureData; +import org.opendaylight.aaa.api.Authentication; + +public class SecureBlockingQueueTest { + private final int MAX_TASKS = 100; + + @Before + public void setup() { + AuthenticationManager.instance().clear(); + } + + @Test + public void testSecureThreadPoolExecutor() throws InterruptedException, ExecutionException { + BlockingQueue<Runnable> queue = new SecureBlockingQueue<>( + new ArrayBlockingQueue<SecureData<Runnable>>(10)); + ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 10, 500, TimeUnit.MILLISECONDS, + queue); + executor.prestartAllCoreThreads(); + for (int cnt = 1; cnt <= MAX_TASKS; cnt++) { + assertEquals(Integer.toString(cnt), + executor.submit(new Task(Integer.toString(cnt), "1111", "user")).get().user()); + } + executor.shutdown(); + } + + @Test + public void testNormalThreadPoolExecutor() throws InterruptedException, ExecutionException { + BlockingQueue<Runnable> queue = new ArrayBlockingQueue<Runnable>(10); + ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 10, 500, TimeUnit.MILLISECONDS, + queue); + executor.prestartAllCoreThreads(); + for (int cnt = 1; cnt <= MAX_TASKS; cnt++) { + assertNull(executor.submit(new Task(Integer.toString(cnt), "1111", "user")).get()); + } + executor.shutdown(); + } + + @Test + public void testQueueOps() throws InterruptedException, ExecutionException { + BlockingQueue<String> queue = new SecureBlockingQueue<>( + new ArrayBlockingQueue<SecureData<String>>(3)); + ExecutorService es = Executors.newFixedThreadPool(3); + es.submit(new Producer("foo", "1111", "user", queue)).get(); + assertEquals(1, queue.size()); + assertEquals("foo", es.submit(new Consumer(queue)).get()); + es.submit(new Producer("bar", "2222", "user", queue)).get(); + assertEquals("bar", queue.peek()); + assertEquals("bar", queue.element()); + assertEquals(1, queue.size()); + assertEquals("bar", queue.poll()); + assertTrue(queue.isEmpty()); + es.shutdown(); + } + + @Test + public void testCollectionOps() throws InterruptedException, ExecutionException { + BlockingQueue<String> queue = new SecureBlockingQueue<>( + new ArrayBlockingQueue<SecureData<String>>(6)); + for (int i = 1; i <= 3; i++) + queue.add("User" + i); + Iterator<String> it = queue.iterator(); + while (it.hasNext()) + assertTrue(it.next().startsWith("User")); + assertEquals(3, queue.toArray().length); + List<String> actual = Arrays.asList(queue.toArray(new String[0])); + assertEquals("User1", actual.iterator().next()); + assertTrue(queue.containsAll(actual)); + queue.addAll(actual); + assertEquals(6, queue.size()); + queue.retainAll(Arrays.asList(new String[] { "User2" })); + assertEquals(2, queue.size()); + assertEquals("User2", queue.iterator().next()); + queue.removeAll(actual); + assertTrue(queue.isEmpty()); + queue.add("hello"); + assertEquals(1, queue.size()); + queue.clear(); + assertTrue(queue.isEmpty()); + } + + @Test + public void testBlockingQueueOps() throws InterruptedException { + BlockingQueue<String> queue = new SecureBlockingQueue<>( + new ArrayBlockingQueue<SecureData<String>>(3)); + queue.offer("foo"); + assertEquals(1, queue.size()); + queue.offer("bar", 500, TimeUnit.MILLISECONDS); + assertEquals(2, queue.size()); + assertEquals("foo", queue.poll()); + assertTrue(queue.contains("bar")); + queue.remove("bar"); + assertEquals(3, queue.remainingCapacity()); + queue.addAll(Arrays.asList(new String[] { "foo", "bar", "tom" })); + assertEquals(3, queue.size()); + assertEquals("foo", queue.poll(500, TimeUnit.MILLISECONDS)); + assertEquals(2, queue.size()); + List<String> drain = new LinkedList<>(); + queue.drainTo(drain); + assertTrue(queue.isEmpty()); + assertEquals(2, drain.size()); + queue.addAll(Arrays.asList(new String[] { "foo", "bar", "tom" })); + drain.clear(); + queue.drainTo(drain, 1); + assertEquals(2, queue.size()); + assertEquals(1, drain.size()); + } + + // Task to run in a ThreadPoolExecutor + private class Task implements Callable<Authentication> { + Task(String name, String userId, String role) { + // Mock that each task has its original authentication context + AuthenticationManager.instance().set( + new AuthenticationBuilder(new ClaimBuilder().setUser(name).setUserId(userId) + .addRole(role).build()).build()); + } + + @Override + public Authentication call() throws Exception { + return AuthenticationManager.instance().get(); + } + } + + // Producer sets auth context + private class Producer implements Callable<String> { + private final String name; + private final String userId; + private final String role; + private final BlockingQueue<String> queue; + + Producer(String name, String userId, String role, BlockingQueue<String> queue) { + this.name = name; + this.userId = userId; + this.role = role; + this.queue = queue; + } + + @Override + public String call() throws InterruptedException { + AuthenticationManager.instance().set( + new AuthenticationBuilder(new ClaimBuilder().setUser(name).setUserId(userId) + .addRole(role).build()).build()); + queue.put(name); + return name; + } + } + + // Consumer gets producer's auth context via data element in queue + private class Consumer implements Callable<String> { + private final BlockingQueue<String> queue; + + Consumer(BlockingQueue<String> queue) { + this.queue = queue; + } + + @Override + public String call() { + queue.remove(); + Authentication auth = AuthenticationManager.instance().get(); + return (auth == null) ? null : auth.user(); + } + } + +} diff --git a/odl-aaa-moon/aaa/aaa-authz/aaa-authz-config/pom.xml b/odl-aaa-moon/aaa/aaa-authz/aaa-authz-config/pom.xml new file mode 100644 index 00000000..42237e41 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authz/aaa-authz-config/pom.xml @@ -0,0 +1,43 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + <parent> + <groupId>org.opendaylight.aaa</groupId> + <artifactId>aaa-parent</artifactId> + <version>0.3.2-Beryllium-SR2</version> + <relativePath>../../parent</relativePath> + </parent> + + <artifactId>authz-service-config</artifactId> + <description>AuthZ Service Configuration files </description> + <packaging>jar</packaging> + <build> + <plugins> + <plugin> + <groupId>org.codehaus.mojo</groupId> + <artifactId>build-helper-maven-plugin</artifactId> + <executions> + <execution> + <id>attach-artifacts</id> + <goals> + <goal>attach-artifact</goal> + </goals> + <phase>package</phase> + <configuration> + <artifacts> + <artifact> + <file>${project.build.directory}/classes/initial/${config.authz.service.configfile}</file> + <type>xml</type> + <classifier>config</classifier> + </artifact> + </artifacts> + </configuration> + </execution> + </executions> + </plugin> + </plugins> + </build> + +</project> diff --git a/odl-aaa-moon/aaa/aaa-authz/aaa-authz-config/src/main/resources/initial/08-authz-config.xml b/odl-aaa-moon/aaa/aaa-authz/aaa-authz-config/src/main/resources/initial/08-authz-config.xml new file mode 100644 index 00000000..5b59ca20 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authz/aaa-authz-config/src/main/resources/initial/08-authz-config.xml @@ -0,0 +1,60 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- vi: set et smarttab sw=4 tabstop=4: --> +<!-- + Copyright (c) 2013 Cisco Systems, Inc. and others. 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 +--> +<snapshot> + <configuration> + <data xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"> + <modules xmlns="urn:opendaylight:params:xml:ns:yang:controller:config"> + + <!-- defines an implementation module --> + <module> + <type xmlns:authz="urn:opendaylight:params:xml:ns:yang:controller:config:aaa-authz:srv">authz:aaa-authz-service</type> + <name>aaa-authz-service</name> + + <dom-broker> + <type xmlns:dom="urn:opendaylight:params:xml:ns:yang:controller:md:sal:dom">dom:dom-broker-osgi-registry</type> + <name>dom-broker</name> + </dom-broker> + + <data-broker> + <type xmlns:binding="urn:opendaylight:params:xml:ns:yang:controller:md:sal:binding">binding:binding-data-broker</type> + <name>binding-data-broker</name> + </data-broker> + + <policies xmlns="urn:opendaylight:params:xml:ns:yang:controller:config:aaa-authz:srv"> + <service xmlns="urn:opendaylight:params:xml:ns:yang:controller:config:aaa-authz:srv">RestConfService</service> + <action xmlns="urn:opendaylight:params:xml:ns:yang:controller:config:aaa-authz:srv">Any</action> + <resource xmlns="urn:opendaylight:params:xml:ns:yang:controller:config:aaa-authz:srv">*</resource> + <role xmlns="urn:opendaylight:params:xml:ns:yang:controller:config:aaa-authz:srv">admin</role> + </policies> + + </module> + </modules> + + <services xmlns="urn:opendaylight:params:xml:ns:yang:controller:config"> + <service> + <type xmlns:dom="urn:opendaylight:params:xml:ns:yang:controller:md:sal:dom">dom:dom-broker-osgi-registry</type> + <instance> + <name>authz-connector-default</name> + <provider> + /modules/module[type='aaa-authz-service'][name='aaa-authz-service'] + </provider> + </instance> + </service> + </services> + + </data> + + + </configuration> + <required-capabilities> + <capability>urn:opendaylight:params:xml:ns:yang:controller:config:aaa-authz:srv?module=aaa-authz-service-impl&revision=2014-07-01</capability> + </required-capabilities> + +</snapshot> diff --git a/odl-aaa-moon/aaa/aaa-authz/aaa-authz-model/pom.xml b/odl-aaa-moon/aaa/aaa-authz/aaa-authz-model/pom.xml new file mode 100644 index 00000000..ee6108bd --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authz/aaa-authz-model/pom.xml @@ -0,0 +1,95 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + + <parent> + <groupId>org.opendaylight.aaa</groupId> + <artifactId>aaa-parent</artifactId> + <version>0.3.2-Beryllium-SR2</version> + <relativePath>../../parent</relativePath> + </parent> + + <artifactId>aaa-authz-model</artifactId> + <name>${project.artifactId}</name> + + <dependencies> + <dependency> + <groupId>org.opendaylight.mdsal</groupId> + <artifactId>yang-binding</artifactId> + </dependency> + <dependency> + <groupId>org.opendaylight.mdsal.model</groupId> + <artifactId>ietf-inet-types</artifactId> + </dependency> + <dependency> + <groupId>org.opendaylight.mdsal.model</groupId> + <artifactId>ietf-yang-types</artifactId> + </dependency> + <dependency> + <groupId>org.opendaylight.mdsal.model</groupId> + <artifactId>yang-ext</artifactId> + </dependency> + </dependencies> + + <build> + <plugins> + <plugin> + <groupId>org.apache.felix</groupId> + <artifactId>maven-bundle-plugin</artifactId> + <extensions>true</extensions> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-javadoc-plugin</artifactId> + <configuration> + <stylesheet>maven</stylesheet> + </configuration> + <executions> + <execution> + <goals> + <goal>aggregate</goal> + </goals> + <phase>site</phase> + </execution> + </executions> + </plugin> + <plugin> + <groupId>org.opendaylight.yangtools</groupId> + <artifactId>yang-maven-plugin</artifactId> + <version>${yangtools.version}</version> + <executions> + <execution> + <goals> + <goal>generate-sources</goal> + </goals> + <configuration> + <yangFilesRootDir>src/main/yang</yangFilesRootDir> + <codeGenerators> + <generator> + <codeGeneratorClass> + org.opendaylight.yangtools.maven.sal.api.gen.plugin.CodeGeneratorImpl + </codeGeneratorClass> + <outputBaseDir>${salGeneratorPath}</outputBaseDir> + </generator> + </codeGenerators> + <inspectDependencies>true</inspectDependencies> + </configuration> + </execution> + </executions> + + <dependencies> + <dependency> + <groupId>org.opendaylight.mdsal</groupId> + <artifactId>maven-sal-api-gen-plugin</artifactId> + <version>${yangtools.version}</version> + <type>jar</type> + </dependency> + </dependencies> + </plugin> + </plugins> + </build> + <packaging>bundle</packaging> + +</project> diff --git a/odl-aaa-moon/aaa/aaa-authz/aaa-authz-model/src/main/yang/authorization-schema.yang b/odl-aaa-moon/aaa/aaa-authz/aaa-authz-model/src/main/yang/authorization-schema.yang new file mode 100644 index 00000000..2e0cf9cb --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authz/aaa-authz-model/src/main/yang/authorization-schema.yang @@ -0,0 +1,190 @@ +module authorization-schema { + yang-version 1; + namespace "urn:aaa:yang:authz:ds"; + prefix "authz"; + organization "TBD"; + + contact "wdec@cisco.com"; + + revision 2014-07-22 { + description + "Initial revision."; + } + + //Main module begins + + //TODO: Refactor service type as URI + + //Define the servicetype; Service is used to identify the requestors' name, which would correspond to an ODL component eg Restconf. Possibly + //the naming will derive from the OSGi bundle name of the AuthZ requesting party. + + typedef service-type { + type string; + } + + //Resource denotes the actual resource that is the subject of the AuthZ request. + + typedef resource-type { + type string; + default "*"; + + //Examples of resources: + //Data : /operational/opendaylight-inventory:nodes/node/openflow:1/node-connector/openflow:1:1 + //Wildcarded data: /operational/opendaylight-inventory:nodes/node/*/node-connector/* + //RPC: /operations/example-ops:reboot + //Wildcarded RPC: /operations/example-ops:* + //Notification: /notifications/example-ops:startup + } + + //Role denotes the normalized role that is attributed to the AuthZ requestor, eg "admin" + + typedef role-type { + type string; + } + + //Domain denotes the customer domain that is the attributed of the AuthZ requestor, eg cisco.com + + typedef domain-type { + type string; + } + + //Action denotes the requested AuthZ action on the resource + //TODO: Refactor as identities to allow for augmentation. + + typedef action-type { + type enumeration { + enum put; + enum commit; + enum exists; + enum getIdentifier; + enum read; + enum cancel; + enum submit; + enum delete; + enum merge; + enum any; + } + default "any"; + } + + typedef authorization-response-type { + type enumeration { + enum not-authorized { value 0; } + enum authorized { value 1; } + } + } + + typedef authorization-duration-type { + type uint32; + } + + // Following grouping is the core AuthZ policy permissions data-structure, dual keyed by service and action. + // Permissions will be set-up per application. NOTE: Group and role can be equivalent. do we need both? + + grouping authorization-grp { + list policies { + key "service"; + leaf service { + type service-type; + } + leaf action { + type action-type; + } + leaf resource { + type resource-type; + mandatory true; + } + leaf role { + type role-type; + mandatory true; + } + leaf authorization { + type authorization-response-type; + } + } + } + + // Following container provides the simple, non-domain specific AuthZ policy data-structure, dual keyed by service and action. + + container simple-authorization { + uses authorization-grp; + } + + // Following container provides the domain AuthZ policy data-structure. Each Policy is extended with a authz-domain-chain, + // which contains a prioritized list of the leafrefs to additional domain policies that also apply to this domain. + // The construct allows the chaining of policies like foo.com -> customer.sp.com -> customer.carrier.com. + + + container domain-authorization { + list domains { + key "domain-name"; + leaf domain-name { + type domain-type; + } + uses authorization-grp; + list authz-domain-chain { + key "priority"; + leaf priority { + type uint32; + } + leaf domain-name { + type leafref { + path "/additional-domain-authz/domains/domain-name"; + } + } + } + } +} + +container additional-domain-authz { + list domains { + key "domain-name"; + leaf domain-name { + type domain-type; + } + uses authorization-grp; + } + } + + + + /* The following is the AuthZ RPC definition */ + + rpc req-authorization { + description + "Check Authorization for a given combination of action and role. + A not-authorized will be returned if unsuccessful."; + + input { + leaf domain-name { + type domain-type; + } + leaf service { + type service-type; + } + leaf action { + type action-type; + mandatory true; + } + + leaf resource { + type resource-type; + mandatory true; + } + leaf role { + type role-type; + mandatory true; + } + + } + + output { + + leaf authorization-response { + type authorization-response-type; + mandatory true; + } + + } + } +} diff --git a/odl-aaa-moon/aaa/aaa-authz/aaa-authz-restconf-config/pom.xml b/odl-aaa-moon/aaa/aaa-authz/aaa-authz-restconf-config/pom.xml new file mode 100644 index 00000000..6104be4b --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authz/aaa-authz-restconf-config/pom.xml @@ -0,0 +1,43 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + <parent> + <groupId>org.opendaylight.aaa</groupId> + <artifactId>aaa-parent</artifactId> + <version>0.3.2-Beryllium-SR2</version> + <relativePath>../../parent</relativePath> + </parent> + + <artifactId>authz-restconf-config</artifactId> + + <description>AuthZ Restconf Connector Configuration file </description> + <packaging>jar</packaging> + <build> + <plugins> + <plugin> + <groupId>org.codehaus.mojo</groupId> + <artifactId>build-helper-maven-plugin</artifactId> + <executions> + <execution> + <id>attach-artifacts</id> + <goals> + <goal>attach-artifact</goal> + </goals> + <phase>package</phase> + <configuration> + <artifacts> + <artifact> + <file>${project.build.directory}/classes/initial/${config.restconf.configfile}</file> + <type>xml</type> + <classifier>config</classifier> + </artifact> + </artifacts> + </configuration> + </execution> + </executions> + </plugin> + </plugins> + </build> +</project> diff --git a/odl-aaa-moon/aaa/aaa-authz/aaa-authz-restconf-config/src/main/resources/initial/09-rest-connector.xml b/odl-aaa-moon/aaa/aaa-authz/aaa-authz-restconf-config/src/main/resources/initial/09-rest-connector.xml new file mode 100644 index 00000000..deba6558 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authz/aaa-authz-restconf-config/src/main/resources/initial/09-rest-connector.xml @@ -0,0 +1,42 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +Copyright (c) 2014 Cisco Systems, Inc. and others. 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 +--> +<snapshot> + <configuration> + <data xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"> + <modules xmlns="urn:opendaylight:params:xml:ns:yang:controller:config"> + + <module> + <type xmlns:rest="urn:opendaylight:params:xml:ns:yang:controller:md:sal:rest:connector">rest:rest-connector-impl</type> + <name>rest-connector-default-impl</name> + <websocket-port>8185</websocket-port> + <dom-broker> + <type xmlns:dom="urn:opendaylight:params:xml:ns:yang:controller:md:sal:dom">dom:dom-broker-osgi-registry</type> + <name>authz-connector-default</name> + </dom-broker> + </module> + </modules> + + <services xmlns="urn:opendaylight:params:xml:ns:yang:controller:config"> + <service> + <type xmlns:rest="urn:opendaylight:params:xml:ns:yang:controller:md:sal:rest:connector">rest:rest-connector</type> + <instance> + <name>rest-connector-default</name> + <provider> + /modules/module[type='rest-connector-impl'][name='rest-connector-default-impl'] + </provider> + </instance> + </service> + </services> + + </data> + </configuration> + <required-capabilities> + <capability>urn:opendaylight:params:xml:ns:yang:controller:md:sal:rest:connector?module=opendaylight-rest-connector&revision=2014-07-24</capability> + </required-capabilities> +</snapshot> diff --git a/odl-aaa-moon/aaa/aaa-authz/aaa-authz-service/pom.xml b/odl-aaa-moon/aaa/aaa-authz/aaa-authz-service/pom.xml new file mode 100644 index 00000000..2c150ce7 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authz/aaa-authz-service/pom.xml @@ -0,0 +1,152 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- ~ Copyright (c) 2014 Cisco Systems, Inc. and others. 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 --> + +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <parent> + <groupId>org.opendaylight.aaa</groupId> + <artifactId>aaa-parent</artifactId> + <version>0.3.2-Beryllium-SR2</version> + <relativePath>../../parent</relativePath> + </parent> + <modelVersion>4.0.0</modelVersion> + + <artifactId>aaa-authz-service</artifactId> + <packaging>bundle</packaging> + + <dependencies> + <dependency> + <groupId>org.opendaylight.controller</groupId> + <artifactId>sal-binding-util</artifactId> + </dependency> + <dependency> + <groupId>org.opendaylight.controller</groupId> + <artifactId>sal-common-util</artifactId> + </dependency> + <dependency> + <groupId>org.opendaylight.yangtools</groupId> + <artifactId>yang-data-api</artifactId> + </dependency> + <dependency> + <groupId>commons-codec</groupId> + <artifactId>commons-codec</artifactId> + </dependency> + <dependency> + <groupId>org.opendaylight.controller</groupId> + <artifactId>sal-binding-api</artifactId> + </dependency> + <dependency> + <groupId>org.opendaylight.controller</groupId> + <artifactId>config-api</artifactId> + </dependency> + <dependency> + <groupId>org.opendaylight.controller</groupId> + <artifactId>sal-binding-config</artifactId> + </dependency> + <dependency> + <groupId>org.opendaylight.aaa</groupId> + <artifactId>aaa-authz-model</artifactId> + </dependency> + <dependency> + <groupId>org.opendaylight.aaa</groupId> + <artifactId>aaa-authn-api</artifactId> + </dependency> + <dependency> + <groupId>org.opendaylight.controller</groupId> + <artifactId>sal-core-api</artifactId> + </dependency> + <dependency> + <groupId>org.opendaylight.controller</groupId> + <artifactId>sal-core-spi</artifactId> + </dependency> + <dependency> + <groupId>org.jboss.resteasy</groupId> + <artifactId>jaxrs-api</artifactId> + <scope>provided</scope> + </dependency> + + <!-- Test dependencies --> + <dependency> + <groupId>junit</groupId> + <artifactId>junit</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.mockito</groupId> + <artifactId>mockito-all</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.slf4j</groupId> + <artifactId>slf4j-simple</artifactId> + <scope>test</scope> + </dependency> + + </dependencies> + + <build> + <plugins> + <plugin> + <groupId>org.apache.felix</groupId> + <artifactId>maven-bundle-plugin</artifactId> + <extensions>true</extensions> + <configuration> + <instructions> + <!-- <Bundle-Activator>org.opendaylight.aaa.authz.srv.AuthzProvider</Bundle-Activator> --> + <Export-Package>org.opendaylight.aaa.config.yang.aaa_srv,</Export-Package> + </instructions> + </configuration> + <!-- <configuration> <Export-Package> </Export-Package> </configuration> --> + </plugin> + <plugin> + <groupId>org.opendaylight.yangtools</groupId> + <artifactId>yang-maven-plugin</artifactId> + <version>${yangtools.version}</version> + <executions> + <execution> + <id>config</id> + <goals> + <goal>generate-sources</goal> + </goals> + <configuration> + <codeGenerators> + <generator> + <codeGeneratorClass> + org.opendaylight.controller.config.yangjmxgenerator.plugin.JMXGenerator + </codeGeneratorClass> + <outputBaseDir>${jmxGeneratorPath}</outputBaseDir> + <additionalConfiguration> + <namespaceToPackage1> + urn:opendaylight:params:xml:ns:yang:controller==org.opendaylight.controller.config.yang + </namespaceToPackage1> + </additionalConfiguration> + </generator> + <generator> + <codeGeneratorClass>org.opendaylight.yangtools.maven.sal.api.gen.plugin.CodeGeneratorImpl</codeGeneratorClass> + <outputBaseDir>${salGeneratorPath}</outputBaseDir> + </generator> + </codeGenerators> + <inspectDependencies>true</inspectDependencies> + </configuration> + </execution> + </executions> + <dependencies> + <dependency> + <groupId>org.opendaylight.controller</groupId> + <artifactId>yang-jmx-generator-plugin</artifactId> + <version>${config.version}</version> + </dependency> + <dependency> + <groupId>org.opendaylight.mdsal</groupId> + <artifactId>maven-sal-api-gen-plugin</artifactId> + <version>${yangtools.version}</version> + </dependency> + </dependencies> + </plugin> + </plugins> + </build> + +</project> diff --git a/odl-aaa-moon/aaa/aaa-authz/aaa-authz-service/src/main/java/org/opendaylight/aaa/authz/srv/AuthzBrokerImpl.java b/odl-aaa-moon/aaa/aaa-authz/aaa-authz-service/src/main/java/org/opendaylight/aaa/authz/srv/AuthzBrokerImpl.java new file mode 100644 index 00000000..d4ac79af --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authz/aaa-authz-service/src/main/java/org/opendaylight/aaa/authz/srv/AuthzBrokerImpl.java @@ -0,0 +1,150 @@ +/* + * Copyright (c) 2014 Cisco Systems, Inc. and others. 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.authz.srv; + +import java.util.Collection; + +import org.opendaylight.aaa.api.AuthenticationService; +import org.opendaylight.controller.md.sal.dom.api.DOMDataBroker; +import org.opendaylight.controller.sal.core.api.Broker; +import org.opendaylight.controller.sal.core.api.Consumer; +import org.opendaylight.controller.sal.core.api.Provider; +import org.osgi.framework.BundleContext; + +/** + * Created by wdec on 26/08/2014. + */ +public class AuthzBrokerImpl implements Broker, AutoCloseable, Provider { + + private Broker broker; + private ProviderSession providerSession; + private AuthenticationService authenticationService; + + public void setBroker(Broker broker) { + this.broker = broker; + } + + @Override + public void close() throws Exception { + + } + + // Implements AuthzBroker handling of registering consumers or providers. + @Override + public ConsumerSession registerConsumer(Consumer consumer) { + + ConsumerSession realSession = broker.registerConsumer(new ConsumerWrapper(consumer)); + AuthzConsumerContextImpl authzConsumerContext = new AuthzConsumerContextImpl(realSession, + this); + consumer.onSessionInitiated(authzConsumerContext); + return authzConsumerContext; + } + + @Override + public ConsumerSession registerConsumer(Consumer consumer, BundleContext bundleContext) { + + ConsumerSession realSession = broker.registerConsumer(new ConsumerWrapper(consumer), + bundleContext); + AuthzConsumerContextImpl authzConsumerContext = new AuthzConsumerContextImpl(realSession, + this); + consumer.onSessionInitiated(authzConsumerContext); + return authzConsumerContext; + } + + @Override + public ProviderSession registerProvider(Provider provider) { + + ProviderSession realSession = broker.registerProvider(new ProviderWrapper(provider)); + AuthzProviderContextImpl authzProviderContext = new AuthzProviderContextImpl(realSession, + this); + provider.onSessionInitiated(authzProviderContext); + return authzProviderContext; + } + + @Override + public ProviderSession registerProvider(Provider provider, BundleContext bundleContext) { + + // Allow the real broker to do its thing, while providing a wrapped + // callback + ProviderSession realSession = broker.registerProvider(new ProviderWrapper(provider), + bundleContext); + + // Create Authz ProviderContext + AuthzProviderContextImpl authzProviderContext = new AuthzProviderContextImpl(realSession, + this); + + // Run onsessionInitiated on injected provider with the AuthZ provider + // context. + provider.onSessionInitiated(authzProviderContext); + return authzProviderContext; + + } + + // Handle the AuthZBroker registration with the real broker + @Override + public void onSessionInitiated(ProviderSession providerSession) { + + // Get now the real DOMDataBroker and register it with the + // AuthzDOMBroker together with the provider session + final DOMDataBroker domDataBroker = providerSession.getService(DOMDataBroker.class); + AuthzDomDataBroker.getInstance().setProviderSession(providerSession); + AuthzDomDataBroker.getInstance().setDomDataBroker(domDataBroker); + AuthzDomDataBroker.getInstance().setAuthService(this.authenticationService); + } + + @Override + public Collection<ProviderFunctionality> getProviderFunctionality() { + return null; + } + + public void setAuthenticationService(AuthenticationService authenticationService) { + this.authenticationService = authenticationService; + } + + // Wrapper for Provider + + public static class ProviderWrapper implements Provider { + private final Provider provider; + + public ProviderWrapper(Provider provider) { + this.provider = provider; + } + + @Override + public void onSessionInitiated(ProviderSession providerSession) { + // Do a Noop when the real broker calls back + } + + @Override + public Collection<ProviderFunctionality> getProviderFunctionality() { + // Allow the RestconfImpl to respond to this + return provider.getProviderFunctionality(); + } + } + + // Wrapper for Consumer + public static class ConsumerWrapper implements Consumer { + + private final Consumer consumer; + + public ConsumerWrapper(Consumer consumer) { + this.consumer = consumer; + } + + @Override + public void onSessionInitiated(ConsumerSession consumerSession) { + // Do a Noop when the real broker calls back + } + + @Override + public Collection<ConsumerFunctionality> getConsumerFunctionality() { + return consumer.getConsumerFunctionality(); + } + } +} diff --git a/odl-aaa-moon/aaa/aaa-authz/aaa-authz-service/src/main/java/org/opendaylight/aaa/authz/srv/AuthzConsumerContextImpl.java b/odl-aaa-moon/aaa/aaa-authz/aaa-authz-service/src/main/java/org/opendaylight/aaa/authz/srv/AuthzConsumerContextImpl.java new file mode 100644 index 00000000..07ba51cd --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authz/aaa-authz-service/src/main/java/org/opendaylight/aaa/authz/srv/AuthzConsumerContextImpl.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2014 Cisco Systems, Inc. and others. 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.authz.srv; + +import org.opendaylight.controller.md.sal.dom.api.DOMDataBroker; +import org.opendaylight.controller.sal.core.api.Broker; +import org.opendaylight.controller.sal.core.api.Broker.ConsumerSession; +import org.opendaylight.controller.sal.core.api.BrokerService; +import org.opendaylight.controller.sal.core.spi.ForwardingConsumerSession; + +/** + * Created by wdec on 28/08/2014. + */ +public class AuthzConsumerContextImpl extends ForwardingConsumerSession { + + private final Broker.ConsumerSession realSession; + + public AuthzConsumerContextImpl(Broker.ConsumerSession realSession, AuthzBrokerImpl authzBroker) { + this.realSession = realSession; + } + + @Override + protected ConsumerSession delegate() { + return realSession; + } + + @Override + public <T extends BrokerService> T getService(Class<T> tClass) { + T t; + // Check for class and return Authz broker only for DOMBroker + if (tClass == DOMDataBroker.class) { + t = (T) AuthzDomDataBroker.getInstance(); + } else { + t = realSession.getService(tClass); + } + // AuthzDomDataBroker.getInstance().setDomDataBroker((DOMDataBroker)t); + return t; + } + +} diff --git a/odl-aaa-moon/aaa/aaa-authz/aaa-authz-service/src/main/java/org/opendaylight/aaa/authz/srv/AuthzDataReadWriteTransaction.java b/odl-aaa-moon/aaa/aaa-authz/aaa-authz-service/src/main/java/org/opendaylight/aaa/authz/srv/AuthzDataReadWriteTransaction.java new file mode 100644 index 00000000..4cc232bc --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authz/aaa-authz-service/src/main/java/org/opendaylight/aaa/authz/srv/AuthzDataReadWriteTransaction.java @@ -0,0 +1,129 @@ +/* + * Copyright (c) 2014 Cisco Systems, Inc. and others. 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.authz.srv; + +import com.google.common.base.Optional; +import com.google.common.util.concurrent.CheckedFuture; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; + +import org.opendaylight.controller.md.sal.common.api.TransactionStatus; +import org.opendaylight.controller.md.sal.common.api.data.LogicalDatastoreType; +import org.opendaylight.controller.md.sal.common.api.data.ReadFailedException; +import org.opendaylight.controller.md.sal.common.api.data.TransactionCommitFailedException; +import org.opendaylight.controller.md.sal.dom.api.DOMDataReadWriteTransaction; +import org.opendaylight.yang.gen.v1.urn.aaa.yang.authz.ds.rev140722.ActionType; +import org.opendaylight.yangtools.yang.common.RpcResult; +import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier; +import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode; + +/** + * Created by wdec on 26/08/2014. + */ +public class AuthzDataReadWriteTransaction implements DOMDataReadWriteTransaction { + + private final DOMDataReadWriteTransaction domDataReadWriteTransaction; + + public AuthzDataReadWriteTransaction(DOMDataReadWriteTransaction domDataReadWriteTransaction) { + this.domDataReadWriteTransaction = domDataReadWriteTransaction; + } + + @Override + public boolean cancel() { + if (AuthzServiceImpl.isAuthorized(ActionType.Cancel)) { + return domDataReadWriteTransaction.cancel(); + } + return false; + } + + @Override + public void delete(LogicalDatastoreType logicalDatastoreType, + YangInstanceIdentifier yangInstanceIdentifier) { + + if (AuthzServiceImpl.isAuthorized(logicalDatastoreType, yangInstanceIdentifier, + ActionType.Delete)) { + domDataReadWriteTransaction.delete(logicalDatastoreType, yangInstanceIdentifier); + } + } + + @Override + public CheckedFuture<Void, TransactionCommitFailedException> submit() { + if (AuthzServiceImpl.isAuthorized(ActionType.Submit)) { + return domDataReadWriteTransaction.submit(); + } + TransactionCommitFailedException e = new TransactionCommitFailedException( + "Unauthorized User"); + return Futures.immediateFailedCheckedFuture(e); + } + + @Deprecated + @Override + public ListenableFuture<RpcResult<TransactionStatus>> commit() { + if (AuthzServiceImpl.isAuthorized(ActionType.Commit)) { + return domDataReadWriteTransaction.commit(); + } + TransactionCommitFailedException e = new TransactionCommitFailedException( + "Unauthorized User"); + return Futures.immediateFailedCheckedFuture(e); + } + + @Override + public CheckedFuture<Optional<NormalizedNode<?, ?>>, ReadFailedException> read( + LogicalDatastoreType logicalDatastoreType, YangInstanceIdentifier yangInstanceIdentifier) { + + if (AuthzServiceImpl.isAuthorized(logicalDatastoreType, yangInstanceIdentifier, + ActionType.Read)) { + return domDataReadWriteTransaction.read(logicalDatastoreType, yangInstanceIdentifier); + } + ReadFailedException e = new ReadFailedException("Authorization Failed"); + return Futures.immediateFailedCheckedFuture(e); + } + + @Override + public CheckedFuture<Boolean, ReadFailedException> exists( + LogicalDatastoreType logicalDatastoreType, YangInstanceIdentifier yangInstanceIdentifier) { + + if (AuthzServiceImpl.isAuthorized(logicalDatastoreType, yangInstanceIdentifier, + ActionType.Exists)) { + return domDataReadWriteTransaction.exists(logicalDatastoreType, yangInstanceIdentifier); + } + ReadFailedException e = new ReadFailedException("Authorization Failed"); + return Futures.immediateFailedCheckedFuture(e); + } + + @Override + public void put(LogicalDatastoreType logicalDatastoreType, + YangInstanceIdentifier yangInstanceIdentifier, NormalizedNode<?, ?> normalizedNode) { + + if (AuthzServiceImpl.isAuthorized(logicalDatastoreType, yangInstanceIdentifier, + ActionType.Put)) { + domDataReadWriteTransaction.put(logicalDatastoreType, yangInstanceIdentifier, + normalizedNode); + } + } + + @Override + public void merge(LogicalDatastoreType logicalDatastoreType, + YangInstanceIdentifier yangInstanceIdentifier, NormalizedNode<?, ?> normalizedNode) { + + if (AuthzServiceImpl.isAuthorized(logicalDatastoreType, yangInstanceIdentifier, + ActionType.Merge)) { + domDataReadWriteTransaction.merge(logicalDatastoreType, yangInstanceIdentifier, + normalizedNode); + } + } + + @Override + public Object getIdentifier() { + if (AuthzServiceImpl.isAuthorized(ActionType.GetIdentifier)) { + return domDataReadWriteTransaction.getIdentifier(); + } + return null; + } +} diff --git a/odl-aaa-moon/aaa/aaa-authz/aaa-authz-service/src/main/java/org/opendaylight/aaa/authz/srv/AuthzDomDataBroker.java b/odl-aaa-moon/aaa/aaa-authz/aaa-authz-service/src/main/java/org/opendaylight/aaa/authz/srv/AuthzDomDataBroker.java new file mode 100644 index 00000000..911f5a48 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authz/aaa-authz-service/src/main/java/org/opendaylight/aaa/authz/srv/AuthzDomDataBroker.java @@ -0,0 +1,100 @@ +/* + * Copyright (c) 2014 Cisco Systems, Inc. and others. 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.authz.srv; + +import java.util.Map; +import org.opendaylight.aaa.api.AuthenticationService; +import org.opendaylight.controller.md.sal.common.api.data.LogicalDatastoreType; +import org.opendaylight.controller.md.sal.common.api.data.TransactionChainListener; +import org.opendaylight.controller.md.sal.dom.api.DOMDataBroker; +import org.opendaylight.controller.md.sal.dom.api.DOMDataBrokerExtension; +import org.opendaylight.controller.md.sal.dom.api.DOMDataChangeListener; +import org.opendaylight.controller.md.sal.dom.api.DOMDataReadOnlyTransaction; +import org.opendaylight.controller.md.sal.dom.api.DOMDataReadWriteTransaction; +import org.opendaylight.controller.md.sal.dom.api.DOMDataWriteTransaction; +import org.opendaylight.controller.md.sal.dom.api.DOMTransactionChain; +import org.opendaylight.controller.sal.core.api.Broker; +import org.opendaylight.controller.sal.core.api.BrokerService; +import org.opendaylight.yangtools.concepts.ListenerRegistration; +import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier; + +/** + * Created by wdec on 26/08/2014. + */ +public class AuthzDomDataBroker implements BrokerService, DOMDataBroker { + + private DOMDataBroker domDataBroker; + private Broker.ProviderSession providerSession; + + private volatile AuthenticationService authService; + + final static AuthzDomDataBroker INSTANCE = new AuthzDomDataBroker(); + + public static AuthzDomDataBroker getInstance() { + return INSTANCE; + } + + public void setDomDataBroker(DOMDataBroker domDataBroker) { + this.domDataBroker = domDataBroker; + } + + public void setProviderSession(Broker.ProviderSession providerSession) { + this.providerSession = providerSession; + } + + public void setAuthService(AuthenticationService authService) { + this.authService = authService; + } + + public AuthenticationService getAuthService() { + return this.authService; + } + + @Override + public DOMDataReadOnlyTransaction newReadOnlyTransaction() { + // new Authz transaction + inject real DOM Transaction + DOMDataReadOnlyTransaction ro = domDataBroker.newReadOnlyTransaction(); + + // return domDataBroker.newReadOnlyTransaction(); //Return original + return new AuthzReadOnlyTransaction(ro); + } + + @Override + public Map<Class<? extends DOMDataBrokerExtension>, DOMDataBrokerExtension> getSupportedExtensions() { + return domDataBroker.getSupportedExtensions(); + } + + @Override + public DOMDataReadWriteTransaction newReadWriteTransaction() { + // return new Authz transaction + inject real DOM Transaction + DOMDataReadWriteTransaction rw = domDataBroker.newReadWriteTransaction(); + return new AuthzDataReadWriteTransaction(rw); + } + + @Override + public DOMDataWriteTransaction newWriteOnlyTransaction() { + DOMDataWriteTransaction wo = domDataBroker.newWriteOnlyTransaction(); + return new AuthzWriteOnlyTransaction(wo); + } + + @Override + public ListenerRegistration<DOMDataChangeListener> registerDataChangeListener( + LogicalDatastoreType logicalDatastoreType, + YangInstanceIdentifier yangInstanceIdentifier, + DOMDataChangeListener domDataChangeListener, DataChangeScope dataChangeScope) { + return domDataBroker.registerDataChangeListener(logicalDatastoreType, + yangInstanceIdentifier, domDataChangeListener, dataChangeScope); + } + + @Override + public DOMTransactionChain createTransactionChain( + TransactionChainListener transactionChainListener) { + return domDataBroker.createTransactionChain(transactionChainListener); + } +} diff --git a/odl-aaa-moon/aaa/aaa-authz/aaa-authz-service/src/main/java/org/opendaylight/aaa/authz/srv/AuthzProviderContextImpl.java b/odl-aaa-moon/aaa/aaa-authz/aaa-authz-service/src/main/java/org/opendaylight/aaa/authz/srv/AuthzProviderContextImpl.java new file mode 100644 index 00000000..dbfea6ed --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authz/aaa-authz-service/src/main/java/org/opendaylight/aaa/authz/srv/AuthzProviderContextImpl.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2014 Cisco Systems, Inc. and others. 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.authz.srv; + +import org.opendaylight.controller.md.sal.dom.api.DOMDataBroker; +import org.opendaylight.controller.sal.core.api.Broker; +import org.opendaylight.controller.sal.core.api.Broker.ProviderSession; +import org.opendaylight.controller.sal.core.api.BrokerService; +import org.opendaylight.controller.sal.core.spi.ForwardingProviderSession; + +/** + * Created by wdec on 28/08/2014. + */ +public class AuthzProviderContextImpl extends ForwardingProviderSession { + + private final Broker.ProviderSession realSession; + + public AuthzProviderContextImpl(Broker.ProviderSession providerSession, + AuthzBrokerImpl authzBroker) { + this.realSession = providerSession; + } + + @Override + protected ProviderSession delegate() { + // TODO Auto-generated method stub + return realSession; + } + + @Override + public <T extends BrokerService> T getService(Class<T> tClass) { + T t; + // Check for class and return Authz broker only for DOMBroker + if (tClass == DOMDataBroker.class) { + t = (T) AuthzDomDataBroker.getInstance(); + } else { + t = realSession.getService(tClass); + } + // AuthzDomDataBroker.getInstance().setDomDataBroker((DOMDataBroker)t); + return t; + } +} diff --git a/odl-aaa-moon/aaa/aaa-authz/aaa-authz-service/src/main/java/org/opendaylight/aaa/authz/srv/AuthzReadOnlyTransaction.java b/odl-aaa-moon/aaa/aaa-authz/aaa-authz-service/src/main/java/org/opendaylight/aaa/authz/srv/AuthzReadOnlyTransaction.java new file mode 100644 index 00000000..c46ffe7c --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authz/aaa-authz-service/src/main/java/org/opendaylight/aaa/authz/srv/AuthzReadOnlyTransaction.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2014 Cisco Systems, Inc. and others. 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.authz.srv; + +import com.google.common.base.Optional; +import com.google.common.util.concurrent.CheckedFuture; +import com.google.common.util.concurrent.Futures; + +import org.opendaylight.controller.md.sal.common.api.data.LogicalDatastoreType; +import org.opendaylight.controller.md.sal.common.api.data.ReadFailedException; +import org.opendaylight.controller.md.sal.dom.api.DOMDataReadOnlyTransaction; +import org.opendaylight.yang.gen.v1.urn.aaa.yang.authz.ds.rev140722.ActionType; +import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier; +import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode; + +/** + * Created by wdec on 28/08/2014. + */ + +public class AuthzReadOnlyTransaction implements DOMDataReadOnlyTransaction { + + private final DOMDataReadOnlyTransaction ro; + + public AuthzReadOnlyTransaction(DOMDataReadOnlyTransaction ro) { + this.ro = ro; + } + + @Override + public void close() { + ro.close(); + } + + @Override + public CheckedFuture<Optional<NormalizedNode<?, ?>>, ReadFailedException> read( + LogicalDatastoreType logicalDatastoreType, YangInstanceIdentifier yangInstanceIdentifier) { + + if (AuthzServiceImpl.isAuthorized(logicalDatastoreType, yangInstanceIdentifier, + ActionType.Read)) { + return ro.read(logicalDatastoreType, yangInstanceIdentifier); + } + ReadFailedException e = new ReadFailedException("Authorization Failed"); + return Futures.immediateFailedCheckedFuture(e); + } + + @Override + public CheckedFuture<Boolean, ReadFailedException> exists( + LogicalDatastoreType logicalDatastoreType, YangInstanceIdentifier yangInstanceIdentifier) { + + if (AuthzServiceImpl.isAuthorized(ActionType.Exists)) { + return ro.exists(logicalDatastoreType, yangInstanceIdentifier); + } + ReadFailedException e = new ReadFailedException("Authorization Failed"); + return Futures.immediateFailedCheckedFuture(e); + } + + @Override + public Object getIdentifier() { + if (AuthzServiceImpl.isAuthorized(ActionType.GetIdentifier)) { + return ro.getIdentifier(); + } + return null; + } +} diff --git a/odl-aaa-moon/aaa/aaa-authz/aaa-authz-service/src/main/java/org/opendaylight/aaa/authz/srv/AuthzServiceImpl.java b/odl-aaa-moon/aaa/aaa-authz/aaa-authz-service/src/main/java/org/opendaylight/aaa/authz/srv/AuthzServiceImpl.java new file mode 100644 index 00000000..fb344812 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authz/aaa-authz-service/src/main/java/org/opendaylight/aaa/authz/srv/AuthzServiceImpl.java @@ -0,0 +1,121 @@ +/* + * Copyright (c) 2014 Cisco Systems, Inc. and others. 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.authz.srv; + +import java.util.List; +import org.opendaylight.aaa.api.Authentication; +import org.opendaylight.aaa.api.AuthenticationService; +import org.opendaylight.controller.config.yang.config.aaa_authz.srv.Policies; +import org.opendaylight.controller.md.sal.common.api.data.LogicalDatastoreType; +import org.opendaylight.yang.gen.v1.urn.aaa.yang.authz.ds.rev140722.ActionType; +import org.opendaylight.yang.gen.v1.urn.aaa.yang.authz.ds.rev140722.AuthorizationResponseType; +import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier; + +/** + * @author lmukkama Date: 9/2/14 + */ +public class AuthzServiceImpl { + + private static List<Policies> listPolicies; + + private static final String WILDCARD_TOKEN = "*"; + + public static boolean isAuthorized(LogicalDatastoreType logicalDatastoreType, + YangInstanceIdentifier yangInstanceIdentifier, ActionType actionType) { + + AuthorizationResponseType authorizationResponseType = AuthzServiceImpl.reqAuthorization( + actionType, logicalDatastoreType, yangInstanceIdentifier); + return authorizationResponseType.equals(AuthorizationResponseType.Authorized); + } + + public static boolean isAuthorized(ActionType actionType) { + AuthorizationResponseType authorizationResponseType = AuthzServiceImpl + .reqAuthorization(actionType); + return authorizationResponseType.equals(AuthorizationResponseType.Authorized); + } + + public static void setPolicies(List<Policies> policies) { + + AuthzServiceImpl.listPolicies = policies; + } + + public static AuthorizationResponseType reqAuthorization(ActionType actionType) { + + AuthenticationService authenticationService = AuthzDomDataBroker.getInstance() + .getAuthService(); + if (authenticationService != null && AuthzServiceImpl.listPolicies != null + && AuthzServiceImpl.listPolicies.size() > 0) { + Authentication authentication = authenticationService.get(); + if (authentication != null && authentication.roles() != null + && authentication.roles().size() > 0) { + return checkAuthorization(actionType, authentication); + } + } + return AuthorizationResponseType.NotAuthorized; + } + + public static AuthorizationResponseType reqAuthorization(ActionType actionType, + LogicalDatastoreType logicalDatastoreType, YangInstanceIdentifier yangInstanceIdentifier) { + + AuthenticationService authenticationService = AuthzDomDataBroker.getInstance() + .getAuthService(); + + if (authenticationService != null && AuthzServiceImpl.listPolicies != null + && AuthzServiceImpl.listPolicies.size() > 0) { + // Authentication Service exists. Can do authorization checks + Authentication authentication = authenticationService.get(); + + if (authentication != null && authentication.roles() != null + && authentication.roles().size() > 0) { + // Authentication claim object exists with atleast one role + return checkAuthorization(actionType, authentication, logicalDatastoreType, + yangInstanceIdentifier); + } + } + + return AuthorizationResponseType.Authorized; + } + + private static AuthorizationResponseType checkAuthorization(ActionType actionType, + Authentication authentication, LogicalDatastoreType logicalDatastoreType, + YangInstanceIdentifier yangInstanceIdentifier) { + + for (Policies policy : AuthzServiceImpl.listPolicies) { + + // Action type is compared as string, since its type is string in + // the config yang. Comparison is case insensitive + if (authentication.roles().contains(policy.getRole().getValue()) + && (policy.getResource().getValue().equals(WILDCARD_TOKEN) || policy + .getResource().getValue().equals(yangInstanceIdentifier.toString())) + && (policy.getAction().toLowerCase() + .equals(ActionType.Any.name().toLowerCase()) || actionType.name() + .toLowerCase().equals(policy.getAction().toLowerCase()))) { + + return AuthorizationResponseType.Authorized; + } + + } + + // For helium release we unauthorize other requests. + return AuthorizationResponseType.NotAuthorized; + } + + private static AuthorizationResponseType checkAuthorization(ActionType actionType, + Authentication authentication) { + + for (Policies policy : AuthzServiceImpl.listPolicies) { + if (authentication.roles().contains(policy.getRole().getValue()) + && (policy.getAction().equalsIgnoreCase(ActionType.Any.name()) || policy + .getAction().equalsIgnoreCase(actionType.name()))) { + return AuthorizationResponseType.Authorized; + } + } + return AuthorizationResponseType.NotAuthorized; + } +} diff --git a/odl-aaa-moon/aaa/aaa-authz/aaa-authz-service/src/main/java/org/opendaylight/aaa/authz/srv/AuthzWriteOnlyTransaction.java b/odl-aaa-moon/aaa/aaa-authz/aaa-authz-service/src/main/java/org/opendaylight/aaa/authz/srv/AuthzWriteOnlyTransaction.java new file mode 100644 index 00000000..1123b928 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authz/aaa-authz-service/src/main/java/org/opendaylight/aaa/authz/srv/AuthzWriteOnlyTransaction.java @@ -0,0 +1,103 @@ +/* + * Copyright (c) 2014 Cisco Systems, Inc. and others. 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.authz.srv; + +import com.google.common.util.concurrent.CheckedFuture; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; + +import org.opendaylight.controller.md.sal.common.api.TransactionStatus; +import org.opendaylight.controller.md.sal.common.api.data.LogicalDatastoreType; +import org.opendaylight.controller.md.sal.common.api.data.TransactionCommitFailedException; +import org.opendaylight.controller.md.sal.dom.api.DOMDataWriteTransaction; +import org.opendaylight.yang.gen.v1.urn.aaa.yang.authz.ds.rev140722.ActionType; +import org.opendaylight.yangtools.yang.common.RpcResult; +import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier; +import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode; + +/** + * Created by wdec on 02/09/2014. + */ +public class AuthzWriteOnlyTransaction implements DOMDataWriteTransaction { + + private final DOMDataWriteTransaction domDataWriteTransaction; + + public AuthzWriteOnlyTransaction(DOMDataWriteTransaction wo) { + this.domDataWriteTransaction = wo; + } + + @Override + public void put(LogicalDatastoreType logicalDatastoreType, + YangInstanceIdentifier yangInstanceIdentifier, NormalizedNode<?, ?> normalizedNode) { + + if (AuthzServiceImpl.isAuthorized(logicalDatastoreType, yangInstanceIdentifier, + ActionType.Put)) { + domDataWriteTransaction.put(logicalDatastoreType, yangInstanceIdentifier, + normalizedNode); + } + } + + @Override + public void merge(LogicalDatastoreType logicalDatastoreType, + YangInstanceIdentifier yangInstanceIdentifier, NormalizedNode<?, ?> normalizedNode) { + + if (AuthzServiceImpl.isAuthorized(logicalDatastoreType, yangInstanceIdentifier, + ActionType.Merge)) { + domDataWriteTransaction.merge(logicalDatastoreType, yangInstanceIdentifier, + normalizedNode); + } + } + + @Override + public boolean cancel() { + if (AuthzServiceImpl.isAuthorized(ActionType.Cancel)) { + return domDataWriteTransaction.cancel(); + } + return false; + } + + @Override + public void delete(LogicalDatastoreType logicalDatastoreType, + YangInstanceIdentifier yangInstanceIdentifier) { + + if (AuthzServiceImpl.isAuthorized(logicalDatastoreType, yangInstanceIdentifier, + ActionType.Delete)) { + domDataWriteTransaction.delete(logicalDatastoreType, yangInstanceIdentifier); + } + } + + @Override + public CheckedFuture<Void, TransactionCommitFailedException> submit() { + if (AuthzServiceImpl.isAuthorized(ActionType.Submit)) { + return domDataWriteTransaction.submit(); + } + TransactionCommitFailedException e = new TransactionCommitFailedException( + "Unauthorized User"); + return Futures.immediateFailedCheckedFuture(e); + } + + @Deprecated + @Override + public ListenableFuture<RpcResult<TransactionStatus>> commit() { + if (AuthzServiceImpl.isAuthorized(ActionType.Commit)) { + return domDataWriteTransaction.commit(); + } + TransactionCommitFailedException e = new TransactionCommitFailedException( + "Unauthorized User"); + return Futures.immediateFailedCheckedFuture(e); + } + + @Override + public Object getIdentifier() { + if (AuthzServiceImpl.isAuthorized(ActionType.GetIdentifier)) { + return domDataWriteTransaction.getIdentifier(); + } + return null; + } +} diff --git a/odl-aaa-moon/aaa/aaa-authz/aaa-authz-service/src/main/java/org/opendaylight/controller/config/yang/config/aaa_authz/srv/AuthzSrvModule.java b/odl-aaa-moon/aaa/aaa-authz/aaa-authz-service/src/main/java/org/opendaylight/controller/config/yang/config/aaa_authz/srv/AuthzSrvModule.java new file mode 100644 index 00000000..a590b982 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authz/aaa-authz-service/src/main/java/org/opendaylight/controller/config/yang/config/aaa_authz/srv/AuthzSrvModule.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2014 Cisco Systems, Inc. and others. 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.controller.config.yang.config.aaa_authz.srv; + +import org.opendaylight.aaa.api.AuthenticationService; +import org.opendaylight.aaa.authz.srv.AuthzBrokerImpl; +import org.opendaylight.aaa.authz.srv.AuthzServiceImpl; +import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceReference; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class AuthzSrvModule extends + org.opendaylight.controller.config.yang.config.aaa_authz.srv.AbstractAuthzSrvModule { + private static final Logger LOG = LoggerFactory.getLogger(AuthzSrvModule.class); + private static boolean simple_config_switch; + private BundleContext bundleContext; + + public AuthzSrvModule(org.opendaylight.controller.config.api.ModuleIdentifier identifier, + org.opendaylight.controller.config.api.DependencyResolver dependencyResolver) { + super(identifier, dependencyResolver); + } + + public AuthzSrvModule(org.opendaylight.controller.config.api.ModuleIdentifier identifier, + org.opendaylight.controller.config.api.DependencyResolver dependencyResolver, + org.opendaylight.controller.config.yang.config.aaa_authz.srv.AuthzSrvModule oldModule, + java.lang.AutoCloseable oldInstance) { + super(identifier, dependencyResolver, oldModule, oldInstance); + } + + @Override + public void customValidation() { + // checkNotNull(getDomBroker(), domBrokerJmxAttribute); + } + + @Override + public java.lang.AutoCloseable createInstance() { + + // Get new AuthZ Broker + final AuthzBrokerImpl authzBrokerImpl = new AuthzBrokerImpl(); + + // Provide real broker to the new Authz broker + authzBrokerImpl.setBroker(getDomBrokerDependency()); + + // Get AuthN service reference and register it with the authzBroker + ServiceReference<AuthenticationService> authServiceReference = bundleContext + .getServiceReference(AuthenticationService.class); + AuthenticationService as = bundleContext.getService(authServiceReference); + authzBrokerImpl.setAuthenticationService(as); + + // Set the policies list to authz serviceimpl + AuthzServiceImpl.setPolicies(getPolicies()); + + // Register AuthZ broker with the real Broker as a provider; triggers + // "onSessionInitiated" in AuthzBrokerImpl + getDomBrokerDependency().registerProvider(authzBrokerImpl); + // TODO ActionType is of type string, not ENUM due to improper + // serialization of ENUMs by config/netconf subsystem. This needs to be + // fixed as soon as config/netconf fixes the problem. + getAction(); + + LOG.info("AuthZ Service Initialized from Config subsystem"); + return authzBrokerImpl; + + } + + public void setBundleContext(BundleContext bundleContext) { + this.bundleContext = bundleContext; + } +} diff --git a/odl-aaa-moon/aaa/aaa-authz/aaa-authz-service/src/main/java/org/opendaylight/controller/config/yang/config/aaa_authz/srv/AuthzSrvModuleFactory.java b/odl-aaa-moon/aaa/aaa-authz/aaa-authz-service/src/main/java/org/opendaylight/controller/config/yang/config/aaa_authz/srv/AuthzSrvModuleFactory.java new file mode 100644 index 00000000..3ff67f54 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authz/aaa-authz-service/src/main/java/org/opendaylight/controller/config/yang/config/aaa_authz/srv/AuthzSrvModuleFactory.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2014 Cisco Systems, Inc. and others. 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 + */ + +/* + * Generated file + * + * Generated from: yang module name: aaa-authz-service-impl yang module local name: aaa-authz-service + * Generated by: org.opendaylight.controller.config.yangjmxgenerator.plugin.JMXGenerator + * Generated at: Thu Jul 24 11:19:40 CEST 2014 + * + * Do not modify this file unless it is present under src/main directory + */ +package org.opendaylight.controller.config.yang.config.aaa_authz.srv; + +import org.opendaylight.controller.config.api.DependencyResolver; +import org.opendaylight.controller.config.api.DynamicMBeanWithInstance; +import org.opendaylight.controller.config.spi.Module; +import org.osgi.framework.BundleContext; + +public class AuthzSrvModuleFactory extends + org.opendaylight.controller.config.yang.config.aaa_authz.srv.AbstractAuthzSrvModuleFactory { + + @Override + public org.opendaylight.controller.config.spi.Module createModule(String instanceName, + org.opendaylight.controller.config.api.DependencyResolver dependencyResolver, + org.osgi.framework.BundleContext bundleContext) { + + final AuthzSrvModule module = (AuthzSrvModule) super.createModule(instanceName, + dependencyResolver, bundleContext); + + module.setBundleContext(bundleContext); + + return module; + + } + + @Override + public Module createModule(final String instanceName, + final DependencyResolver dependencyResolver, final DynamicMBeanWithInstance old, + final BundleContext bundleContext) throws Exception { + final AuthzSrvModule module = (AuthzSrvModule) super.createModule(instanceName, + dependencyResolver, old, bundleContext); + + module.setBundleContext(bundleContext); + + return module; + } +} diff --git a/odl-aaa-moon/aaa/aaa-authz/aaa-authz-service/src/main/yang/aaa-authz-service-impl.yang b/odl-aaa-moon/aaa/aaa-authz/aaa-authz-service/src/main/yang/aaa-authz-service-impl.yang new file mode 100644 index 00000000..954d0480 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authz/aaa-authz-service/src/main/yang/aaa-authz-service-impl.yang @@ -0,0 +1,115 @@ +module aaa-authz-service-impl { + + yang-version 1; + namespace "urn:opendaylight:params:xml:ns:yang:controller:config:aaa-authz:srv"; + prefix "aaa-authz-srv-impl"; + + import config { prefix config; revision-date 2013-04-05; } + import rpc-context { prefix rpcx; revision-date 2013-06-17; } + import opendaylight-md-sal-binding { prefix mdsal; revision-date 2013-10-28; } + import opendaylight-md-sal-dom {prefix dom;} + import authorization-schema { prefix authzs; revision-date 2014-07-22; } + import ietf-inet-types {prefix inet; revision-date 2010-09-24;} + + description + "This module contains the base YANG definitions for + AuthZ implementation."; + + revision "2014-07-01" { + description + "Initial revision."; + } + + + // This is the definition of the service implementation as a module identity. + identity aaa-authz-service { + base config:module-type; + // Specifies the prefix for generated java classes. + config:java-name-prefix AuthzSrv; + config:provided-service dom:dom-broker-osgi-registry; + } + + // Augments the 'configuration' choice node under modules/module. + + augment "/config:modules/config:module/config:configuration" { + case aaa-authz-service { + when "/config:modules/config:module/config:type = 'aaa-authz-service'"; + +//Defines reference to the intended broker under the AuthZ broker + + container dom-broker { + uses config:service-ref { + refine type { + mandatory true; + config:required-identity dom:dom-broker-osgi-registry; + } + } + } + + container data-broker { + uses config:service-ref { + refine type { + mandatory true; + config:required-identity mdsal:binding-data-broker; + + } + } + } + +//Simple Authz data leafs: + + leaf authz-role { + type string; + } + leaf service { + type authzs:service-type; + } + + // ENUMs cannot be used right now (config subsystem + netconf cannot properly serialize enums), using strings instead + // In the generated module use Enum.valueOf from that string. + // Expected values are following strnigs: create, read, update, delete, execute, subscribe, any; + leaf action { + type string; + description "String representation of enum authzs:action-type expecting following values create, read, update, delete, execute, subscribe, any"; + //type authzs:action-type; + + } + leaf resource { + type authzs:resource-type; + + } + leaf role { + type authzs:role-type; + } + + + + //TODO: Check why uses below doesn't make the outer list be part of the source name-space in yang code generator. + //uses authzs:authorization-grp; + list policies { + key "service"; + leaf service { + type authzs:service-type; + } + // Grouping uses ENUMs and enums are not correctly serialized in Config + Netconf + // Same as with action one level ip + leaf action { + type string; + description "String representation of enum authzs:action-type expecting following values create, read, update, delete, execute, subscribe, any"; + //type authzs:action-type; + } + leaf resource { + type authzs:resource-type; + + } + leaf role { + type authzs:role-type; + + } + } + + + } + } + +} diff --git a/odl-aaa-moon/aaa/aaa-authz/aaa-authz-service/src/test/java/org/opendaylight/aaa/authz/srv/AuthzConsumerContextImplTest.java b/odl-aaa-moon/aaa/aaa-authz/aaa-authz-service/src/test/java/org/opendaylight/aaa/authz/srv/AuthzConsumerContextImplTest.java new file mode 100644 index 00000000..fb033341 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authz/aaa-authz-service/src/test/java/org/opendaylight/aaa/authz/srv/AuthzConsumerContextImplTest.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2014 Cisco Systems, Inc. and others. 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.authz.srv; + +import org.junit.Assert; +import org.junit.Before; +import org.mockito.Mockito; +import org.opendaylight.controller.md.sal.dom.api.DOMDataBroker; +import org.opendaylight.controller.sal.core.api.Broker; +import org.opendaylight.controller.sal.core.api.Provider; + +public class AuthzConsumerContextImplTest { + + private Broker.ConsumerSession realconsumercontext; + private Provider realprovidercontext; + private AuthzBrokerImpl authzBroker; + private Broker realbroker; + + @Before + public void beforeTest() { + realconsumercontext = Mockito.mock(Broker.ConsumerSession.class); + realprovidercontext = Mockito.mock(Provider.class); + realbroker = Mockito.mock(Broker.class); + realbroker.registerProvider(realprovidercontext); + authzBroker = Mockito.mock(AuthzBrokerImpl.class); + } + + @org.junit.Test + public void testGetService() throws Exception { + AuthzConsumerContextImpl authzConsumerContext = new AuthzConsumerContextImpl( + realconsumercontext, authzBroker); + + Assert.assertEquals("Expected Authz session context", + authzConsumerContext.getService(DOMDataBroker.class).getClass(), + AuthzDomDataBroker.class); + // Assert.assertEquals("Expected Authz session context", + // authzConsumerContext.getService(SchemaService.class).getClass(), + // SchemaService.class); + } +}
\ No newline at end of file diff --git a/odl-aaa-moon/aaa/aaa-authz/pom.xml b/odl-aaa-moon/aaa/aaa-authz/pom.xml new file mode 100644 index 00000000..a5e37680 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-authz/pom.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + <parent> + <groupId>org.opendaylight.aaa</groupId> + <artifactId>aaa-parent</artifactId> + <version>0.3.2-Beryllium-SR2</version> + <relativePath>../parent</relativePath> + </parent> + + <artifactId>aaa-authz</artifactId> + <name>${project.artifactId}</name> + <packaging>pom</packaging> + + <modules> + <module>aaa-authz-model</module> + <module>aaa-authz-service</module> + <module>aaa-authz-config</module> + <module>aaa-authz-restconf-config</module> + </modules> +</project> diff --git a/odl-aaa-moon/aaa/aaa-credential-store-api/pom.xml b/odl-aaa-moon/aaa/aaa-credential-store-api/pom.xml new file mode 100644 index 00000000..b43ac11c --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-credential-store-api/pom.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +(c) Copyright 2015 Hewlett-Packard Development Company, L.P. and others. 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 +--> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + <parent> + <groupId>org.opendaylight.mdsal</groupId> + <artifactId>binding-parent</artifactId> + <version>0.8.2-Beryllium-SR2</version> + <relativePath/> + </parent> + + <groupId>org.opendaylight.aaa</groupId> + <artifactId>aaa-credential-store-api</artifactId> + <version>0.3.2-Beryllium-SR2</version> + <packaging>bundle</packaging> +</project> diff --git a/odl-aaa-moon/aaa/aaa-credential-store-api/src/main/yang/credential-model.yang b/odl-aaa-moon/aaa/aaa-credential-store-api/src/main/yang/credential-model.yang new file mode 100644 index 00000000..7d1f55a3 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-credential-store-api/src/main/yang/credential-model.yang @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2015 Hewlett-Packard Development Company, L.P. and others. 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 + */ +module credential-store { + namespace "urn:opendaylight:params:xml:ns:yang:aaa:credential-store"; + prefix "cs"; + + description "Defines and extensible model for storing various types of security credentials."; + + revision "2015-02-26" { description "Initial revision."; } + + identity credential-type { + description + "Credential base type. All credential types must be derived from this identity."; + } + + typedef credential-type-ref { + description "reference to an entry in the credential store based on id."; + type instance-identifier; + } + + container credential-store { + list credential { + key "id"; + + leaf id { + description "Unique identifier for this credential entry."; + type string; + } + + leaf type { + description "The type of credential represented in this entry."; + type identityref { + base credential-type; + } + } + + choice value { + description "Extension point. Contains the data specific to the credential type."; + } + } + } +} diff --git a/odl-aaa-moon/aaa/aaa-h2-store/.gitignore b/odl-aaa-moon/aaa/aaa-h2-store/.gitignore new file mode 100644 index 00000000..1dd33310 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-h2-store/.gitignore @@ -0,0 +1,2 @@ +/target/ +/target/ diff --git a/odl-aaa-moon/aaa/aaa-h2-store/pom.xml b/odl-aaa-moon/aaa/aaa-h2-store/pom.xml new file mode 100644 index 00000000..d40f8858 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-h2-store/pom.xml @@ -0,0 +1,160 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + <parent> + <groupId>org.opendaylight.aaa</groupId> + <artifactId>aaa-parent</artifactId> + <version>0.3.2-Beryllium-SR2</version> + <relativePath>../parent</relativePath> + </parent> + + <artifactId>aaa-h2-store</artifactId> + <packaging>bundle</packaging> + + <dependencies> + <dependency> + <groupId>org.opendaylight.controller</groupId> + <artifactId>config-api</artifactId> + <version>${config.version}</version> + </dependency> + <dependency> + <groupId>org.opendaylight.controller</groupId> + <artifactId>sal-binding-config</artifactId> + </dependency> + <dependency> + <groupId>org.opendaylight.controller</groupId> + <artifactId>sal-binding-api</artifactId> + </dependency> + <dependency> + <groupId>org.opendaylight.controller</groupId> + <artifactId>sal-common-util</artifactId> + </dependency> + <dependency> + <groupId>org.apache.commons</groupId> + <artifactId>commons-lang3</artifactId> + </dependency> + + <dependency> + <groupId>org.opendaylight.aaa</groupId> + <artifactId>aaa-authn-api</artifactId> + </dependency> + <dependency> + <groupId>org.opendaylight.aaa</groupId> + <artifactId>aaa-authn</artifactId> + </dependency> + <dependency> + <groupId>org.slf4j</groupId> + <artifactId>slf4j-api</artifactId> + </dependency> + <dependency> + <groupId>org.apache.felix</groupId> + <artifactId>org.apache.felix.dependencymanager</artifactId> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>org.mockito</groupId> + <artifactId>mockito-all</artifactId> + <scope>test</scope> + </dependency> + + <!-- JDBC --> + <dependency> + <groupId>com.h2database</groupId> + <artifactId>h2</artifactId> + </dependency> + <!-- Testing Dependencies --> + <dependency> + <groupId>junit</groupId> + <artifactId>junit</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.slf4j</groupId> + <artifactId>slf4j-simple</artifactId> + <scope>test</scope> + </dependency> + + </dependencies> + <build> + <plugins> + <plugin> + <groupId>org.apache.felix</groupId> + <artifactId>maven-bundle-plugin</artifactId> + <version>${bundle.plugin.version}</version> + <extensions>true</extensions> + <configuration> + <instructions> + <Import-Package>com.google.*,org.opendaylight.aaa.api.*,org.apache.felix.*,org.slf4j.*,org.opendaylight.*,org.osgi.*,org.apache.commons.lang3</Import-Package> + <Private-Package>org.h2.*</Private-Package> + <Embed-Dependency>h2</Embed-Dependency> + </instructions> + </configuration> + </plugin> + <plugin> + <groupId>org.opendaylight.yangtools</groupId> + <artifactId>yang-maven-plugin</artifactId> + <version>${yangtools.version}</version> + <executions> + <execution> + <id>config</id> + <goals> + <goal>generate-sources</goal> + </goals> + <configuration> + <codeGenerators> + <generator> + <codeGeneratorClass>org.opendaylight.controller.config.yangjmxgenerator.plugin.JMXGenerator</codeGeneratorClass> + <outputBaseDir>${jmxGeneratorPath}</outputBaseDir> + <additionalConfiguration> + <namespaceToPackage1>urn:opendaylight:params:xml:ns:yang:controller==org.opendaylight.controller.config.yang</namespaceToPackage1> + </additionalConfiguration> + </generator> + <generator> + <codeGeneratorClass>org.opendaylight.yangtools.maven.sal.api.gen.plugin.CodeGeneratorImpl</codeGeneratorClass> + <outputBaseDir>${salGeneratorPath}</outputBaseDir> + </generator> + </codeGenerators> + <inspectDependencies>true</inspectDependencies> + </configuration> + </execution> + </executions> + <dependencies> + <dependency> + <groupId>org.opendaylight.mdsal</groupId> + <artifactId>maven-sal-api-gen-plugin</artifactId> + <version>${yangtools.version}</version> + <type>jar</type> + </dependency> + <dependency> + <groupId>org.opendaylight.controller</groupId> + <artifactId>yang-jmx-generator-plugin</artifactId> + <version>${config.version}</version> + </dependency> + </dependencies> + </plugin> + <plugin> + <groupId>org.codehaus.mojo</groupId> + <artifactId>build-helper-maven-plugin</artifactId> + <executions> + <execution> + <id>attach-artifacts</id> + <goals> + <goal>attach-artifact</goal> + </goals> + <phase>package</phase> + <configuration> + <artifacts> + <artifact> + <file>${project.build.directory}/classes/initial/08-aaa-h2-store-config.xml</file> + <type>xml</type> + <classifier>config</classifier> + </artifact> + </artifacts> + </configuration> + </execution> + </executions> + </plugin> + </plugins> + </build> +</project> diff --git a/odl-aaa-moon/aaa/aaa-h2-store/src/main/java/org/opendaylight/aaa/h2/config/IdmLightConfig.java b/odl-aaa-moon/aaa/aaa-h2-store/src/main/java/org/opendaylight/aaa/h2/config/IdmLightConfig.java new file mode 100644 index 00000000..a35ca48f --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-h2-store/src/main/java/org/opendaylight/aaa/h2/config/IdmLightConfig.java @@ -0,0 +1,133 @@ +/* + * Copyright (c) 2014, 2015 Hewlett-Packard Development Company, L.P. and others. 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.h2.config; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Responsible for providing configuration properties for the IDMLight/H2 + * data store implementation. + * + * @author peter.mellquist@hp.com + * + */ +public class IdmLightConfig { + + private static final Logger LOG = LoggerFactory.getLogger(IdmLightConfig.class); + + /** + * The default timeout for db connections in seconds. + */ + private static final int DEFAULT_DB_TIMEOUT = 3; + + /** + * The default password for the database + */ + private static final String DEFAULT_PASSWORD = "bar"; + + /** + * The default username for the database + */ + private static final String DEFAULT_USERNAME = "foo"; + + /** + * The default driver for the databse is H2; a pure-java implementation + * of JDBC. + */ + private static final String DEFAULT_JDBC_DRIVER = "org.h2.Driver"; + + /** + * The default connection string includes the intention to use h2 as + * the JDBC driver, and the path for the file is located relative to + * KARAF_HOME. + */ + private static final String DEFAULT_CONNECTION_STRING = "jdbc:h2:./"; + + /** + * The default filename for the database file. + */ + private static final String DEFAULT_IDMLIGHT_DB_FILENAME = "idmlight.db"; + + /** + * The database filename + */ + private String dbName; + + /** + * the database connection string + */ + private String dbPath; + + /** + * The database driver (i.e., H2) + */ + private String dbDriver; + + /** + * The database password. This is not the same as AAA credentials! + */ + private String dbUser; + + /** + * The database username. This is not the same as AAA credentials! + */ + private String dbPwd; + + /** + * Timeout for database connections in seconds + */ + private int dbValidTimeOut; + + /** + * Creates an valid database configuration using default values. + */ + public IdmLightConfig() { + // TODO make this configurable + dbName = DEFAULT_IDMLIGHT_DB_FILENAME; + dbPath = DEFAULT_CONNECTION_STRING + dbName; + dbDriver = DEFAULT_JDBC_DRIVER; + dbUser = DEFAULT_USERNAME; + dbPwd = DEFAULT_PASSWORD; + dbValidTimeOut = DEFAULT_DB_TIMEOUT; + } + + /** + * Outputs some debugging information surrounding idmlight config + */ + public void log() { + LOG.info("DB Path : {}", dbPath); + LOG.info("DB Driver : {}", dbDriver); + LOG.info("DB Valid Time Out : {}", dbValidTimeOut); + } + + public String getDbName() { + return this.dbName; + } + + public String getDbPath() { + return this.dbPath; + } + + public String getDbDriver() { + return this.dbDriver; + } + + public String getDbUser() { + return this.dbUser; + } + + public String getDbPwd() { + return this.dbPwd; + } + + public int getDbValidTimeOut() { + return this.dbValidTimeOut; + } +} diff --git a/odl-aaa-moon/aaa/aaa-h2-store/src/main/java/org/opendaylight/aaa/h2/persistence/AbstractStore.java b/odl-aaa-moon/aaa/aaa-h2-store/src/main/java/org/opendaylight/aaa/h2/persistence/AbstractStore.java new file mode 100644 index 00000000..ba00eb84 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-h2-store/src/main/java/org/opendaylight/aaa/h2/persistence/AbstractStore.java @@ -0,0 +1,187 @@ +/* + * Copyright © 2016 Red Hat, Inc. and others. + * + * 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.h2.persistence; + +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.ArrayList; +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Base class for H2 stores. + */ +abstract class AbstractStore<T> { + /** + * Logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(AbstractStore.class); + + /** + * The name of the table used to represent this store. + */ + private final String tableName; + + /** + * Database connection, only used for tests. + */ + Connection dbConnection = null; + + /** + * Table types we're interested in (when checking tables' existence). + */ + public static final String[] TABLE_TYPES = new String[] { "TABLE" }; + + /** + * Creates an instance. + * + * @param tableName The name of the table being managed. + */ + protected AbstractStore(String tableName) { + this.tableName = tableName; + } + + /** + * Returns a database connection. It is the caller's responsibility to close it. If the managed table does not + * exist, it will be created (using {@link #getTableCreationStatement()}). + * + * @return A database connection. + * + * @throws StoreException if an error occurs. + */ + protected Connection dbConnect() throws StoreException { + Connection conn = H2Store.getConnection(dbConnection); + try { + // Ensure table check/creation is atomic + synchronized (this) { + DatabaseMetaData dbm = conn.getMetaData(); + try (ResultSet rs = dbm.getTables(null, null, tableName, TABLE_TYPES)) { + if (rs.next()) { + LOG.debug("Table {} already exists", tableName); + } else { + LOG.info("Table {} does not exist, creating it", tableName); + try (Statement stmt = conn.createStatement()) { + stmt.executeUpdate(getTableCreationStatement()); + } + } + } + } + } catch (SQLException e) { + LOG.error("Error connecting to the H2 database", e); + throw new StoreException("Cannot connect to database server", e); + } + return conn; + } + + /** + * Empties the store. + * + * @throws StoreException if a connection error occurs. + */ + protected void dbClean() throws StoreException { + try (Connection c = dbConnect()) { + // The table name can't be a parameter in a prepared statement + String sql = "DELETE FROM " + tableName; + c.createStatement().execute(sql); + } catch (SQLException e) { + LOG.error("Error clearing table {}", tableName, e); + throw new StoreException("Error clearing table " + tableName, e); + } + } + + /** + * Returns the SQL code required to create the managed table. + * + * @return The SQL table creation statement. + */ + protected abstract String getTableCreationStatement(); + + /** + * Lists all the stored items. + * + * @return The stored item. + * + * @throws StoreException if an error occurs. + */ + protected List<T> listAll() throws StoreException { + List<T> result = new ArrayList<>(); + String query = "SELECT * FROM " + tableName; + try (Connection conn = dbConnect(); + Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery(query)) { + while (rs.next()) { + result.add(fromResultSet(rs)); + } + } catch (SQLException e) { + LOG.error("Error listing all items from {}", tableName, e); + throw new StoreException(e); + } + return result; + } + + /** + * Lists the stored items returned by the given statement. + * + * @param ps The statement (which must be ready for execution). It is the caller's reponsibility to close this. + * + * @return The stored items. + * + * @throws StoreException if an error occurs. + */ + protected List<T> listFromStatement(PreparedStatement ps) throws StoreException { + List<T> result = new ArrayList<>(); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + result.add(fromResultSet(rs)); + } + } catch (SQLException e) { + LOG.error("Error listing matching items from {}", tableName, e); + throw new StoreException(e); + } + return result; + } + + /** + * Extracts the first item returned by the given statement, if any. + * + * @param ps The statement (which must be ready for execution). It is the caller's reponsibility to close this. + * + * @return The first item, or {@code null} if none. + * + * @throws StoreException if an error occurs. + */ + protected T firstFromStatement(PreparedStatement ps) throws StoreException { + try (ResultSet rs = ps.executeQuery()) { + if (rs.next()) { + return fromResultSet(rs); + } else { + return null; + } + } catch (SQLException e) { + LOG.error("Error listing first matching item from {}", tableName, e); + throw new StoreException(e); + } + } + + /** + * Converts a single row in a result set to an instance of the managed type. + * + * @param rs The result set (which is ready for extraction; {@link ResultSet#next()} must <b>not</b> be called). + * + * @return The corresponding instance. + * + * @throws SQLException if an error occurs. + */ + protected abstract T fromResultSet(ResultSet rs) throws SQLException; +} diff --git a/odl-aaa-moon/aaa/aaa-h2-store/src/main/java/org/opendaylight/aaa/h2/persistence/DomainStore.java b/odl-aaa-moon/aaa/aaa-h2-store/src/main/java/org/opendaylight/aaa/h2/persistence/DomainStore.java new file mode 100644 index 00000000..aa8f4b30 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-h2-store/src/main/java/org/opendaylight/aaa/h2/persistence/DomainStore.java @@ -0,0 +1,166 @@ +/* + * Copyright (c) 2014, 2016 Hewlett-Packard Development Company, L.P. and others. 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.h2.persistence; + +import com.google.common.base.Preconditions; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; + +import org.apache.commons.lang3.StringEscapeUtils; +import org.opendaylight.aaa.api.model.Domain; +import org.opendaylight.aaa.api.model.Domains; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * + * @author peter.mellquist@hp.com + * + */ +public class DomainStore extends AbstractStore<Domain> { + private static final Logger LOG = LoggerFactory.getLogger(DomainStore.class); + + protected final static String SQL_ID = "domainid"; + protected final static String SQL_NAME = "name"; + protected final static String SQL_DESCR = "description"; + protected final static String SQL_ENABLED = "enabled"; + private static final String TABLE_NAME = "DOMAINS"; + + protected DomainStore() { + super(TABLE_NAME); + } + + @Override + protected String getTableCreationStatement() { + return "CREATE TABLE DOMAINS " + + "(domainid VARCHAR(128) PRIMARY KEY," + + "name VARCHAR(128) UNIQUE NOT NULL, " + + "description VARCHAR(128) , " + + "enabled INTEGER NOT NULL)"; + } + + @Override + protected Domain fromResultSet(ResultSet rs) throws SQLException { + Domain domain = new Domain(); + domain.setDomainid(rs.getString(SQL_ID)); + domain.setName(rs.getString(SQL_NAME)); + domain.setDescription(rs.getString(SQL_DESCR)); + domain.setEnabled(rs.getInt(SQL_ENABLED) == 1); + return domain; + } + + protected Domains getDomains() throws StoreException { + Domains domains = new Domains(); + domains.setDomains(listAll()); + return domains; + } + + protected Domains getDomains(String domainName) throws StoreException { + LOG.debug("getDomains for: {}", domainName); + Domains domains = new Domains(); + try (Connection conn = dbConnect(); + PreparedStatement pstmt = conn.prepareStatement("SELECT * FROM DOMAINS WHERE name = ?")) { + pstmt.setString(1, domainName); + LOG.debug("query string: {}", pstmt.toString()); + domains.setDomains(listFromStatement(pstmt)); + } catch (SQLException e) { + LOG.error("Error listing domains matching {}", domainName, e); + throw new StoreException("Error listing domains", e); + } + return domains; + } + + protected Domain getDomain(String id) throws StoreException { + try (Connection conn = dbConnect(); + PreparedStatement pstmt = conn.prepareStatement("SELECT * FROM DOMAINS WHERE domainid = ? ")) { + pstmt.setString(1, id); + LOG.debug("query string: {}", pstmt.toString()); + return firstFromStatement(pstmt); + } catch (SQLException e) { + LOG.error("Error retrieving domain {}", id, e); + throw new StoreException("Error loading domain", e); + } + } + + protected Domain createDomain(Domain domain) throws StoreException { + Preconditions.checkNotNull(domain); + Preconditions.checkNotNull(domain.getName()); + Preconditions.checkNotNull(domain.isEnabled()); + String query = "insert into DOMAINS (domainid,name,description,enabled) values(?, ?, ?, ?)"; + try (Connection conn = dbConnect(); + PreparedStatement statement = conn.prepareStatement(query)) { + statement.setString(1, domain.getName()); + statement.setString(2, domain.getName()); + statement.setString(3, domain.getDescription()); + statement.setInt(4, domain.isEnabled() ? 1 : 0); + int affectedRows = statement.executeUpdate(); + if (affectedRows == 0) { + throw new StoreException("Creating domain failed, no rows affected."); + } + domain.setDomainid(domain.getName()); + return domain; + } catch (SQLException e) { + LOG.error("Error creating domain {}", domain.getName(), e); + throw new StoreException("Error creating domain", e); + } + } + + protected Domain putDomain(Domain domain) throws StoreException { + Domain savedDomain = this.getDomain(domain.getDomainid()); + if (savedDomain == null) { + return null; + } + + if (domain.getDescription() != null) { + savedDomain.setDescription(domain.getDescription()); + } + if (domain.getName() != null) { + savedDomain.setName(domain.getName()); + } + if (domain.isEnabled() != null) { + savedDomain.setEnabled(domain.isEnabled()); + } + + String query = "UPDATE DOMAINS SET description = ?, enabled = ? WHERE domainid = ?"; + try (Connection conn = dbConnect(); + PreparedStatement statement = conn.prepareStatement(query)) { + statement.setString(1, savedDomain.getDescription()); + statement.setInt(2, savedDomain.isEnabled() ? 1 : 0); + statement.setString(3, savedDomain.getDomainid()); + statement.executeUpdate(); + } catch (SQLException e) { + LOG.error("Error updating domain {}", domain.getDomainid(), e); + throw new StoreException("Error updating domain", e); + } + + return savedDomain; + } + + protected Domain deleteDomain(String domainid) throws StoreException { + domainid = StringEscapeUtils.escapeHtml4(domainid); + Domain deletedDomain = this.getDomain(domainid); + if (deletedDomain == null) { + return null; + } + String query = String.format("DELETE FROM DOMAINS WHERE domainid = '%s'", domainid); + try (Connection conn = dbConnect(); + Statement statement = conn.createStatement()) { + int deleteCount = statement.executeUpdate(query); + LOG.debug("deleted {} records", deleteCount); + return deletedDomain; + } catch (SQLException e) { + LOG.error("Error deleting domain {}", domainid, e); + throw new StoreException("Error deleting domain", e); + } + } +} diff --git a/odl-aaa-moon/aaa/aaa-h2-store/src/main/java/org/opendaylight/aaa/h2/persistence/GrantStore.java b/odl-aaa-moon/aaa/aaa-h2-store/src/main/java/org/opendaylight/aaa/h2/persistence/GrantStore.java new file mode 100644 index 00000000..ee86e0ba --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-h2-store/src/main/java/org/opendaylight/aaa/h2/persistence/GrantStore.java @@ -0,0 +1,158 @@ +/* + * Copyright (c) 2014, 2016 Hewlett-Packard Development Company, L.P. and others. 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.h2.persistence; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; + +import org.apache.commons.lang3.StringEscapeUtils; +import org.opendaylight.aaa.api.IDMStoreUtil; +import org.opendaylight.aaa.api.model.Grant; +import org.opendaylight.aaa.api.model.Grants; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * + * @author peter.mellquist@hp.com + * + */ +public class GrantStore extends AbstractStore<Grant> { + private static final Logger LOG = LoggerFactory.getLogger(GrantStore.class); + + protected final static String SQL_ID = "grantid"; + protected final static String SQL_TENANTID = "domainid"; + protected final static String SQL_USERID = "userid"; + protected final static String SQL_ROLEID = "roleid"; + private static final String TABLE_NAME = "GRANTS"; + + protected GrantStore() { + super(TABLE_NAME); + } + + @Override + protected String getTableCreationStatement() { + return "CREATE TABLE GRANTS " + + "(grantid VARCHAR(128) PRIMARY KEY," + + "domainid VARCHAR(128) NOT NULL, " + + "userid VARCHAR(128) NOT NULL, " + + "roleid VARCHAR(128) NOT NULL)"; + } + + protected Grant fromResultSet(ResultSet rs) throws SQLException { + Grant grant = new Grant(); + try { + grant.setGrantid(rs.getString(SQL_ID)); + grant.setDomainid(rs.getString(SQL_TENANTID)); + grant.setUserid(rs.getString(SQL_USERID)); + grant.setRoleid(rs.getString(SQL_ROLEID)); + } catch (SQLException sqle) { + LOG.error("SQL Exception: ", sqle); + throw sqle; + } + return grant; + } + + protected Grants getGrants(String did, String uid) throws StoreException { + Grants grants = new Grants(); + try (Connection conn = dbConnect(); + PreparedStatement pstmt = conn + .prepareStatement("SELECT * FROM grants WHERE domainid = ? AND userid = ?")) { + pstmt.setString(1, did); + pstmt.setString(2, uid); + LOG.debug("query string: {}", pstmt.toString()); + grants.setGrants(listFromStatement(pstmt)); + } catch (SQLException s) { + throw new StoreException("SQL Exception : " + s); + } + return grants; + } + + protected Grants getGrants(String userid) throws StoreException { + Grants grants = new Grants(); + try (Connection conn = dbConnect(); + PreparedStatement pstmt = conn.prepareStatement("SELECT * FROM GRANTS WHERE userid = ? ")) { + pstmt.setString(1, userid); + LOG.debug("query string: {}", pstmt.toString()); + grants.setGrants(listFromStatement(pstmt)); + } catch (SQLException s) { + throw new StoreException("SQL Exception : " + s); + } + return grants; + } + + protected Grant getGrant(String id) throws StoreException { + try (Connection conn = dbConnect(); + PreparedStatement pstmt = conn.prepareStatement("SELECT * FROM GRANTS WHERE grantid = ? ")) { + pstmt.setString(1, id); + LOG.debug("query string: ", pstmt.toString()); + return firstFromStatement(pstmt); + } catch (SQLException s) { + throw new StoreException("SQL Exception : " + s); + } + } + + protected Grant getGrant(String did, String uid, String rid) throws StoreException { + try (Connection conn = dbConnect(); + PreparedStatement pstmt = conn + .prepareStatement("SELECT * FROM GRANTS WHERE domainid = ? AND userid = ? AND roleid = ? ")) { + pstmt.setString(1, did); + pstmt.setString(2, uid); + pstmt.setString(3, rid); + LOG.debug("query string: {}", pstmt.toString()); + return firstFromStatement(pstmt); + } catch (SQLException s) { + throw new StoreException("SQL Exception : " + s); + } + } + + protected Grant createGrant(Grant grant) throws StoreException { + String query = "insert into grants (grantid,domainid,userid,roleid) values(?,?,?,?)"; + try (Connection conn = dbConnect(); + PreparedStatement statement = conn.prepareStatement(query)) { + statement.setString( + 1, + IDMStoreUtil.createGrantid(grant.getUserid(), grant.getDomainid(), + grant.getRoleid())); + statement.setString(2, grant.getDomainid()); + statement.setString(3, grant.getUserid()); + statement.setString(4, grant.getRoleid()); + int affectedRows = statement.executeUpdate(); + if (affectedRows == 0) { + throw new StoreException("Creating grant failed, no rows affected."); + } + grant.setGrantid(IDMStoreUtil.createGrantid(grant.getUserid(), grant.getDomainid(), + grant.getRoleid())); + return grant; + } catch (SQLException s) { + throw new StoreException("SQL Exception : " + s); + } + } + + protected Grant deleteGrant(String grantid) throws StoreException { + grantid = StringEscapeUtils.escapeHtml4(grantid); + Grant savedGrant = this.getGrant(grantid); + if (savedGrant == null) { + return null; + } + + String query = String.format("DELETE FROM GRANTS WHERE grantid = '%s'", grantid); + try (Connection conn = dbConnect(); + Statement statement = conn.createStatement()) { + int deleteCount = statement.executeUpdate(query); + LOG.debug("deleted {} records", deleteCount); + return savedGrant; + } catch (SQLException s) { + throw new StoreException("SQL Exception : " + s); + } + } +} diff --git a/odl-aaa-moon/aaa/aaa-h2-store/src/main/java/org/opendaylight/aaa/h2/persistence/H2Store.java b/odl-aaa-moon/aaa/aaa-h2-store/src/main/java/org/opendaylight/aaa/h2/persistence/H2Store.java new file mode 100644 index 00000000..da40a17b --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-h2-store/src/main/java/org/opendaylight/aaa/h2/persistence/H2Store.java @@ -0,0 +1,316 @@ +/* + * Copyright (c) 2015 Cisco Systems and others. 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.h2.persistence; + +import java.sql.Connection; +import java.sql.DriverManager; + +import org.opendaylight.aaa.api.IDMStoreException; +import org.opendaylight.aaa.api.IDMStoreUtil; +import org.opendaylight.aaa.api.IIDMStore; +import org.opendaylight.aaa.api.model.Domain; +import org.opendaylight.aaa.api.model.Domains; +import org.opendaylight.aaa.api.model.Grant; +import org.opendaylight.aaa.api.model.Grants; +import org.opendaylight.aaa.api.model.Role; +import org.opendaylight.aaa.api.model.Roles; +import org.opendaylight.aaa.api.model.User; +import org.opendaylight.aaa.api.model.Users; +import org.opendaylight.aaa.h2.config.IdmLightConfig; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class H2Store implements IIDMStore { + + private static final Logger LOG = LoggerFactory.getLogger(H2Store.class); + + private static IdmLightConfig config = new IdmLightConfig(); + private DomainStore domainStore = new DomainStore(); + private UserStore userStore = new UserStore(); + private RoleStore roleStore = new RoleStore(); + private GrantStore grantStore = new GrantStore(); + + public H2Store() { + } + + public static Connection getConnection(Connection existingConnection) throws StoreException { + Connection connection = existingConnection; + try { + if (existingConnection == null || existingConnection.isClosed()) { + new org.h2.Driver(); + connection = DriverManager.getConnection(config.getDbPath(), config.getDbUser(), + config.getDbPwd()); + } + } catch (Exception e) { + throw new StoreException("Cannot connect to database server" + e); + } + + return connection; + } + + public static IdmLightConfig getConfig() { + return config; + } + + @Override + public Domain writeDomain(Domain domain) throws IDMStoreException { + try { + return domainStore.createDomain(domain); + } catch (StoreException e) { + LOG.error("StoreException encountered while writing domain", e); + throw new IDMStoreException(e); + } + } + + @Override + public Domain readDomain(String domainid) throws IDMStoreException { + try { + return domainStore.getDomain(domainid); + } catch (StoreException e) { + LOG.error("StoreException encountered while reading domain", e); + throw new IDMStoreException(e); + } + } + + @Override + public Domain deleteDomain(String domainid) throws IDMStoreException { + try { + return domainStore.deleteDomain(domainid); + } catch (StoreException e) { + LOG.error("StoreException encountered while deleting domain", e); + throw new IDMStoreException(e); + } + } + + @Override + public Domain updateDomain(Domain domain) throws IDMStoreException { + try { + return domainStore.putDomain(domain); + } catch (StoreException e) { + LOG.error("StoreException encountered while updating domain", e); + throw new IDMStoreException(e); + } + } + + @Override + public Domains getDomains() throws IDMStoreException { + try { + return domainStore.getDomains(); + } catch (StoreException e) { + LOG.error("StoreException encountered while reading domains", e); + throw new IDMStoreException(e); + } + } + + @Override + public Role writeRole(Role role) throws IDMStoreException { + try { + return roleStore.createRole(role); + } catch (StoreException e) { + LOG.error("StoreException encountered while writing role", e); + throw new IDMStoreException(e); + } + } + + @Override + public Role readRole(String roleid) throws IDMStoreException { + try { + return roleStore.getRole(roleid); + } catch (StoreException e) { + LOG.error("StoreException encountered while reading role", e); + throw new IDMStoreException(e); + } + } + + @Override + public Role deleteRole(String roleid) throws IDMStoreException { + try { + return roleStore.deleteRole(roleid); + } catch (StoreException e) { + LOG.error("StoreException encountered while deleting role", e); + throw new IDMStoreException(e); + } + } + + @Override + public Role updateRole(Role role) throws IDMStoreException { + try { + return roleStore.putRole(role); + } catch (StoreException e) { + LOG.error("StoreException encountered while updating role", e); + throw new IDMStoreException(e); + } + } + + @Override + public Roles getRoles() throws IDMStoreException { + try { + return roleStore.getRoles(); + } catch (StoreException e) { + LOG.error("StoreException encountered while getting roles", e); + throw new IDMStoreException(e); + } + } + + @Override + public User writeUser(User user) throws IDMStoreException { + try { + return userStore.createUser(user); + } catch (StoreException e) { + LOG.error("StoreException encountered while writing user", e); + throw new IDMStoreException(e); + } + } + + @Override + public User readUser(String userid) throws IDMStoreException { + try { + return userStore.getUser(userid); + } catch (StoreException e) { + LOG.error("StoreException encountered while reading user", e); + throw new IDMStoreException(e); + } + } + + @Override + public User deleteUser(String userid) throws IDMStoreException { + try { + return userStore.deleteUser(userid); + } catch (StoreException e) { + LOG.error("StoreException encountered while deleting user", e); + throw new IDMStoreException(e); + } + } + + @Override + public User updateUser(User user) throws IDMStoreException { + try { + return userStore.putUser(user); + } catch (StoreException e) { + LOG.error("StoreException encountered while updating user", e); + throw new IDMStoreException(e); + } + } + + @Override + public Users getUsers(String username, String domain) throws IDMStoreException { + try { + return userStore.getUsers(username, domain); + } catch (StoreException e) { + LOG.error("StoreException encountered while reading users", e); + throw new IDMStoreException(e); + } + } + + @Override + public Users getUsers() throws IDMStoreException { + try { + return userStore.getUsers(); + } catch (StoreException e) { + LOG.error("StoreException encountered while reading users", e); + throw new IDMStoreException(e); + } + } + + @Override + public Grant writeGrant(Grant grant) throws IDMStoreException { + try { + return grantStore.createGrant(grant); + } catch (StoreException e) { + LOG.error("StoreException encountered while writing grant", e); + throw new IDMStoreException(e); + } + } + + @Override + public Grant readGrant(String grantid) throws IDMStoreException { + try { + return grantStore.getGrant(grantid); + } catch (StoreException e) { + LOG.error("StoreException encountered while reading grant", e); + throw new IDMStoreException(e); + } + } + + @Override + public Grant deleteGrant(String grantid) throws IDMStoreException { + try { + return grantStore.deleteGrant(grantid); + } catch (StoreException e) { + LOG.error("StoreException encountered while deleting grant", e); + throw new IDMStoreException(e); + } + } + + @Override + public Grants getGrants(String domainid, String userid) throws IDMStoreException { + try { + return grantStore.getGrants(domainid, userid); + } catch (StoreException e) { + LOG.error("StoreException encountered while getting grants", e); + throw new IDMStoreException(e); + } + } + + @Override + public Grants getGrants(String userid) throws IDMStoreException { + try { + return grantStore.getGrants(userid); + } catch (StoreException e) { + LOG.error("StoreException encountered while getting grants", e); + throw new IDMStoreException(e); + } + } + + @Override + public Grant readGrant(String domainid, String userid, String roleid) throws IDMStoreException { + return readGrant(IDMStoreUtil.createGrantid(userid, domainid, roleid)); + } + + public static Domain createDomain(String domainName, boolean enable) throws StoreException { + DomainStore ds = new DomainStore(); + Domain d = new Domain(); + d.setName(domainName); + d.setEnabled(enable); + return ds.createDomain(d); + } + + public static User createUser(String name, String password, String domain, String description, + String email, boolean enabled, String SALT) throws StoreException { + UserStore us = new UserStore(); + User u = new User(); + u.setName(name); + u.setDomainid(domain); + u.setDescription(description); + u.setEmail(email); + u.setEnabled(enabled); + u.setPassword(password); + u.setSalt(SALT); + return us.createUser(u); + } + + public static Role createRole(String name, String domain, String description) + throws StoreException { + RoleStore rs = new RoleStore(); + Role r = new Role(); + r.setDescription(description); + r.setName(name); + r.setDomainid(domain); + return rs.createRole(r); + } + + public static Grant createGrant(String domain, String user, String role) throws StoreException { + GrantStore gs = new GrantStore(); + Grant g = new Grant(); + g.setDomainid(domain); + g.setRoleid(role); + g.setUserid(user); + return gs.createGrant(g); + } +} diff --git a/odl-aaa-moon/aaa/aaa-h2-store/src/main/java/org/opendaylight/aaa/h2/persistence/RoleStore.java b/odl-aaa-moon/aaa/aaa-h2-store/src/main/java/org/opendaylight/aaa/h2/persistence/RoleStore.java new file mode 100644 index 00000000..e7defa4a --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-h2-store/src/main/java/org/opendaylight/aaa/h2/persistence/RoleStore.java @@ -0,0 +1,151 @@ +/* + * Copyright (c) 2014, 2016 Hewlett-Packard Development Company, L.P. and others. 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.h2.persistence; + +import com.google.common.base.Preconditions; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; + +import org.apache.commons.lang3.StringEscapeUtils; +import org.opendaylight.aaa.api.IDMStoreUtil; +import org.opendaylight.aaa.api.model.Role; +import org.opendaylight.aaa.api.model.Roles; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * + * @author peter.mellquist@hp.com + * + */ +public class RoleStore extends AbstractStore<Role> { + private static final Logger LOG = LoggerFactory.getLogger(RoleStore.class); + + protected final static String SQL_ID = "roleid"; + protected final static String SQL_DOMAIN_ID = "domainid"; + protected final static String SQL_NAME = "name"; + protected final static String SQL_DESCR = "description"; + private static final String TABLE_NAME = "ROLES"; + + protected RoleStore() { + super(TABLE_NAME); + } + + @Override + protected String getTableCreationStatement() { + return "CREATE TABLE ROLES " + + "(roleid VARCHAR(128) PRIMARY KEY," + + "name VARCHAR(128) NOT NULL, " + + "domainid VARCHAR(128) NOT NULL, " + + "description VARCHAR(128) NOT NULL)"; + } + + protected Role fromResultSet(ResultSet rs) throws SQLException { + Role role = new Role(); + try { + role.setRoleid(rs.getString(SQL_ID)); + role.setDomainid(rs.getString(SQL_DOMAIN_ID)); + role.setName(rs.getString(SQL_NAME)); + role.setDescription(rs.getString(SQL_DESCR)); + } catch (SQLException sqle) { + LOG.error("SQL Exception: ", sqle); + throw sqle; + } + return role; + } + + protected Roles getRoles() throws StoreException { + Roles roles = new Roles(); + roles.setRoles(listAll()); + return roles; + } + + protected Role getRole(String id) throws StoreException { + try (Connection conn = dbConnect(); + PreparedStatement pstmt = conn + .prepareStatement("SELECT * FROM ROLES WHERE roleid = ? ")) { + pstmt.setString(1, id); + LOG.debug("query string: {}", pstmt.toString()); + return firstFromStatement(pstmt); + } catch (SQLException s) { + throw new StoreException("SQL Exception: " + s); + } + } + + protected Role createRole(Role role) throws StoreException { + Preconditions.checkNotNull(role); + Preconditions.checkNotNull(role.getName()); + Preconditions.checkNotNull(role.getDomainid()); + String query = "insert into roles (roleid,domainid,name,description) values(?,?,?,?)"; + try (Connection conn = dbConnect(); + PreparedStatement statement = conn.prepareStatement(query)) { + role.setRoleid(IDMStoreUtil.createRoleid(role.getName(), role.getDomainid())); + statement.setString(1, role.getRoleid()); + statement.setString(2, role.getDomainid()); + statement.setString(3, role.getName()); + statement.setString(4, role.getDescription()); + int affectedRows = statement.executeUpdate(); + if (affectedRows == 0) { + throw new StoreException("Creating role failed, no rows affected."); + } + return role; + } catch (SQLException s) { + throw new StoreException("SQL Exception : " + s); + } + } + + protected Role putRole(Role role) throws StoreException { + + Role savedRole = this.getRole(role.getRoleid()); + if (savedRole == null) { + return null; + } + + if (role.getDescription() != null) { + savedRole.setDescription(role.getDescription()); + } + if (role.getName() != null) { + savedRole.setName(role.getName()); + } + + String query = "UPDATE roles SET description = ? WHERE roleid = ?"; + try (Connection conn = dbConnect(); + PreparedStatement statement = conn.prepareStatement(query)) { + statement.setString(1, savedRole.getDescription()); + statement.setString(2, savedRole.getRoleid()); + statement.executeUpdate(); + } catch (SQLException s) { + throw new StoreException("SQL Exception : " + s); + } + + return savedRole; + } + + protected Role deleteRole(String roleid) throws StoreException { + roleid = StringEscapeUtils.escapeHtml4(roleid); + Role savedRole = this.getRole(roleid); + if (savedRole == null) { + return null; + } + + String query = String.format("DELETE FROM ROLES WHERE roleid = '%s'", roleid); + try (Connection conn = dbConnect(); + Statement statement = conn.createStatement()) { + int deleteCount = statement.executeUpdate(query); + LOG.debug("deleted {} records", deleteCount); + return savedRole; + } catch (SQLException s) { + throw new StoreException("SQL Exception : " + s); + } + } +} diff --git a/odl-aaa-moon/aaa/aaa-h2-store/src/main/java/org/opendaylight/aaa/h2/persistence/StoreException.java b/odl-aaa-moon/aaa/aaa-h2-store/src/main/java/org/opendaylight/aaa/h2/persistence/StoreException.java new file mode 100644 index 00000000..7d2f2b9a --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-h2-store/src/main/java/org/opendaylight/aaa/h2/persistence/StoreException.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2014, 2016 Hewlett-Packard Development Company, L.P. and others. 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.h2.persistence; + +/** + * Exception indicating an error in an H2 data store. + * + * @author peter.mellquist@hp.com + */ + +public class StoreException extends Exception { + public StoreException(String message) { + super(message); + } + + public StoreException(String message, Throwable cause) { + super(message, cause); + } + + public StoreException(Throwable cause) { + super(cause); + } +} diff --git a/odl-aaa-moon/aaa/aaa-h2-store/src/main/java/org/opendaylight/aaa/h2/persistence/UserStore.java b/odl-aaa-moon/aaa/aaa-h2-store/src/main/java/org/opendaylight/aaa/h2/persistence/UserStore.java new file mode 100644 index 00000000..96b8013f --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-h2-store/src/main/java/org/opendaylight/aaa/h2/persistence/UserStore.java @@ -0,0 +1,202 @@ +/* + * Copyright (c) 2014, 2016 Hewlett-Packard Development Company, L.P. and others. 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.h2.persistence; + +import com.google.common.base.Preconditions; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; + +import org.apache.commons.lang3.StringEscapeUtils; +import org.opendaylight.aaa.api.IDMStoreUtil; +import org.opendaylight.aaa.api.SHA256Calculator; +import org.opendaylight.aaa.api.model.User; +import org.opendaylight.aaa.api.model.Users; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * + * @author peter.mellquist@hp.com + * + */ +public class UserStore extends AbstractStore<User> { + private static final Logger LOG = LoggerFactory.getLogger(UserStore.class); + + protected final static String SQL_ID = "userid"; + protected final static String SQL_DOMAIN_ID = "domainid"; + protected final static String SQL_NAME = "name"; + protected final static String SQL_EMAIL = "email"; + protected final static String SQL_PASSWORD = "password"; + protected final static String SQL_DESCR = "description"; + protected final static String SQL_ENABLED = "enabled"; + protected final static String SQL_SALT = "salt"; + private static final String TABLE_NAME = "USERS"; + + protected UserStore() { + super(TABLE_NAME); + } + + @Override + protected String getTableCreationStatement() { + return "CREATE TABLE users " + + "(userid VARCHAR(128) PRIMARY KEY," + + "name VARCHAR(128) NOT NULL, " + + "domainid VARCHAR(128) NOT NULL, " + + "email VARCHAR(128) NOT NULL, " + + "password VARCHAR(128) NOT NULL, " + + "description VARCHAR(128) NOT NULL, " + + "salt VARCHAR(15) NOT NULL, " + + "enabled INTEGER NOT NULL)"; + } + + @Override + protected User fromResultSet(ResultSet rs) throws SQLException { + User user = new User(); + try { + user.setUserid(rs.getString(SQL_ID)); + user.setDomainid(rs.getString(SQL_DOMAIN_ID)); + user.setName(rs.getString(SQL_NAME)); + user.setEmail(rs.getString(SQL_EMAIL)); + user.setPassword(rs.getString(SQL_PASSWORD)); + user.setDescription(rs.getString(SQL_DESCR)); + user.setEnabled(rs.getInt(SQL_ENABLED) == 1); + user.setSalt(rs.getString(SQL_SALT)); + } catch (SQLException sqle) { + LOG.error("SQL Exception: ", sqle); + throw sqle; + } + return user; + } + + protected Users getUsers() throws StoreException { + Users users = new Users(); + users.setUsers(listAll()); + return users; + } + + protected Users getUsers(String username, String domain) throws StoreException { + LOG.debug("getUsers for: {} in domain {}", username, domain); + + Users users = new Users(); + try (Connection conn = dbConnect(); + PreparedStatement pstmt = conn.prepareStatement("SELECT * FROM USERS WHERE userid = ? ")) { + pstmt.setString(1, IDMStoreUtil.createUserid(username, domain)); + LOG.debug("query string: {}", pstmt.toString()); + users.setUsers(listFromStatement(pstmt)); + } catch (SQLException s) { + throw new StoreException("SQL Exception : " + s); + } + return users; + } + + protected User getUser(String id) throws StoreException { + try (Connection conn = dbConnect(); + PreparedStatement pstmt = conn.prepareStatement("SELECT * FROM USERS WHERE userid = ? ")) { + pstmt.setString(1, id); + LOG.debug("query string: {}", pstmt.toString()); + return firstFromStatement(pstmt); + } catch (SQLException s) { + throw new StoreException("SQL Exception : " + s); + } + } + + protected User createUser(User user) throws StoreException { + Preconditions.checkNotNull(user); + Preconditions.checkNotNull(user.getName()); + Preconditions.checkNotNull(user.getDomainid()); + + user.setSalt(SHA256Calculator.generateSALT()); + String query = "insert into users (userid,domainid,name,email,password,description,enabled,salt) values(?,?,?,?,?,?,?,?)"; + try (Connection conn = dbConnect(); + PreparedStatement statement = conn.prepareStatement(query)) { + user.setUserid(IDMStoreUtil.createUserid(user.getName(), user.getDomainid())); + statement.setString(1, user.getUserid()); + statement.setString(2, user.getDomainid()); + statement.setString(3, user.getName()); + statement.setString(4, user.getEmail()); + statement.setString(5, SHA256Calculator.getSHA256(user.getPassword(), user.getSalt())); + statement.setString(6, user.getDescription()); + statement.setInt(7, user.isEnabled() ? 1 : 0); + statement.setString(8, user.getSalt()); + int affectedRows = statement.executeUpdate(); + if (affectedRows == 0) { + throw new StoreException("Creating user failed, no rows affected."); + } + return user; + } catch (SQLException s) { + throw new StoreException("SQL Exception : " + s); + } + } + + protected User putUser(User user) throws StoreException { + + User savedUser = this.getUser(user.getUserid()); + if (savedUser == null) { + return null; + } + + if (user.getDescription() != null) { + savedUser.setDescription(user.getDescription()); + } + if (user.getName() != null) { + savedUser.setName(user.getName()); + } + if (user.isEnabled() != null) { + savedUser.setEnabled(user.isEnabled()); + } + if (user.getEmail() != null) { + savedUser.setEmail(user.getEmail()); + } + if (user.getPassword() != null) { + // If a new salt is provided, use it. Otherwise, derive salt from existing. + String salt = user.getSalt(); + if (salt == null) { + salt = savedUser.getSalt(); + } + savedUser.setPassword(SHA256Calculator.getSHA256(user.getPassword(), salt)); + } + + String query = "UPDATE users SET email = ?, password = ?, description = ?, enabled = ? WHERE userid = ?"; + try (Connection conn = dbConnect(); + PreparedStatement statement = conn.prepareStatement(query)) { + statement.setString(1, savedUser.getEmail()); + statement.setString(2, savedUser.getPassword()); + statement.setString(3, savedUser.getDescription()); + statement.setInt(4, savedUser.isEnabled() ? 1 : 0); + statement.setString(5, savedUser.getUserid()); + statement.executeUpdate(); + } catch (SQLException s) { + throw new StoreException("SQL Exception : " + s); + } + + return savedUser; + } + + protected User deleteUser(String userid) throws StoreException { + userid = StringEscapeUtils.escapeHtml4(userid); + User savedUser = this.getUser(userid); + if (savedUser == null) { + return null; + } + + String query = String.format("DELETE FROM USERS WHERE userid = '%s'", userid); + try (Connection conn = dbConnect(); + Statement statement = conn.createStatement()) { + int deleteCount = statement.executeUpdate(query); + LOG.debug("deleted {} records", deleteCount); + return savedUser; + } catch (SQLException s) { + throw new StoreException("SQL Exception : " + s); + } + } +} diff --git a/odl-aaa-moon/aaa/aaa-h2-store/src/main/java/org/opendaylight/yang/gen/v1/config/aaa/authn/h2/store/rev151128/AAAH2StoreModule.java b/odl-aaa-moon/aaa/aaa-h2-store/src/main/java/org/opendaylight/yang/gen/v1/config/aaa/authn/h2/store/rev151128/AAAH2StoreModule.java new file mode 100644 index 00000000..fe7dd2a6 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-h2-store/src/main/java/org/opendaylight/yang/gen/v1/config/aaa/authn/h2/store/rev151128/AAAH2StoreModule.java @@ -0,0 +1,49 @@ +package org.opendaylight.yang.gen.v1.config.aaa.authn.h2.store.rev151128; + +import org.opendaylight.aaa.api.IIDMStore; +import org.opendaylight.aaa.h2.persistence.H2Store; +import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceRegistration; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class AAAH2StoreModule extends org.opendaylight.yang.gen.v1.config.aaa.authn.h2.store.rev151128.AbstractAAAH2StoreModule { + + private BundleContext bundleContext; + private static final Logger LOG = LoggerFactory.getLogger(AAAH2StoreModule.class); + + public AAAH2StoreModule(org.opendaylight.controller.config.api.ModuleIdentifier identifier, org.opendaylight.controller.config.api.DependencyResolver dependencyResolver) { + super(identifier, dependencyResolver); + } + + public AAAH2StoreModule(org.opendaylight.controller.config.api.ModuleIdentifier identifier, org.opendaylight.controller.config.api.DependencyResolver dependencyResolver, org.opendaylight.yang.gen.v1.config.aaa.authn.h2.store.rev151128.AAAH2StoreModule oldModule, java.lang.AutoCloseable oldInstance) { + super(identifier, dependencyResolver, oldModule, oldInstance); + } + + @Override + public java.lang.AutoCloseable createInstance() { + final H2Store h2Store = new H2Store(); + final ServiceRegistration<?> serviceRegistration = bundleContext.registerService(IIDMStore.class.getName(), h2Store, null); + LOG.info("AAA H2 Store Initialized"); + return new AutoCloseable() { + @Override + public void close() throws Exception { + serviceRegistration.unregister(); + } + }; + } + + /** + * @param bundleContext + */ + public void setBundleContext(BundleContext bundleContext) { + this.bundleContext = bundleContext; + } + + /** + * @return the bundleContext + */ + public BundleContext getBundleContext() { + return bundleContext; + } +} diff --git a/odl-aaa-moon/aaa/aaa-h2-store/src/main/java/org/opendaylight/yang/gen/v1/config/aaa/authn/h2/store/rev151128/AAAH2StoreModuleFactory.java b/odl-aaa-moon/aaa/aaa-h2-store/src/main/java/org/opendaylight/yang/gen/v1/config/aaa/authn/h2/store/rev151128/AAAH2StoreModuleFactory.java new file mode 100644 index 00000000..dc9e7f99 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-h2-store/src/main/java/org/opendaylight/yang/gen/v1/config/aaa/authn/h2/store/rev151128/AAAH2StoreModuleFactory.java @@ -0,0 +1,29 @@ +/* +* Generated file +* +* Generated from: yang module name: aaa-h2-store yang module local name: aaa-h2-store +* Generated by: org.opendaylight.controller.config.yangjmxgenerator.plugin.JMXGenerator +* Generated at: Sat Nov 28 11:00:15 PST 2015 +* +* Do not modify this file unless it is present under src/main directory +*/ +package org.opendaylight.yang.gen.v1.config.aaa.authn.h2.store.rev151128; + +import org.opendaylight.controller.config.api.DependencyResolver; +import org.osgi.framework.BundleContext; + +public class AAAH2StoreModuleFactory extends org.opendaylight.yang.gen.v1.config.aaa.authn.h2.store.rev151128.AbstractAAAH2StoreModuleFactory { + @Override + public AAAH2StoreModule instantiateModule(String instanceName, DependencyResolver dependencyResolver, AAAH2StoreModule oldModule, AutoCloseable oldInstance, BundleContext bundleContext) { + AAAH2StoreModule module = super.instantiateModule(instanceName, dependencyResolver, oldModule, oldInstance, bundleContext); + module.setBundleContext(bundleContext); + return module; + } + + @Override + public AAAH2StoreModule instantiateModule(String instanceName, DependencyResolver dependencyResolver, BundleContext bundleContext) { + AAAH2StoreModule module = super.instantiateModule(instanceName, dependencyResolver, bundleContext); + module.setBundleContext(bundleContext); + return module; + } +} diff --git a/odl-aaa-moon/aaa/aaa-h2-store/src/main/resources/initial/08-aaa-h2-store-config.xml b/odl-aaa-moon/aaa/aaa-h2-store/src/main/resources/initial/08-aaa-h2-store-config.xml new file mode 100644 index 00000000..cfe60812 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-h2-store/src/main/resources/initial/08-aaa-h2-store-config.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- vi: set et smarttab sw=4 tabstop=4: --> +<!-- + Copyright (c) 2015 Cisco Systems, Inc. and others. 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 +--> +<snapshot> + <configuration> + <data xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"> + <modules xmlns="urn:opendaylight:params:xml:ns:yang:controller:config"> + <module> + <type xmlns:authn="config:aaa:authn:h2:store">authn:aaa-h2-store</type> + <name>aaa-h2-store</name> + </module> + </modules> + </data> + </configuration> + <required-capabilities> + <capability>config:aaa:authn:h2:store?module=aaa-h2-store&revision=2015-11-28</capability> + </required-capabilities> + +</snapshot> + diff --git a/odl-aaa-moon/aaa/aaa-h2-store/src/main/yang/aaa-h2-store.yang b/odl-aaa-moon/aaa/aaa-h2-store/src/main/yang/aaa-h2-store.yang new file mode 100644 index 00000000..af2d9bdc --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-h2-store/src/main/yang/aaa-h2-store.yang @@ -0,0 +1,28 @@ +module aaa-h2-store { + yang-version 1; + namespace "config:aaa:authn:h2:store"; + prefix "aaa-h2-store"; + organization "OpenDayLight"; + + import config { prefix config; revision-date 2013-04-05; } + import opendaylight-md-sal-binding { prefix mdsal; revision-date 2013-10-28; } + + contact "saichler@gmail.com"; + + revision 2015-11-28 { + description + "Initial revision."; + } + + identity aaa-h2-store { + base config:module-type; + config:java-name-prefix AAAH2Store; + } + + augment "/config:modules/config:module/config:configuration" { + case aaa-h2-store { + when "/config:modules/config:module/config:type = 'aaa-h2-store'"; + } + } + +} diff --git a/odl-aaa-moon/aaa/aaa-h2-store/src/test/java/org/opendaylight/aaa/h2/persistence/DomainStoreTest.java b/odl-aaa-moon/aaa/aaa-h2-store/src/test/java/org/opendaylight/aaa/h2/persistence/DomainStoreTest.java new file mode 100644 index 00000000..f11a99eb --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-h2-store/src/test/java/org/opendaylight/aaa/h2/persistence/DomainStoreTest.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2014, 2015 Hewlett-Packard Development Company, L.P. and others. 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.h2.persistence; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; +import org.opendaylight.aaa.api.model.Domains; +import org.opendaylight.aaa.h2.persistence.DomainStore; + +public class DomainStoreTest { + + Connection connectionMock = mock(Connection.class); + private final DomainStore domainStoreUnderTest = new DomainStore(); + + @Before + public void setup() { + domainStoreUnderTest.dbConnection = connectionMock; + } + + @After + public void teardown() { + // dts.destroy(); + } + + @Test + public void getDomainsTest() throws SQLException, Exception { + // Setup Mock Behavior + String[] tableTypes = { "TABLE" }; + Mockito.when(connectionMock.isClosed()).thenReturn(false); + DatabaseMetaData dbmMock = mock(DatabaseMetaData.class); + Mockito.when(connectionMock.getMetaData()).thenReturn(dbmMock); + ResultSet rsUserMock = mock(ResultSet.class); + Mockito.when(dbmMock.getTables(null, null, "DOMAINS", tableTypes)).thenReturn(rsUserMock); + Mockito.when(rsUserMock.next()).thenReturn(true); + + Statement stmtMock = mock(Statement.class); + Mockito.when(connectionMock.createStatement()).thenReturn(stmtMock); + + ResultSet rsMock = getMockedResultSet(); + Mockito.when(stmtMock.executeQuery(anyString())).thenReturn(rsMock); + + // Run Test + Domains domains = domainStoreUnderTest.getDomains(); + + // Verify + assertTrue(domains.getDomains().size() == 1); + verify(stmtMock).close(); + } + + public ResultSet getMockedResultSet() throws SQLException { + ResultSet rsMock = mock(ResultSet.class); + Mockito.when(rsMock.next()).thenReturn(true).thenReturn(false); + Mockito.when(rsMock.getInt(DomainStore.SQL_ID)).thenReturn(1); + Mockito.when(rsMock.getString(DomainStore.SQL_NAME)).thenReturn("DomainName_1"); + Mockito.when(rsMock.getString(DomainStore.SQL_DESCR)).thenReturn("Desc_1"); + Mockito.when(rsMock.getInt(DomainStore.SQL_ENABLED)).thenReturn(1); + return rsMock; + } +} diff --git a/odl-aaa-moon/aaa/aaa-h2-store/src/test/java/org/opendaylight/aaa/h2/persistence/GrantStoreTest.java b/odl-aaa-moon/aaa/aaa-h2-store/src/test/java/org/opendaylight/aaa/h2/persistence/GrantStoreTest.java new file mode 100644 index 00000000..168b67e2 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-h2-store/src/test/java/org/opendaylight/aaa/h2/persistence/GrantStoreTest.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2014, 2016 Hewlett-Packard Development Company, L.P. and others. 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.h2.persistence; + +import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; +import org.opendaylight.aaa.api.model.Grants; + +public class GrantStoreTest { + + Connection connectionMock = mock(Connection.class); + private final GrantStore grantStoreUnderTest = new GrantStore(); + private String did = "5"; + private String uid = "5"; + + @Before + public void setup() { + grantStoreUnderTest.dbConnection = connectionMock; + } + + @Test + public void getGrantsTest() throws Exception { + // Setup Mock Behavior + String[] tableTypes = { "TABLE" }; + Mockito.when(connectionMock.isClosed()).thenReturn(false); + DatabaseMetaData dbmMock = mock(DatabaseMetaData.class); + Mockito.when(connectionMock.getMetaData()).thenReturn(dbmMock); + ResultSet rsUserMock = mock(ResultSet.class); + Mockito.when(dbmMock.getTables(null, null, "GRANTS", tableTypes)).thenReturn(rsUserMock); + Mockito.when(rsUserMock.next()).thenReturn(true); + + PreparedStatement pstmtMock = mock(PreparedStatement.class); + Mockito.when(connectionMock.prepareStatement(anyString())).thenReturn(pstmtMock); + + ResultSet rsMock = getMockedResultSet(); + Mockito.when(pstmtMock.executeQuery()).thenReturn(rsMock); + + // Run Test + Grants grants = grantStoreUnderTest.getGrants(did, uid); + + // Verify + assertTrue(grants.getGrants().size() == 1); + verify(pstmtMock).close(); + } + + public ResultSet getMockedResultSet() throws SQLException { + ResultSet rsMock = mock(ResultSet.class); + Mockito.when(rsMock.next()).thenReturn(true).thenReturn(false); + Mockito.when(rsMock.getInt(GrantStore.SQL_ID)).thenReturn(1); + Mockito.when(rsMock.getString(GrantStore.SQL_TENANTID)).thenReturn(did); + Mockito.when(rsMock.getString(GrantStore.SQL_USERID)).thenReturn(uid); + Mockito.when(rsMock.getString(GrantStore.SQL_ROLEID)).thenReturn("Role_1"); + + return rsMock; + + } + +} diff --git a/odl-aaa-moon/aaa/aaa-h2-store/src/test/java/org/opendaylight/aaa/h2/persistence/H2StoreTest.java b/odl-aaa-moon/aaa/aaa-h2-store/src/test/java/org/opendaylight/aaa/h2/persistence/H2StoreTest.java new file mode 100644 index 00000000..f583a302 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-h2-store/src/test/java/org/opendaylight/aaa/h2/persistence/H2StoreTest.java @@ -0,0 +1,187 @@ +/* + * Copyright (c) 2016 Cisco Systems, Inc. and others. 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.h2.persistence; + +import java.io.File; +import java.sql.SQLException; + +import org.junit.AfterClass; +import org.junit.Assert; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.opendaylight.aaa.api.IDMStoreUtil; +import org.opendaylight.aaa.api.IIDMStore; +import org.opendaylight.aaa.api.model.Domain; +import org.opendaylight.aaa.api.model.Grant; +import org.opendaylight.aaa.api.model.Role; +import org.opendaylight.aaa.api.model.User; + +public class H2StoreTest { + @BeforeClass + public static void start() { + File f = new File("idmlight.db.mv.db"); + if (f.exists()) { + f.delete(); + } + f = new File("idmlight.db.trace.db"); + if (f.exists()) { + f.delete(); + } + } + + @AfterClass + public static void end() { + File f = new File("idmlight.db.mv.db"); + if (f.exists()) { + f.delete(); + } + f = new File("idmlight.db.trace.db"); + if (f.exists()) { + f.delete(); + } + } + + @Before + public void before() throws StoreException, SQLException { + UserStore us = new UserStore(); + us.dbClean(); + DomainStore ds = new DomainStore(); + ds.dbClean(); + RoleStore rs = new RoleStore(); + rs.dbClean(); + GrantStore gs = new GrantStore(); + gs.dbClean(); + } + + @Test + public void testCreateDefaultDomain() throws StoreException { + Domain d = new Domain(); + Assert.assertEquals(true, d != null); + DomainStore ds = new DomainStore(); + d.setName(IIDMStore.DEFAULT_DOMAIN); + d.setEnabled(true); + d = ds.createDomain(d); + Assert.assertEquals(true, d != null); + } + + @Test + public void testCreateTempRole() throws StoreException { + Role role = H2Store.createRole("temp", "temp domain", "Temp Testing role"); + Assert.assertEquals(true, role != null); + } + + @Test + public void testCreateUser() throws StoreException { + User user = H2Store.createUser("test", "pass", "domain", "desc", "email", true, "SALT"); + Assert.assertEquals(true, user != null); + } + + @Test + public void testCreateGrant() throws StoreException { + Domain d = H2Store.createDomain("sdn", true); + Role role = H2Store.createRole("temp", "temp domain", "Temp Testing role"); + User user = H2Store.createUser("test", "pass", "domain", "desc", "email", true, "SALT"); + Grant g = H2Store.createGrant(d.getDomainid(), user.getUserid(), role.getRoleid()); + Assert.assertEquals(true, g != null); + } + + @Test + public void testUpdatingUserEmail() throws StoreException { + UserStore us = new UserStore(); + Domain d = H2Store.createDomain("sdn", true); + User user = H2Store.createUser("test", "pass", d.getDomainid(), "desc", "email", true, + "SALT"); + + user.setName("test"); + user = us.putUser(user); + Assert.assertEquals(true, user != null); + + user.setEmail("Test@Test.com"); + user = us.putUser(user); + + user = new User(); + user.setName("test"); + user.setDomainid(d.getDomainid()); + user = us.getUser(IDMStoreUtil.createUserid(user.getName(), user.getDomainid())); + + Assert.assertEquals("Test@Test.com", user.getEmail()); + } + /* + * @Test public void testCreateUserViaAPI() throws StoreException { Domain d + * = StoreBuilder.createDomain("sdn",true); + * + * User user = new User(); user.setName("Hello"); user.setPassword("Hello"); + * user.setDomainid(d.getDomainid()); UserHandler h = new UserHandler(); + * h.createUser(null, user); + * + * User u = new User(); u.setName("Hello"); u.setDomainid(d.getDomainid()); + * UserStore us = new UserStore(); u = + * us.getUser(IDMStoreUtil.createUserid(u.getName(),u.getDomainid())); + * + * Assert.assertEquals(true, u != null); } + * + * @Test public void testUpdateUserViaAPI() throws StoreException { Domain d + * = StoreBuilder.createDomain("sdn",true); + * + * User user = new User(); user.setName("Hello"); user.setPassword("Hello"); + * user.setDomainid(d.getDomainid()); UserHandler h = new UserHandler(); + * h.createUser(null, user); + * + * user.setEmail("Hello@Hello.com"); user.setPassword("Test123"); + * h.putUser(null, user, "" + user.getUserid()); + * + * UserStore us = new UserStore(); + * + * User u = new User(); u.setName("Hello"); u.setDomainid(d.getDomainid()); + * u = us.getUser(IDMStoreUtil.createUserid(u.getName(),u.getDomainid())); + * + * Assert.assertEquals("Hello@Hello.com", u.getEmail()); + * + * String hash = SHA256Calculator.getSHA256("Test123", u.getSalt()); + * Assert.assertEquals(u.getPassword(), hash); } + * + * @Test public void testUpdateUserRoleViaAPI() throws StoreException { + * Domain d = StoreBuilder.createDomain("sdn",true); Role role1 = + * StoreBuilder.createRole("temp1",d.getDomainid(),"Temp Testing role"); + * Role role2 = + * StoreBuilder.createRole("temp2",d.getDomainid(),"Temp Testing role"); + * + * User user = new User(); user.setName("Hello"); user.setPassword("Hello"); + * user.setDomainid(d.getDomainid()); + * + * UserHandler h = new UserHandler(); h.createUser(null, user); + * + * user.setEmail("Hello@Hello.com"); user.setPassword("Test123"); + * h.putUser(null, user, user.getUserid()); + * + * Grant g = new Grant(); g.setUserid(user.getUserid()); + * g.setDomainid(d.getDomainid()); g.setRoleid(role1.getRoleid()); + * GrantStore gs = new GrantStore(); g = gs.createGrant(g); + * + * Assert.assertEquals(true, g != null); Assert.assertEquals(g.getRoleid(), + * role1.getRoleid()); + * + * g = gs.deleteGrant(IDMStoreUtil.createGrantid(user.getUserid(), + * d.getDomainid(), role1.getRoleid())); g.setRoleid(role2.getRoleid()); g = + * gs.createGrant(g); + * + * Assert.assertEquals(true, g != null); Assert.assertEquals(g.getRoleid(), + * role2.getRoleid()); + * + * User u = new User(); u.setName("Hello"); u.setDomainid(d.getDomainid()); + * UserStore us = new UserStore(); u = + * us.getUser(IDMStoreUtil.createUserid(u.getName(),u.getDomainid())); + * + * Assert.assertEquals("Hello@Hello.com", u.getEmail()); + * + * String hash = SHA256Calculator.getSHA256("Test123", u.getSalt()); + * Assert.assertEquals(true, hash.equals(u.getPassword())); } + */ +} diff --git a/odl-aaa-moon/aaa/aaa-h2-store/src/test/java/org/opendaylight/aaa/h2/persistence/RoleStoreTest.java b/odl-aaa-moon/aaa/aaa-h2-store/src/test/java/org/opendaylight/aaa/h2/persistence/RoleStoreTest.java new file mode 100644 index 00000000..37cb17a6 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-h2-store/src/test/java/org/opendaylight/aaa/h2/persistence/RoleStoreTest.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2014, 2015 Hewlett-Packard Development Company, L.P. and others. 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.h2.persistence; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; +import org.opendaylight.aaa.api.model.Roles; +import org.opendaylight.aaa.h2.persistence.RoleStore; + +public class RoleStoreTest { + + Connection connectionMock = mock(Connection.class); + private final RoleStore RoleStoreUnderTest = new RoleStore(); + + @Before + public void setup() { + RoleStoreUnderTest.dbConnection = connectionMock; + } + + @After + public void teardown() { + // dts.destroy(); + } + + @Test + public void getRolesTest() throws SQLException, Exception { + // Setup Mock Behavior + String[] tableTypes = { "TABLE" }; + Mockito.when(connectionMock.isClosed()).thenReturn(false); + DatabaseMetaData dbmMock = mock(DatabaseMetaData.class); + Mockito.when(connectionMock.getMetaData()).thenReturn(dbmMock); + ResultSet rsUserMock = mock(ResultSet.class); + Mockito.when(dbmMock.getTables(null, null, "ROLES", tableTypes)).thenReturn(rsUserMock); + Mockito.when(rsUserMock.next()).thenReturn(true); + + Statement stmtMock = mock(Statement.class); + Mockito.when(connectionMock.createStatement()).thenReturn(stmtMock); + + ResultSet rsMock = getMockedResultSet(); + Mockito.when(stmtMock.executeQuery(anyString())).thenReturn(rsMock); + + // Run Test + Roles roles = RoleStoreUnderTest.getRoles(); + + // Verify + assertTrue(roles.getRoles().size() == 1); + verify(stmtMock).close(); + + } + + public ResultSet getMockedResultSet() throws SQLException { + ResultSet rsMock = mock(ResultSet.class); + Mockito.when(rsMock.next()).thenReturn(true).thenReturn(false); + Mockito.when(rsMock.getInt(RoleStore.SQL_ID)).thenReturn(1); + Mockito.when(rsMock.getString(RoleStore.SQL_NAME)).thenReturn("RoleName_1"); + Mockito.when(rsMock.getString(RoleStore.SQL_DESCR)).thenReturn("Desc_1"); + return rsMock; + } +} diff --git a/odl-aaa-moon/aaa/aaa-h2-store/src/test/java/org/opendaylight/aaa/h2/persistence/UserStoreTest.java b/odl-aaa-moon/aaa/aaa-h2-store/src/test/java/org/opendaylight/aaa/h2/persistence/UserStoreTest.java new file mode 100644 index 00000000..e214c261 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-h2-store/src/test/java/org/opendaylight/aaa/h2/persistence/UserStoreTest.java @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2014, 2015 Hewlett-Packard Development Company, L.P. and others. 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.h2.persistence; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; +import org.opendaylight.aaa.api.model.Users; +import org.opendaylight.aaa.h2.persistence.UserStore; + +public class UserStoreTest { + + Connection connectionMock = mock(Connection.class); + private final UserStore userStoreUnderTest = new UserStore(); + + @Before + public void setup() { + userStoreUnderTest.dbConnection = connectionMock; + } + + @After + public void teardown() { + // dts.destroy(); + } + + @Test + public void getUsersTest() throws SQLException, Exception { + // Setup Mock Behavior + String[] tableTypes = { "TABLE" }; + Mockito.when(connectionMock.isClosed()).thenReturn(false); + DatabaseMetaData dbmMock = mock(DatabaseMetaData.class); + Mockito.when(connectionMock.getMetaData()).thenReturn(dbmMock); + ResultSet rsUserMock = mock(ResultSet.class); + Mockito.when(dbmMock.getTables(null, null, "USERS", tableTypes)).thenReturn(rsUserMock); + Mockito.when(rsUserMock.next()).thenReturn(true); + + Statement stmtMock = mock(Statement.class); + Mockito.when(connectionMock.createStatement()).thenReturn(stmtMock); + + ResultSet rsMock = getMockedResultSet(); + Mockito.when(stmtMock.executeQuery(anyString())).thenReturn(rsMock); + + // Run Test + Users users = userStoreUnderTest.getUsers(); + + // Verify + assertTrue(users.getUsers().size() == 1); + verify(stmtMock).close(); + + } + + public ResultSet getMockedResultSet() throws SQLException { + ResultSet rsMock = mock(ResultSet.class); + Mockito.when(rsMock.next()).thenReturn(true).thenReturn(false); + Mockito.when(rsMock.getInt(UserStore.SQL_ID)).thenReturn(1); + Mockito.when(rsMock.getString(UserStore.SQL_NAME)).thenReturn("Name_1"); + Mockito.when(rsMock.getString(UserStore.SQL_EMAIL)).thenReturn("Name_1@company.com"); + Mockito.when(rsMock.getString(UserStore.SQL_PASSWORD)).thenReturn("Pswd_1"); + Mockito.when(rsMock.getString(UserStore.SQL_DESCR)).thenReturn("Desc_1"); + Mockito.when(rsMock.getInt(UserStore.SQL_ENABLED)).thenReturn(1); + return rsMock; + } +} diff --git a/odl-aaa-moon/aaa/aaa-idmlight/pom.xml b/odl-aaa-moon/aaa/aaa-idmlight/pom.xml new file mode 100644 index 00000000..2ca5ff69 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-idmlight/pom.xml @@ -0,0 +1,229 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + <parent> + <groupId>org.opendaylight.aaa</groupId> + <artifactId>aaa-parent</artifactId> + <version>0.3.2-Beryllium-SR2</version> + <relativePath>../parent</relativePath> + </parent> + + <artifactId>aaa-idmlight</artifactId> + <packaging>bundle</packaging> + + <dependencies> + <!--Yang Binding --> + <dependency> + <groupId>org.opendaylight.controller</groupId> + <artifactId>config-api</artifactId> + <version>${config.version}</version> + </dependency> + <dependency> + <groupId>org.opendaylight.controller</groupId> + <artifactId>sal-binding-config</artifactId> + </dependency> + <dependency> + <groupId>org.opendaylight.controller</groupId> + <artifactId>sal-binding-api</artifactId> + </dependency> + <dependency> + <groupId>org.opendaylight.controller</groupId> + <artifactId>sal-common-util</artifactId> + </dependency> + + <dependency> + <groupId>org.opendaylight.aaa</groupId> + <artifactId>aaa-authn-api</artifactId> + </dependency> + <dependency> + <groupId>org.opendaylight.aaa</groupId> + <artifactId>aaa-authn</artifactId> + </dependency> + <dependency> + <groupId>org.slf4j</groupId> + <artifactId>slf4j-api</artifactId> + </dependency> + <dependency> + <groupId>com.sun.jersey</groupId> + <artifactId>jersey-server</artifactId> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>javax.servlet</groupId> + <artifactId>javax.servlet-api</artifactId> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>org.apache.felix</groupId> + <artifactId>org.apache.felix.dependencymanager</artifactId> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>org.mockito</groupId> + <artifactId>mockito-all</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.osgi</groupId> + <artifactId>org.osgi.core</artifactId> + </dependency> + + <!-- JSON JAXB Stuff --> + <dependency> + <groupId>com.fasterxml.jackson.core</groupId> + <artifactId>jackson-core</artifactId> + </dependency> + <dependency> + <groupId>com.fasterxml.jackson.core</groupId> + <artifactId>jackson-annotations</artifactId> + </dependency> + <dependency> + <groupId>com.fasterxml.jackson.core</groupId> + <artifactId>jackson-databind</artifactId> + </dependency> + <dependency> + <groupId>com.fasterxml.jackson.datatype</groupId> + <artifactId>jackson-datatype-json-org</artifactId> + </dependency> + <dependency> + <groupId>com.fasterxml.jackson.jaxrs</groupId> + <artifactId>jackson-jaxrs-base</artifactId> + </dependency> + <dependency> + <groupId>com.fasterxml.jackson.jaxrs</groupId> + <artifactId>jackson-jaxrs-json-provider</artifactId> + </dependency> + <dependency> + <groupId>com.fasterxml.jackson.module</groupId> + <artifactId>jackson-module-jaxb-annotations</artifactId> + </dependency> + + <dependency> + <groupId>org.eclipse.jetty</groupId> + <artifactId>jetty-servlets</artifactId> + <scope>provided</scope> + </dependency> + + <!-- Testing Dependencies --> + <dependency> + <groupId>com.sun.jersey.jersey-test-framework</groupId> + <artifactId>jersey-test-framework-grizzly2</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>junit</groupId> + <artifactId>junit</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.slf4j</groupId> + <artifactId>slf4j-simple</artifactId> + <scope>test</scope> + </dependency> + + </dependencies> + + <build> + <plugins> + <plugin> + <groupId>org.opendaylight.yangtools</groupId> + <artifactId>yang-maven-plugin</artifactId> + <version>${yangtools.version}</version> + <executions> + <execution> + <id>config</id> + <goals> + <goal>generate-sources</goal> + </goals> + <configuration> + <codeGenerators> + <generator> + <codeGeneratorClass>org.opendaylight.controller.config.yangjmxgenerator.plugin.JMXGenerator</codeGeneratorClass> + <outputBaseDir>${jmxGeneratorPath}</outputBaseDir> + <additionalConfiguration> + <namespaceToPackage1>urn:opendaylight:params:xml:ns:yang:controller==org.opendaylight.controller.config.yang</namespaceToPackage1> + </additionalConfiguration> + </generator> + <generator> + <codeGeneratorClass>org.opendaylight.yangtools.maven.sal.api.gen.plugin.CodeGeneratorImpl</codeGeneratorClass> + <outputBaseDir>${salGeneratorPath}</outputBaseDir> + </generator> + </codeGenerators> + <inspectDependencies>true</inspectDependencies> + </configuration> + </execution> + </executions> + <dependencies> + <dependency> + <groupId>org.opendaylight.mdsal</groupId> + <artifactId>maven-sal-api-gen-plugin</artifactId> + <version>${yangtools.version}</version> + <type>jar</type> + </dependency> + <dependency> + <groupId>org.opendaylight.controller</groupId> + <artifactId>yang-jmx-generator-plugin</artifactId> + <version>${config.version}</version> + </dependency> + </dependencies> + </plugin> + <plugin> + <groupId>org.codehaus.mojo</groupId> + <artifactId>build-helper-maven-plugin</artifactId> + <executions> + <execution> + <id>attach-artifacts</id> + <goals> + <goal>attach-artifact</goal> + </goals> + <phase>package</phase> + <configuration> + <artifacts> + <artifact> + <file>${project.build.directory}/classes/initial/08-aaa-idmlight-config.xml</file> + <type>xml</type> + <classifier>config</classifier> + </artifact> + </artifacts> + </configuration> + </execution> + <execution> + <id>attach-artifacts-idmtool</id> + <goals> + <goal>attach-artifact</goal> + </goals> + <phase>package</phase> + <configuration> + <artifacts> + <artifact> + <file>${project.build.directory}/classes/idmtool.py</file> + <type>py</type> + <classifier>config</classifier> + </artifact> + </artifacts> + </configuration> + </execution> + + </executions> + </plugin> + <plugin> + <groupId>org.apache.felix</groupId> + <artifactId>maven-bundle-plugin</artifactId> + <!-- override default version so we don't use bnd 2.3.0 when embedding sqlite --> + + <extensions>true</extensions> + <configuration> + <instructions> + <Import-Package>org.opendaylight.aaa.shiro.realm,org.apache.shiro.web.env,org.apache.shiro.authc,org.opendaylight.aaa.shiro.web.env,org.opendaylight.aaa.shiro.filters,javax.servlet.http,javax.ws.rs,javax.ws.rs.core,javax.xml.bind.annotation,org.apache.felix.dm,org.opendaylight.aaa,org.opendaylight.aaa.api.*,org.osgi.framework,org.slf4j,org.eclipse.jetty.servlets,com.sun.jersey.spi.container.servlet,com.google.*,org.opendaylight.*,org.osgi.util.tracker</Import-Package> + <Web-ContextPath>/auth</Web-ContextPath> + <!--<Web-Connectors>adminConn</Web-Connectors> --> + <!--Bundle-Activator>org.opendaylight.aaa.idm.Activator</Bundle-Activator--> + </instructions> + <manifestLocation>${project.basedir}/META-INF</manifestLocation> + </configuration> + </plugin> + </plugins> + </build> + +</project> diff --git a/odl-aaa-moon/aaa/aaa-idmlight/src/main/java/org/opendaylight/aaa/idm/IdmLightApplication.java b/odl-aaa-moon/aaa/aaa-idmlight/src/main/java/org/opendaylight/aaa/idm/IdmLightApplication.java new file mode 100644 index 00000000..6fcba5d6 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-idmlight/src/main/java/org/opendaylight/aaa/idm/IdmLightApplication.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2014, 2015 Hewlett-Packard Development Company, L.P. and others. 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.idm; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +import javax.ws.rs.core.Application; + +import org.opendaylight.aaa.idm.rest.DomainHandler; +import org.opendaylight.aaa.idm.rest.RoleHandler; +import org.opendaylight.aaa.idm.rest.UserHandler; +import org.opendaylight.aaa.idm.rest.VersionHandler; + +/** + * A JAX-RS application for IdmLight. The REST endpoints delivered by this + * application are in the form: + * <code>http://{HOST}:{PORT}/auth/v1/</code> + * + * For example, the users REST endpoint is: + * <code>http://{HOST}:{PORT}/auth/v1/users</code> + * + * This application is responsible for interaction with the backing h2 + * database store. + * + * @author liemmn + * @author Ryan Goulding (ryandgoulding@gmail.com) + * @see <code>org.opendaylight.aaa.idm.rest.DomainHandler</code> + * @see <code>org.opendaylight.aaa.idm.rest.UserHandler</code> + * @see <code>org.opendaylight.aaa.idm.rest.RoleHandler</code> + */ +public class IdmLightApplication extends Application { + + //TODO create a bug to address the fact that the implementation assumes 128 + // as the max length, even though this claims 256. + /** + * The maximum field length for identity fields. + */ + public static final int MAX_FIELD_LEN = 256; + public IdmLightApplication() { + } + + @Override + public Set<Class<?>> getClasses() { + return new HashSet<Class<?>>(Arrays.asList(VersionHandler.class, + DomainHandler.class, + RoleHandler.class, + UserHandler.class)); + } +} diff --git a/odl-aaa-moon/aaa/aaa-idmlight/src/main/java/org/opendaylight/aaa/idm/IdmLightProxy.java b/odl-aaa-moon/aaa/aaa-idmlight/src/main/java/org/opendaylight/aaa/idm/IdmLightProxy.java new file mode 100644 index 00000000..d17d2b13 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-idmlight/src/main/java/org/opendaylight/aaa/idm/IdmLightProxy.java @@ -0,0 +1,208 @@ +/* + * Copyright (c) 2014, 2015 Hewlett-Packard Development Company, L.P. and others. 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.idm; + +import com.google.common.base.Preconditions; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import org.opendaylight.aaa.ClaimBuilder; +import org.opendaylight.aaa.api.AuthenticationException; +import org.opendaylight.aaa.api.Claim; +import org.opendaylight.aaa.api.CredentialAuth; +import org.opendaylight.aaa.api.IDMStoreException; +import org.opendaylight.aaa.api.IIDMStore; +import org.opendaylight.aaa.api.IdMService; +import org.opendaylight.aaa.api.PasswordCredentials; +import org.opendaylight.aaa.api.SHA256Calculator; +import org.opendaylight.aaa.api.model.Domain; +import org.opendaylight.aaa.api.model.Grant; +import org.opendaylight.aaa.api.model.Grants; +import org.opendaylight.aaa.api.model.Role; +import org.opendaylight.aaa.api.model.User; +import org.opendaylight.aaa.api.model.Users; +import org.opendaylight.yang.gen.v1.config.aaa.authn.idmlight.rev151204.AAAIDMLightModule; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * An OSGi proxy for the IdmLight server. + * + */ +public class IdmLightProxy implements CredentialAuth<PasswordCredentials>, IdMService { + + private static final Logger LOG = LoggerFactory.getLogger(IdmLightProxy.class); + + /** + * claimCache is responsible for storing the active claims per domain. The + * outer map is keyed by domain, and the inner map is keyed by + * <code>PasswordCredentials</code>. + */ + private static Map<String, Map<PasswordCredentials, Claim>> claimCache = new ConcurrentHashMap<>(); + + // adds a store for the default "sdn" domain + static { + claimCache.put(IIDMStore.DEFAULT_DOMAIN, + new ConcurrentHashMap<PasswordCredentials, Claim>()); + } + + @Override + public Claim authenticate(PasswordCredentials creds) { + Preconditions.checkNotNull(creds); + Preconditions.checkNotNull(creds.username()); + Preconditions.checkNotNull(creds.password()); + String domain = creds.domain() == null ? IIDMStore.DEFAULT_DOMAIN : creds.domain(); + // FIXME: Add cache invalidation + Map<PasswordCredentials, Claim> cache = claimCache.get(domain); + if (cache == null) { + cache = new ConcurrentHashMap<PasswordCredentials, Claim>(); + claimCache.put(domain, cache); + } + Claim claim = cache.get(creds); + if (claim == null) { + synchronized (claimCache) { + claim = cache.get(creds); + if (claim == null) { + claim = dbAuthenticate(creds); + if (claim != null) { + cache.put(creds, claim); + } + } + } + } + return claim; + } + + /** + * Clears the cache of any active claims. + */ + public static synchronized void clearClaimCache() { + LOG.info("Clearing the claim cache"); + for (Map<PasswordCredentials, Claim> cache : claimCache.values()) { + cache.clear(); + } + } + + private static Claim dbAuthenticate(PasswordCredentials creds) { + Domain domain = null; + User user = null; + String credsDomain = creds.domain() == null ? IIDMStore.DEFAULT_DOMAIN : creds.domain(); + // check to see domain exists + // TODO: ensure domain names are unique change to 'getDomain' + LOG.debug("get domain"); + try { + domain = AAAIDMLightModule.getStore().readDomain(credsDomain); + if (domain == null) { + throw new AuthenticationException("Domain :" + credsDomain + " does not exist"); + } + } catch (IDMStoreException e) { + throw new AuthenticationException("Error while fetching domain", e); + } + + // check to see user exists and passes cred check + try { + LOG.debug("check user / pwd"); + Users users = AAAIDMLightModule.getStore().getUsers(creds.username(), credsDomain); + List<User> userList = users.getUsers(); + if (userList.size() == 0) { + throw new AuthenticationException("User :" + creds.username() + + " does not exist in domain " + credsDomain); + } + user = userList.get(0); + if (!SHA256Calculator.getSHA256(creds.password(), user.getSalt()).equals( + user.getPassword())) { + throw new AuthenticationException("UserName / Password not found"); + } + + // get all grants & roles for this domain and user + LOG.debug("get grants"); + List<String> roles = new ArrayList<String>(); + Grants grants = AAAIDMLightModule.getStore().getGrants(domain.getDomainid(), + user.getUserid()); + List<Grant> grantList = grants.getGrants(); + for (int z = 0; z < grantList.size(); z++) { + Grant grant = grantList.get(z); + Role role = AAAIDMLightModule.getStore().readRole(grant.getRoleid()); + if (role != null) { + roles.add(role.getName()); + } + } + + // build up the claim + LOG.debug("build a claim"); + ClaimBuilder claim = new ClaimBuilder(); + claim.setUserId(user.getUserid().toString()); + claim.setUser(creds.username()); + claim.setDomain(credsDomain); + for (int z = 0; z < roles.size(); z++) { + claim.addRole(roles.get(z)); + } + return claim.build(); + } catch (IDMStoreException se) { + throw new AuthenticationException("idm data store exception :" + se.toString() + se); + } + } + + @Override + public List<String> listDomains(String userId) { + LOG.debug("list Domains for userId: {}", userId); + List<String> domains = new ArrayList<String>(); + try { + Grants grants = AAAIDMLightModule.getStore().getGrants(userId); + List<Grant> grantList = grants.getGrants(); + for (int z = 0; z < grantList.size(); z++) { + Grant grant = grantList.get(z); + Domain domain = AAAIDMLightModule.getStore().readDomain(grant.getDomainid()); + domains.add(domain.getName()); + } + return domains; + } catch (IDMStoreException se) { + LOG.warn("error getting domains ", se.toString(), se); + return domains; + } + + } + + @Override + public List<String> listRoles(String userId, String domainName) { + LOG.debug("listRoles"); + List<String> roles = new ArrayList<String>(); + + try { + // find domain name for specied domain name + String did = null; + try { + Domain domain = AAAIDMLightModule.getStore().readDomain(domainName); + if (domain == null) { + LOG.debug("DomainName: {}", domainName + " Not found!"); + return roles; + } + did = domain.getDomainid(); + } catch (IDMStoreException e) { + return roles; + } + + // find all grants for uid and did + Grants grants = AAAIDMLightModule.getStore().getGrants(did, userId); + List<Grant> grantList = grants.getGrants(); + for (int z = 0; z < grantList.size(); z++) { + Grant grant = grantList.get(z); + Role role = AAAIDMLightModule.getStore().readRole(grant.getRoleid()); + roles.add(role.getName()); + } + + return roles; + } catch (IDMStoreException se) { + LOG.warn("error getting roles ", se.toString(), se); + return roles; + } + } +} diff --git a/odl-aaa-moon/aaa/aaa-idmlight/src/main/java/org/opendaylight/aaa/idm/StoreBuilder.java b/odl-aaa-moon/aaa/aaa-idmlight/src/main/java/org/opendaylight/aaa/idm/StoreBuilder.java new file mode 100644 index 00000000..111665c6 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-idmlight/src/main/java/org/opendaylight/aaa/idm/StoreBuilder.java @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2014, 2015 Hewlett-Packard Development Company, L.P. and others. 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.idm; + +import org.opendaylight.aaa.api.IDMStoreException; +import org.opendaylight.aaa.api.IIDMStore; +import org.opendaylight.aaa.api.model.Domain; +import org.opendaylight.aaa.api.model.Grant; +import org.opendaylight.aaa.api.model.Role; +import org.opendaylight.aaa.api.model.User; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * StoreBuilder is triggered during feature installation by + * <code>AAAIDMLightModule.createInstance()</code>. StoreBuilder is responsible + * for initializing the H2 database with initial default user account + * information. By default, the following users are created: + * <ol> + * <li>admin</li> + * <li>user</li> + * </ol> + * + * By default, the following domain is created: + * <ol> + * <li>sdn</li> + * </ol> + * + * By default, the following grants are created: + * <ol> + * <li>admin with admin role on sdn</li> + * <li>admin with user role on sdn</li> + * <li>user with user role on sdn</li> + * </ol> + * + * @author peter.mellquist@hp.com + * @author saichler@cisco.com + */ +public class StoreBuilder { + + private static final Logger LOG = LoggerFactory.getLogger(StoreBuilder.class); + + public static void init(IIDMStore store) throws IDMStoreException { + LOG.info("creating idmlight schema in store"); + + // Check whether the default domain exists. If it exists, then do not + // create default data in the store. + // TODO Address the fact that someone may delete the sdn domain, or make + // sdn mandatory. + Domain defaultDomain = store.readDomain(IIDMStore.DEFAULT_DOMAIN); + if (defaultDomain != null) { + LOG.info("Found default domain in Store, skipping insertion of default data"); + return; + } + + // make domain + Domain domain = new Domain(); + User adminUser = new User(); + User userUser = new User(); + Role adminRole = new Role(); + Role userRole = new Role(); + domain.setEnabled(true); + domain.setName(IIDMStore.DEFAULT_DOMAIN); + domain.setDescription("default odl sdn domain"); + domain = store.writeDomain(domain); + + // Create default users + // "admin" user + adminUser.setEnabled(true); + adminUser.setName("admin"); + adminUser.setDomainid(domain.getDomainid()); + adminUser.setDescription("admin user"); + adminUser.setEmail(""); + adminUser.setPassword("admin"); + adminUser = store.writeUser(adminUser); + // "user" user + userUser.setEnabled(true); + userUser.setName("user"); + userUser.setDomainid(domain.getDomainid()); + userUser.setDescription("user user"); + userUser.setEmail(""); + userUser.setPassword("user"); + userUser = store.writeUser(userUser); + + // Create default Roles ("admin" and "user") + adminRole.setName("admin"); + adminRole.setDomainid(domain.getDomainid()); + adminRole.setDescription("a role for admins"); + adminRole = store.writeRole(adminRole); + userRole.setName("user"); + userRole.setDomainid(domain.getDomainid()); + userRole.setDescription("a role for users"); + userRole = store.writeRole(userRole); + + // Create default grants + Grant grant = new Grant(); + grant.setDomainid(domain.getDomainid()); + grant.setUserid(userUser.getUserid()); + grant.setRoleid(userRole.getRoleid()); + grant = store.writeGrant(grant); + + grant.setDomainid(domain.getDomainid()); + grant.setUserid(adminUser.getUserid()); + grant.setRoleid(userRole.getRoleid()); + grant = store.writeGrant(grant); + + grant.setDomainid(domain.getDomainid()); + grant.setUserid(adminUser.getUserid()); + grant.setRoleid(adminRole.getRoleid()); + grant = store.writeGrant(grant); + } +} diff --git a/odl-aaa-moon/aaa/aaa-idmlight/src/main/java/org/opendaylight/aaa/idm/rest/DomainHandler.java b/odl-aaa-moon/aaa/aaa-idmlight/src/main/java/org/opendaylight/aaa/idm/rest/DomainHandler.java new file mode 100644 index 00000000..7ddc0748 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-idmlight/src/main/java/org/opendaylight/aaa/idm/rest/DomainHandler.java @@ -0,0 +1,591 @@ +/* + * Copyright (c) 2014, 2015 Hewlett-Packard Development Company, L.P. and others. 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.idm.rest; + +import java.util.ArrayList; +import java.util.List; +import javax.ws.rs.Consumes; +import javax.ws.rs.DELETE; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriInfo; +import org.opendaylight.aaa.api.IDMStoreException; +import org.opendaylight.aaa.api.model.Claim; +import org.opendaylight.aaa.api.model.Domain; +import org.opendaylight.aaa.api.model.Domains; +import org.opendaylight.aaa.api.model.Grant; +import org.opendaylight.aaa.api.model.Grants; +import org.opendaylight.aaa.api.model.IDMError; +import org.opendaylight.aaa.api.model.Role; +import org.opendaylight.aaa.api.model.Roles; +import org.opendaylight.aaa.api.model.User; +import org.opendaylight.aaa.api.model.UserPwd; +import org.opendaylight.aaa.api.model.Users; +import org.opendaylight.aaa.idm.IdmLightProxy; +import org.opendaylight.yang.gen.v1.config.aaa.authn.idmlight.rev151204.AAAIDMLightModule; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * REST application used to manipulate the H2 database domains table. The REST + * endpoint is <code>/auth/v1/domains</code>. + * + * The following provides examples of curl commands and payloads to utilize the + * domains REST endpoint: + * + * <b>Get All Domains</b> + * <code>curl -u admin:admin http://{HOST}:{PORT}/auth/v1/domains</code> + * + * <b>Get A Specific Domain</b> + * <code>curl -u admin:admin http://{HOST}:{PORT}/auth/v1/domains/{id}</code> + * + * <b>Create A Domain</b> + * <code>curl -u admin:admin -X POST -H "Content-Type: application/json" --data-binary {@literal @}domain.json http://{HOST}:{PORT}/auth/v1/domains</code> + * Example domain.json <code>{ + * "description": "new domain", + * "enabled", "true", + * "name", "not sdn" + * }</code> + * + * <b>Update A Domain</b> + * <code>curl -u admin:admin -X PUT -H "Content-Type: application/json" --data-binary {@literal @}domain.json http://{HOST}:{PORT}/auth/v1/domains</code> + * Example domain.json <code>{ + * "description": "new domain description", + * "enabled", "true", + * "name", "not sdn" + * }</code> + * + * @author peter.mellquist@hp.com + * @author Ryan Goulding (ryandgoulding@gmail.com) + */ +@Path("/v1/domains") +public class DomainHandler { + + private static final Logger LOG = LoggerFactory.getLogger(DomainHandler.class); + + /** + * Extracts all domains. + * + * @return a response with all domains stored in the H2 database + */ + @GET + @Produces("application/json") + public Response getDomains() { + LOG.info("Get /domains"); + Domains domains = null; + try { + domains = AAAIDMLightModule.getStore().getDomains(); + } catch (IDMStoreException se) { + LOG.error("StoreException: ", se); + IDMError idmerror = new IDMError(); + idmerror.setMessage("Internal error getting domains"); + idmerror.setDetails(se.getMessage()); + return Response.status(500).entity(idmerror).build(); + } + return Response.ok(domains).build(); + } + + /** + * Extracts the domain represented by <code>domainId</code>. + * + * @param domainId the string domain (i.e., "sdn") + * @return a response with the specified domain + */ + @GET + @Path("/{id}") + @Produces("application/json") + public Response getDomain(@PathParam("id") String domainId) { + LOG.info("Get /domains/{}", domainId); + Domain domain = null; + try { + domain = AAAIDMLightModule.getStore().readDomain(domainId); + } catch (IDMStoreException se) { + LOG.error("StoreException: ", se); + IDMError idmerror = new IDMError(); + idmerror.setMessage("Internal error getting domain"); + idmerror.setDetails(se.getMessage()); + return Response.status(500).entity(idmerror).build(); + } + + if (domain == null) { + IDMError idmerror = new IDMError(); + idmerror.setMessage("Not found! domain id :" + domainId); + return Response.status(404).entity(idmerror).build(); + } + return Response.ok(domain).build(); + } + + /** + * Creates a domain. The name attribute is required for domain creation. + * Enabled and description fields are optional. Optional fields default + * in the following manner: + * <code>enabled</code>: <code>false</code> + * <code>description</code>: An empty string (<code>""</code>). + * + * @param info passed from Jersey + * @param domain designated by the REST payload + * @return A response stating success or failure of domain creation. + */ + @POST + @Consumes("application/json") + @Produces("application/json") + public Response createDomain(@Context UriInfo info, Domain domain) { + LOG.info("Post /domains"); + try { + if (domain.isEnabled() == null) { + domain.setEnabled(false); + } + if (domain.getName() == null) { + domain.setName(""); + } + if (domain.getDescription() == null) { + domain.setDescription(""); + } + domain = AAAIDMLightModule.getStore().writeDomain(domain); + } catch (IDMStoreException se) { + LOG.error("StoreException: ", se); + IDMError idmerror = new IDMError(); + idmerror.setMessage("Internal error creating domain"); + idmerror.setDetails(se.getMessage()); + return Response.status(500).entity(idmerror).build(); + } + return Response.status(201).entity(domain).build(); + } + + /** + * Updates a domain. + * + * @param info passed from Jersey + * @param domain the REST payload + * @param domainId the last part of the path, containing the specified domain id + * @return A response stating success or failure of domain update. + */ + @PUT + @Path("/{id}") + @Consumes("application/json") + @Produces("application/json") + public Response putDomain(@Context UriInfo info, Domain domain, @PathParam("id") String domainId) { + LOG.info("Put /domains/{}", domainId); + try { + domain.setDomainid(domainId); + domain = AAAIDMLightModule.getStore().updateDomain(domain); + if (domain == null) { + IDMError idmerror = new IDMError(); + idmerror.setMessage("Not found! Domain id :" + domainId); + return Response.status(404).entity(idmerror).build(); + } + IdmLightProxy.clearClaimCache(); + return Response.status(200).entity(domain).build(); + } catch (IDMStoreException se) { + LOG.error("StoreException: ", se); + IDMError idmerror = new IDMError(); + idmerror.setMessage("Internal error putting domain"); + idmerror.setDetails(se.getMessage()); + return Response.status(500).entity(idmerror).build(); + } + } + + /** + * Deletes a domain. + * + * @param info passed from Jersey + * @param domainId the last part of the path, containing the specified domain id + * @return A response stating success or failure of domain deletion. + */ + @DELETE + @Path("/{id}") + public Response deleteDomain(@Context UriInfo info, @PathParam("id") String domainId) { + LOG.info("Delete /domains/{}", domainId); + + try { + Domain domain = AAAIDMLightModule.getStore().deleteDomain(domainId); + if (domain == null) { + IDMError idmerror = new IDMError(); + idmerror.setMessage("Not found! Domain id :" + domainId); + return Response.status(404).entity(idmerror).build(); + } + } catch (IDMStoreException se) { + LOG.error("StoreException: ", se); + IDMError idmerror = new IDMError(); + idmerror.setMessage("Internal error deleting Domain"); + idmerror.setDetails(se.getMessage()); + return Response.status(500).entity(idmerror).build(); + } + IdmLightProxy.clearClaimCache(); + return Response.status(204).build(); + } + + /** + * Creates a grant. A grant defines the role a particular user is given on + * a particular domain. For example, by default, AAA installs a grant for + * the "admin" user, granting permission to act with "admin" role on the + * "sdn" domain. + * + * @param info passed from Jersey + * @param domainId the domain the user is allowed to access + * @param userId the user that is allowed to access the domain + * @param grant the payload containing role access controls + * @return A response stating success or failure of grant creation. + */ + @POST + @Path("/{did}/users/{uid}/roles") + @Consumes("application/json") + @Produces("application/json") + public Response createGrant(@Context UriInfo info, @PathParam("did") String domainId, + @PathParam("uid") String userId, Grant grant) { + LOG.info("Post /domains/{}/users/{}/roles", domainId, userId); + Domain domain = null; + User user = null; + Role role = null; + String roleId = null; + + // validate domain id + try { + domain = AAAIDMLightModule.getStore().readDomain(domainId); + } catch (IDMStoreException se) { + LOG.error("StoreException: ", se); + IDMError idmerror = new IDMError(); + idmerror.setMessage("Internal error getting domain"); + idmerror.setDetails(se.getMessage()); + return Response.status(500).entity(idmerror).build(); + } + if (domain == null) { + IDMError idmerror = new IDMError(); + idmerror.setMessage("Not found! domain id :" + domainId); + return Response.status(404).entity(idmerror).build(); + } + grant.setDomainid(domainId); + + try { + user = AAAIDMLightModule.getStore().readUser(userId); + } catch (IDMStoreException se) { + LOG.error("StoreException: ", se); + IDMError idmerror = new IDMError(); + idmerror.setMessage("Internal error getting user"); + idmerror.setDetails(se.getMessage()); + return Response.status(500).entity(idmerror).build(); + } + if (user == null) { + IDMError idmerror = new IDMError(); + idmerror.setMessage("Not found! User id :" + userId); + return Response.status(404).entity(idmerror).build(); + } + grant.setUserid(userId); + + // validate role id + try { + roleId = grant.getRoleid(); + LOG.info("roleid = {}", roleId); + } catch (NumberFormatException nfe) { + IDMError idmerror = new IDMError(); + idmerror.setMessage("Invalid Role id :" + grant.getRoleid()); + return Response.status(404).entity(idmerror).build(); + } + try { + role = AAAIDMLightModule.getStore().readRole(roleId); + } catch (IDMStoreException se) { + LOG.error("StoreException: ", se); + IDMError idmerror = new IDMError(); + idmerror.setMessage("Internal error getting role"); + idmerror.setDetails(se.getMessage()); + return Response.status(500).entity(idmerror).build(); + } + if (role == null) { + IDMError idmerror = new IDMError(); + idmerror.setMessage("Not found! role :" + grant.getRoleid()); + return Response.status(404).entity(idmerror).build(); + } + + // see if grant already exists for this + try { + Grant existingGrant = AAAIDMLightModule.getStore().readGrant(domainId, userId, roleId); + if (existingGrant != null) { + IDMError idmerror = new IDMError(); + idmerror.setMessage("Grant already exists for did:" + domainId + " uid:" + userId + + " rid:" + roleId); + return Response.status(403).entity(idmerror).build(); + } + } catch (IDMStoreException se) { + LOG.error("StoreException: ", se); + IDMError idmerror = new IDMError(); + idmerror.setMessage("Internal error creating grant"); + idmerror.setDetails(se.getMessage()); + return Response.status(500).entity(idmerror).build(); + } + + // create grant + try { + grant = AAAIDMLightModule.getStore().writeGrant(grant); + } catch (IDMStoreException se) { + LOG.error("StoreException: ", se); + IDMError idmerror = new IDMError(); + idmerror.setMessage("Internal error creating grant"); + idmerror.setDetails(se.getMessage()); + return Response.status(500).entity(idmerror).build(); + } + + IdmLightProxy.clearClaimCache(); + return Response.status(201).entity(grant).build(); + } + + /** + * Used to validate user access. + * + * @param info passed from Jersey + * @param domainId the domain in question + * @param userpwd the password attempt + * @return A response stating success or failure of user validation. + */ + @POST + @Path("/{did}/users/roles") + @Consumes("application/json") + @Produces("application/json") + public Response validateUser(@Context UriInfo info, @PathParam("did") String domainId, + UserPwd userpwd) { + + LOG.info("GET /domains/{}/users", domainId); + Domain domain = null; + Claim claim = new Claim(); + List<Role> roleList = new ArrayList<Role>(); + + try { + domain = AAAIDMLightModule.getStore().readDomain(domainId); + } catch (IDMStoreException se) { + LOG.error("StoreException: ", se); + IDMError idmerror = new IDMError(); + idmerror.setMessage("Internal error getting domain"); + idmerror.setDetails(se.getMessage()); + return Response.status(500).entity(idmerror).build(); + } + if (domain == null) { + IDMError idmerror = new IDMError(); + idmerror.setMessage("Not found! Domain id :" + domainId); + return Response.status(404).entity(idmerror).build(); + } + + // check request body for username and pwd + String username = userpwd.getUsername(); + if (username == null) { + IDMError idmerror = new IDMError(); + idmerror.setMessage("username not specfied in request body"); + return Response.status(400).entity(idmerror).build(); + } + String pwd = userpwd.getUserpwd(); + if (pwd == null) { + IDMError idmerror = new IDMError(); + idmerror.setMessage("userpwd not specfied in request body"); + return Response.status(400).entity(idmerror).build(); + } + + // find userid for user + try { + Users users = AAAIDMLightModule.getStore().getUsers(username, domainId); + List<User> userList = users.getUsers(); + if (userList.size() == 0) { + IDMError idmerror = new IDMError(); + idmerror.setMessage("did not find username: " + username); + return Response.status(404).entity(idmerror).build(); + } + User user = userList.get(0); + String userPwd = user.getPassword(); + String reqPwd = userpwd.getUserpwd(); + if (!userPwd.equals(reqPwd)) { + IDMError idmerror = new IDMError(); + idmerror.setMessage("password does not match for username: " + username); + return Response.status(401).entity(idmerror).build(); + } + claim.setDomainid(domainId); + claim.setUsername(username); + claim.setUserid(user.getUserid()); + try { + Grants grants = AAAIDMLightModule.getStore().getGrants(domainId, user.getUserid()); + List<Grant> grantsList = grants.getGrants(); + for (int i = 0; i < grantsList.size(); i++) { + Grant grant = grantsList.get(i); + Role role = AAAIDMLightModule.getStore().readRole(grant.getRoleid()); + roleList.add(role); + } + } catch (IDMStoreException se) { + LOG.error("StoreException: ", se); + IDMError idmerror = new IDMError(); + idmerror.setMessage("Internal error getting Roles"); + idmerror.setDetails(se.getMessage()); + return Response.status(500).entity(idmerror).build(); + } + claim.setRoles(roleList); + } catch (IDMStoreException se) { + LOG.error("StoreException: ", se); + IDMError idmerror = new IDMError(); + idmerror.setMessage("Internal error getting user"); + idmerror.setDetails(se.getMessage()); + return Response.status(500).entity(idmerror).build(); + } + + return Response.ok(claim).build(); + } + + /** + * Get the grants for a user on a domain. + * + * @param info passed from Jersey + * @param domainId the domain in question + * @param userId the user in question + * @return A response containing the grants for a user on a domain. + */ + @GET + @Path("/{did}/users/{uid}/roles") + @Produces("application/json") + public Response getRoles(@Context UriInfo info, @PathParam("did") String domainId, + @PathParam("uid") String userId) { + LOG.info("GET /domains/{}/users/{}/roles", domainId, userId); + Domain domain = null; + User user = null; + Roles roles = new Roles(); + List<Role> roleList = new ArrayList<Role>(); + + try { + domain = AAAIDMLightModule.getStore().readDomain(domainId); + } catch (IDMStoreException se) { + LOG.error("StoreException: ", se); + IDMError idmerror = new IDMError(); + idmerror.setMessage("Internal error getting domain"); + idmerror.setDetails(se.getMessage()); + return Response.status(500).entity(idmerror).build(); + } + if (domain == null) { + IDMError idmerror = new IDMError(); + idmerror.setMessage("Not found! Domain id :" + domainId); + return Response.status(404).entity(idmerror).build(); + } + + try { + user = AAAIDMLightModule.getStore().readUser(userId); + } catch (IDMStoreException se) { + LOG.error("StoreException: ", se); + IDMError idmerror = new IDMError(); + idmerror.setMessage("Internal error getting user"); + idmerror.setDetails(se.getMessage()); + return Response.status(500).entity(idmerror).build(); + } + if (user == null) { + IDMError idmerror = new IDMError(); + idmerror.setMessage("Not found! User id :" + userId); + return Response.status(404).entity(idmerror).build(); + } + + try { + Grants grants = AAAIDMLightModule.getStore().getGrants(domainId, userId); + List<Grant> grantsList = grants.getGrants(); + for (int i = 0; i < grantsList.size(); i++) { + Grant grant = grantsList.get(i); + Role role = AAAIDMLightModule.getStore().readRole(grant.getRoleid()); + roleList.add(role); + } + } catch (IDMStoreException se) { + LOG.error("StoreException: ", se); + IDMError idmerror = new IDMError(); + idmerror.setMessage("Internal error getting Roles"); + idmerror.setDetails(se.getMessage()); + return Response.status(500).entity(idmerror).build(); + } + + roles.setRoles(roleList); + return Response.ok(roles).build(); + } + + /** + * Delete a grant. + * + * @param info passed from Jersey + * @param domainId the domain for the grant + * @param userId the user for the grant + * @param roleId the role for the grant + * @return A response stating success or failure of the grant deletion. + */ + @DELETE + @Path("/{did}/users/{uid}/roles/{rid}") + public Response deleteGrant(@Context UriInfo info, @PathParam("did") String domainId, + @PathParam("uid") String userId, @PathParam("rid") String roleId) { + Domain domain = null; + User user = null; + Role role = null; + + try { + domain = AAAIDMLightModule.getStore().readDomain(domainId); + } catch (IDMStoreException se) { + LOG.error("Error deleting Grant : ", se); + IDMError idmerror = new IDMError(); + idmerror.setMessage("Internal error getting domain"); + idmerror.setDetails(se.getMessage()); + return Response.status(500).entity(idmerror).build(); + } + if (domain == null) { + IDMError idmerror = new IDMError(); + idmerror.setMessage("Not found! Domain id :" + domainId); + return Response.status(404).entity(idmerror).build(); + } + + try { + user = AAAIDMLightModule.getStore().readUser(userId); + } catch (IDMStoreException se) { + LOG.error("StoreException : ", se); + IDMError idmerror = new IDMError(); + idmerror.setMessage("Internal error getting user"); + idmerror.setDetails(se.getMessage()); + return Response.status(500).entity(idmerror).build(); + } + if (user == null) { + IDMError idmerror = new IDMError(); + idmerror.setMessage("Not found! User id :" + userId); + return Response.status(404).entity(idmerror).build(); + } + + try { + role = AAAIDMLightModule.getStore().readRole(roleId); + } catch (IDMStoreException se) { + LOG.error("StoreException: ", se); + IDMError idmerror = new IDMError(); + idmerror.setMessage("Internal error getting Role"); + idmerror.setDetails(se.getMessage()); + return Response.status(500).entity(idmerror).build(); + } + if (role == null) { + IDMError idmerror = new IDMError(); + idmerror.setMessage("Not found! Role id :" + roleId); + return Response.status(404).entity(idmerror).build(); + } + + // see if grant already exists + try { + Grant existingGrant = AAAIDMLightModule.getStore().readGrant(domainId, userId, roleId); + if (existingGrant == null) { + IDMError idmerror = new IDMError(); + idmerror.setMessage("Grant does not exist for did:" + domainId + " uid:" + userId + + " rid:" + roleId); + return Response.status(404).entity(idmerror).build(); + } + existingGrant = AAAIDMLightModule.getStore().deleteGrant(existingGrant.getGrantid()); + } catch (IDMStoreException se) { + LOG.error("StoreException: ", se); + IDMError idmerror = new IDMError(); + idmerror.setMessage("Internal error creating grant"); + idmerror.setDetails(se.getMessage()); + return Response.status(500).entity(idmerror).build(); + } + IdmLightProxy.clearClaimCache(); + return Response.status(204).build(); + } + +} diff --git a/odl-aaa-moon/aaa/aaa-idmlight/src/main/java/org/opendaylight/aaa/idm/rest/RoleHandler.java b/odl-aaa-moon/aaa/aaa-idmlight/src/main/java/org/opendaylight/aaa/idm/rest/RoleHandler.java new file mode 100644 index 00000000..34a60c0c --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-idmlight/src/main/java/org/opendaylight/aaa/idm/rest/RoleHandler.java @@ -0,0 +1,228 @@ +/* + * Copyright (c) 2014, 2015 Hewlett-Packard Development Company, L.P. and others. 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.idm.rest; + +import javax.ws.rs.Consumes; +import javax.ws.rs.DELETE; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriInfo; + +import org.opendaylight.aaa.api.IDMStoreException; +import org.opendaylight.aaa.api.model.IDMError; +import org.opendaylight.aaa.api.model.Role; +import org.opendaylight.aaa.api.model.Roles; +import org.opendaylight.aaa.idm.IdmLightApplication; +import org.opendaylight.aaa.idm.IdmLightProxy; +import org.opendaylight.yang.gen.v1.config.aaa.authn.idmlight.rev151204.AAAIDMLightModule; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * REST application used to manipulate the H2 database roles table. The REST + * endpoint is <code>/auth/v1/roles</code>. + * + * The following provides examples of curl commands and payloads to utilize the + * roles REST endpoint: + * + * <b>Get All Roles</b> + * <code>curl -u admin:admin http://{HOST}:{PORT}/auth/v1/roles</code> + * + * <b>Get A Specific Role</b> + * <code>curl -u admin:admin http://{HOST}:{PORT}/auth/v1/roles/{id}</code> + * + * <b>Create A Role</b> + * <code>curl -u admin:admin -X POST -H "Content-Type: application/json" --data-binary {@literal @}role.json http://{HOST}:{PORT}/auth/v1/roles</code> + * An example of role.json: + * <code>{ + * "name":"IT Administrator", + * "description":"A user role for IT admins" + * }</code> + * + * <b>Update A Role</b> + * <code>curl -u admin:admin -X PUT -H "Content-Type: application/json" --data-binary {@literal @}role.json http://{HOST}:{PORT}/auth/v1/roles/{id}</code> + * An example of role.json: + * <code>{ + * "name":"IT Administrator Limited", + * "description":"A user role for IT admins who can only do one thing" + * }</code> + * + * @author peter.mellquist@hp.com + * @author Ryan Goulding (ryandgoulding@gmail.com) + */ +@Path("/v1/roles") +public class RoleHandler { + private static final Logger LOG = LoggerFactory.getLogger(RoleHandler.class); + + /** + * Extracts all roles. + * + * @return A response with all roles in the H2 database, or internal error if one is encountered + */ + @GET + @Produces("application/json") + public Response getRoles() { + LOG.info("get /roles"); + Roles roles = null; + try { + roles = AAAIDMLightModule.getStore().getRoles(); + } catch (IDMStoreException se) { + return new IDMError(500, "internal error getting roles", se.getMessage()).response(); + } + return Response.ok(roles).build(); + } + + /** + * Extract a specific role identified by <code>id</code> + * + * @param id the String id for the role + * @return A response with the role identified by <code>id</code>, or internal error if one is encountered + */ + @GET + @Path("/{id}") + @Produces("application/json") + public Response getRole(@PathParam("id") String id) { + LOG.info("get /roles/{}", id); + Role role = null; + + try { + role = AAAIDMLightModule.getStore().readRole(id); + } catch (IDMStoreException se) { + return new IDMError(500, "internal error getting roles", se.getMessage()).response(); + } + + if (role == null) { + return new IDMError(404, "role not found id :" + id, "").response(); + } + return Response.ok(role).build(); + } + + /** + * Creates a role. + * + * @param info passed from Jersey + * @param role the role JSON payload + * @return A response stating success or failure of role creation, or internal error if one is encountered + */ + @POST + @Consumes("application/json") + @Produces("application/json") + public Response createRole(@Context UriInfo info, Role role) { + LOG.info("Post /roles"); + try { + // TODO: role names should be unique! + // name + if (role.getName() == null) { + return new IDMError(404, "name must be defined on role create", "").response(); + } else if (role.getName().length() > IdmLightApplication.MAX_FIELD_LEN) { + return new IDMError(400, "role name max length is :" + + IdmLightApplication.MAX_FIELD_LEN, "").response(); + } + + // domain + if (role.getDomainid() == null) { + return new IDMError(404, + "The role's domain must be defined on role when creating a role.", "") + .response(); + } else if (role.getDomainid().length() > IdmLightApplication.MAX_FIELD_LEN) { + return new IDMError(400, "role domain max length is :" + + IdmLightApplication.MAX_FIELD_LEN, "").response(); + } + + // description + if (role.getDescription() == null) { + role.setDescription(""); + } else if (role.getDescription().length() > IdmLightApplication.MAX_FIELD_LEN) { + return new IDMError(400, "role description max length is :" + + IdmLightApplication.MAX_FIELD_LEN, "").response(); + } + + role = AAAIDMLightModule.getStore().writeRole(role); + } catch (IDMStoreException se) { + return new IDMError(500, "internal error creating role", se.getMessage()).response(); + } + + return Response.status(201).entity(role).build(); + } + + /** + * Updates a specific role identified by <code>id</code>. + * + * @param info passed from Jersey + * @param role the role JSON payload + * @param id the String id for the role + * @return A response stating success or failure of role update, or internal error if one occurs + */ + @PUT + @Path("/{id}") + @Consumes("application/json") + @Produces("application/json") + public Response putRole(@Context UriInfo info, Role role, @PathParam("id") String id) { + LOG.info("put /roles/{}", id); + + try { + role.setRoleid(id); + + // name + // TODO: names should be unique + if ((role.getName() != null) + && (role.getName().length() > IdmLightApplication.MAX_FIELD_LEN)) { + return new IDMError(400, "role name max length is :" + + IdmLightApplication.MAX_FIELD_LEN, "").response(); + } + + // description + if ((role.getDescription() != null) + && (role.getDescription().length() > IdmLightApplication.MAX_FIELD_LEN)) { + return new IDMError(400, "role description max length is :" + + IdmLightApplication.MAX_FIELD_LEN, "").response(); + } + + role = AAAIDMLightModule.getStore().updateRole(role); + if (role == null) { + return new IDMError(404, "role id not found :" + id, "").response(); + } + IdmLightProxy.clearClaimCache(); + return Response.status(200).entity(role).build(); + } catch (IDMStoreException se) { + return new IDMError(500, "internal error putting role", se.getMessage()).response(); + } + } + + /** + * Delete a role. + * + * @param info passed from Jersey + * @param id the String id for the role + * @return A response stating success or failure of user deletion, or internal error if one occurs + */ + @DELETE + @Path("/{id}") + public Response deleteRole(@Context UriInfo info, @PathParam("id") String id) { + LOG.info("Delete /roles/{}", id); + + try { + Role role = AAAIDMLightModule.getStore().deleteRole(id); + if (role == null) { + return new IDMError(404, "role id not found :" + id, "").response(); + } + } catch (IDMStoreException se) { + return new IDMError(500, "internal error deleting role", se.getMessage()).response(); + } + IdmLightProxy.clearClaimCache(); + return Response.status(204).build(); + } + +} diff --git a/odl-aaa-moon/aaa/aaa-idmlight/src/main/java/org/opendaylight/aaa/idm/rest/UserHandler.java b/odl-aaa-moon/aaa/aaa-idmlight/src/main/java/org/opendaylight/aaa/idm/rest/UserHandler.java new file mode 100644 index 00000000..1649faa2 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-idmlight/src/main/java/org/opendaylight/aaa/idm/rest/UserHandler.java @@ -0,0 +1,420 @@ +/* + * Copyright (c) 2014, 2015 Hewlett-Packard Development Company, L.P. and others. 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.idm.rest; + +import java.util.Collection; + +import javax.ws.rs.Consumes; +import javax.ws.rs.DELETE; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriInfo; + +import org.opendaylight.aaa.api.IDMStoreException; +import org.opendaylight.aaa.api.model.IDMError; +import org.opendaylight.aaa.api.model.User; +import org.opendaylight.aaa.api.model.Users; +import org.opendaylight.aaa.idm.IdmLightApplication; +import org.opendaylight.aaa.idm.IdmLightProxy; +import org.opendaylight.yang.gen.v1.config.aaa.authn.idmlight.rev151204.AAAIDMLightModule; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * REST application used to manipulate the H2 database users table. The REST + * endpoint is <code>/auth/v1/users</code>. + * + * The following provides examples of how curl commands and payloads to utilize + * the users REST endpoint: + * + * <b>Get All Users</b> + * <code>curl -u admin:admin http://{HOST}:{PORT}/auth/v1/users</code> + * + * <b>Get A Specific User</b> + * <code>curl -u admin:admin http://{HOST}:{PORT}/auth/v1/users/{id}</code> + * + * <b>Create A User</b> + * <code>curl -u admin:admin -X POST -H "Content-type: application/json" --data-binary {@literal @}user.json http://{HOST}:{PORT}/auth/v1/users</code> + * An example of user.json file is: + * <code>{ + * "name": "admin2", + * "password", "admin2", + * "domain": "sdn" + * }</code> + * + * <b>Update A User</b> + * <code>curl -u admin:admin -X PUT -H "Content-type: application/json" --data-binary {@literal @}user.json http://{HOST}:{PORT}/auth/v1/users/{id}</code> + * An example of user.json file is: + * <code>{ + * "name": "admin2", + * "password", "admin2", + * "domain": "sdn", + * "description", "Simple description." + * }</code> + * + * <b>Delete A User</b> + * <code>curl -u admin:admin -X DELETE http://{HOST}:{PORT}/auth/v1/users/{id}</code> + * + * @author peter.mellquist@hp.com + * @author Ryan Goulding (ryandgoulding@gmail.com) + */ +@Path("/v1/users") +public class UserHandler { + + private static final Logger LOG = LoggerFactory.getLogger(UserHandler.class); + + /** + * If a user is created through the <code>/auth/v1/users</code> rest + * endpoint without a password, the default password is assigned to the + * user. + */ + private final static String DEFAULT_PWD = "changeme"; + + /** + * When an HTTP GET is performed on <code>/auth/v1/users</code>, the + * password field is replaced with <code>REDACTED_PASSWORD</code> for + * security reasons. + */ + private static final String REDACTED_PASSWORD = "**********"; + + /** + * When an HTTP GET is performed on <code>/auth/v1/users</code>, the salt + * field is replaced with <code>REDACTED_SALT</code> for security reasons. + */ + private static final String REDACTED_SALT = "**********"; + + /** + * When creating a user, the description is optional and defaults to an + * empty string. + */ + private static final String DEFAULT_DESCRIPTION = ""; + + /** + * When creating a user, the email is optional and defaults to an empty + * string. + */ + private static final String DEFAULT_EMAIL = ""; + + /** + * Extracts all users. The password and salt fields are redacted for + * security reasons. + * + * @return A response containing the users, or internal error if one occurs + */ + @GET + @Produces("application/json") + public Response getUsers() { + LOG.info("GET /auth/v1/users (extracts all users)"); + + try { + final Users users = AAAIDMLightModule.getStore().getUsers(); + + // Redact the password and salt for security purposes. + final Collection<User> usersList = users.getUsers(); + for (User user : usersList) { + redactUserPasswordInfo(user); + } + + return Response.ok(users).build(); + } catch (IDMStoreException se) { + return internalError("getting", se); + } + } + + /** + * Extracts the user represented by <code>id</code>. The password and salt + * fields are redacted for security reasons. + * + * @param id the unique id of representing the user account + * @return A response with the user information, or internal error if one occurs + */ + @GET + @Path("/{id}") + @Produces("application/json") + public Response getUser(@PathParam("id") String id) { + LOG.info("GET auth/v1/users/ {} (extract user with specified id)", id); + + try { + final User user = AAAIDMLightModule.getStore().readUser(id); + + if (user == null) { + final String error = "user not found! id: " + id; + return new IDMError(404, error, "").response(); + } + + // Redact the password and salt for security purposes. + redactUserPasswordInfo(user); + + return Response.ok(user).build(); + } catch (IDMStoreException se) { + return internalError("getting", se); + } + } + + /** + * REST endpoint to create a user. Name and domain are required attributes, + * and all other fields (description, email, password, enabled) are + * optional. Optional fields default in the following manner: + * <code>description</code>: An empty string (<code>""</code>). + * <code>email</code>: An empty string (<code>""</code>). + * <code>password</code>: <code>changeme</code> <code>enabled</code>: + * <code>true</code> + * + * If a password is not provided, please ensure you change the default + * password ASAP for security reasons! + * + * @param info passed from Jersey + * @param user the user defined in the JSON payload + * @return A response stating success or failure of user creation + */ + @POST + @Consumes("application/json") + @Produces("application/json") + public Response createUser(@Context UriInfo info, User user) { + LOG.info("POST /auth/v1/users (create a user with the specified payload"); + + // The "enabled" field is optional, and defaults to true. + if (user.isEnabled() == null) { + user.setEnabled(true); + } + + // The "name" field is required. + final String userName = user.getName(); + if (userName == null) { + return missingRequiredField("name"); + } + // The "name" field has a maximum length. + if (userName.length() > IdmLightApplication.MAX_FIELD_LEN) { + return providedFieldTooLong("name", IdmLightApplication.MAX_FIELD_LEN); + } + + // The "domain field is required. + final String domainId = user.getDomainid(); + if (domainId == null) { + return missingRequiredField("domain"); + } + // The "domain" field has a maximum length. + if (domainId.length() > IdmLightApplication.MAX_FIELD_LEN) { + return providedFieldTooLong("domain", IdmLightApplication.MAX_FIELD_LEN); + } + + // The "description" field is optional and defaults to "". + final String userDescription = user.getDescription(); + if (userDescription == null) { + user.setDescription(DEFAULT_DESCRIPTION); + } + // The "description" field has a maximum length. + if (userDescription.length() > IdmLightApplication.MAX_FIELD_LEN) { + return providedFieldTooLong("description", IdmLightApplication.MAX_FIELD_LEN); + } + + // The "email" field is optional and defaults to "". + final String userEmail = user.getEmail(); + if (userEmail == null) { + user.setEmail(DEFAULT_EMAIL); + } + if (userEmail.length() > IdmLightApplication.MAX_FIELD_LEN) { + return providedFieldTooLong("email", IdmLightApplication.MAX_FIELD_LEN); + } + // TODO add a check on email format here. + + // The "password" field is optional and defautls to "changeme". + final String userPassword = user.getPassword(); + if (userPassword == null) { + user.setPassword(DEFAULT_PWD); + } else if (userPassword.length() > IdmLightApplication.MAX_FIELD_LEN) { + return providedFieldTooLong("password", IdmLightApplication.MAX_FIELD_LEN); + } + + try { + // At this point, fields have been properly verified. Create the + // user account + final User createdUser = AAAIDMLightModule.getStore().writeUser(user); + user.setUserid(createdUser.getUserid()); + } catch (IDMStoreException se) { + return internalError("creating", se); + } + + // Redact the password and salt for security reasons. + redactUserPasswordInfo(user); + // TODO report back to the client a warning message to change the + // default password if none was specified. + return Response.status(201).entity(user).build(); + } + + /** + * REST endpoint to update a user account. + * + * @param info passed from Jersey + * @param user the user defined in the JSON payload + * @param id the unique id for the user that will be updated + * @return A response stating success or failure of the user update + */ + @PUT + @Path("/{id}") + @Consumes("application/json") + @Produces("application/json") + public Response putUser(@Context UriInfo info, User user, @PathParam("id") String id) { + + LOG.info("PUT /auth/v1/users/{} (Updates a user account)", id); + + try { + user.setUserid(id); + + if (checkInputFieldLength(user.getPassword())) { + return providedFieldTooLong("password", IdmLightApplication.MAX_FIELD_LEN); + } + + if (checkInputFieldLength(user.getName())) { + return providedFieldTooLong("name", IdmLightApplication.MAX_FIELD_LEN); + } + + if (checkInputFieldLength(user.getDescription())) { + return providedFieldTooLong("description", IdmLightApplication.MAX_FIELD_LEN); + } + + if (checkInputFieldLength(user.getEmail())) { + return providedFieldTooLong("email", IdmLightApplication.MAX_FIELD_LEN); + } + + if (checkInputFieldLength(user.getDomainid())) { + return providedFieldTooLong("domain", IdmLightApplication.MAX_FIELD_LEN); + } + + user = AAAIDMLightModule.getStore().updateUser(user); + if (user == null) { + return new IDMError(404, String.format("User not found for id %s", id), "").response(); + } + + IdmLightProxy.clearClaimCache(); + + // Redact the password and salt for security reasons. + redactUserPasswordInfo(user); + return Response.status(200).entity(user).build(); + } catch (IDMStoreException se) { + return internalError("updating", se); + } + } + + /** + * REST endpoint to delete a user account. + * + * @param info passed from Jersey + * @param id the unique id of the user which is being deleted + * @return A response stating success or failure of user deletion + */ + @DELETE + @Path("/{id}") + public Response deleteUser(@Context UriInfo info, @PathParam("id") String id) { + LOG.info("DELETE /auth/v1/users/{} (Delete a user account)", id); + + try { + final User user = AAAIDMLightModule.getStore().deleteUser(id); + + if (user == null) { + return new IDMError(404, + String.format("Error deleting user. " + + "Couldn't find user with id %s", id), + "").response(); + } + } catch (IDMStoreException se) { + return internalError("deleting", se); + } + + // Successfully deleted the user; report success to the client. + IdmLightProxy.clearClaimCache(); + return Response.status(204).build(); + } + + /** + * Creates a <code>Response</code> related to an internal server error. + * + * @param verbal such as "creating", "deleting", "updating" + * @param e The exception, which is propagated in the response + * @return A response containing internal error with specific reasoning + */ + private Response internalError(final String verbal, final Exception e) { + LOG.error("There was an internal error {} the user", verbal, e); + return new IDMError(500, + String.format("There was an internal error %s the user", verbal), + e.getMessage()).response(); + } + + /** + * Creates a <code>Response</code> related to the user not providing a + * required field. + * + * @param fieldName the name of the field which is missing + * @return A response explaining that the request is missing a field + */ + private Response missingRequiredField(final String fieldName) { + + return new IDMError(400, + String.format("%s is required to create the user account. " + + "Please provide a %s in your payload.", fieldName, fieldName), + "").response(); + } + + /** + * Creates a <code>Response</code> related to the user providing a field + * that is too long. + * + * @param fieldName the name of the field that is too long + * @param maxFieldLength the maximum length of <code>fieldName</code> + * @return A response containing the bad field and the maximum field length + */ + private Response providedFieldTooLong(final String fieldName, final int maxFieldLength) { + + return new IDMError(400, + getProvidedFieldTooLongMessage(fieldName, maxFieldLength), + "").response(); + } + + /** + * Creates the client-facing message related to the user providing a field + * that is too long. + * + * @param fieldName the name of the field that is too long + * @param maxFieldLength the maximum length of <code>fieldName</code> + * @return + */ + private static String getProvidedFieldTooLongMessage(final String fieldName, + final int maxFieldLength) { + + return String.format("The provided {} field is too long. " + + "The max length is {}.", fieldName, maxFieldLength); + } + + /** + * Prepares a user account for output by redacting the appropriate fields. + * This method side-effects the <code>user</code> parameter. + * + * @param user the user account which will have fields redacted + */ + private static void redactUserPasswordInfo(final User user) { + user.setPassword(REDACTED_PASSWORD); + user.setSalt(REDACTED_SALT); + } + + /** + * Validate the input field length + * + * @param inputField + * @return true if input field bigger than the MAX_FIELD_LEN + */ + private boolean checkInputFieldLength(final String inputField) { + return inputField != null && (inputField.length() > IdmLightApplication.MAX_FIELD_LEN); + } +} diff --git a/odl-aaa-moon/aaa/aaa-idmlight/src/main/java/org/opendaylight/aaa/idm/rest/VersionHandler.java b/odl-aaa-moon/aaa/aaa-idmlight/src/main/java/org/opendaylight/aaa/idm/rest/VersionHandler.java new file mode 100644 index 00000000..f865162a --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-idmlight/src/main/java/org/opendaylight/aaa/idm/rest/VersionHandler.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2014, 2015 Hewlett-Packard Development Company, L.P. and others. 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.idm.rest; + +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Context; + +import org.opendaylight.aaa.api.model.Version; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * + * @author peter.mellquist@hp.com + * + */ +@Deprecated +@Path("/") +public class VersionHandler { + private static final Logger LOG = LoggerFactory.getLogger(VersionHandler.class);; + + protected static String CURRENT_VERSION = "v1"; + protected static String LAST_UPDATED = "2014-04-18T18:30:02.25Z"; + protected static String CURRENT_STATUS = "CURRENT"; + + @GET + @Produces("application/json") + public Version getVersion(@Context HttpServletRequest request) { + LOG.info("Get /"); + Version version = new Version(); + version.setId(CURRENT_VERSION); + version.setUpdated(LAST_UPDATED); + version.setStatus(CURRENT_STATUS); + return version; + } + +} diff --git a/odl-aaa-moon/aaa/aaa-idmlight/src/main/java/org/opendaylight/yang/gen/v1/config/aaa/authn/idmlight/rev151204/AAAIDMLightModule.java b/odl-aaa-moon/aaa/aaa-idmlight/src/main/java/org/opendaylight/yang/gen/v1/config/aaa/authn/idmlight/rev151204/AAAIDMLightModule.java new file mode 100644 index 00000000..d6872635 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-idmlight/src/main/java/org/opendaylight/yang/gen/v1/config/aaa/authn/idmlight/rev151204/AAAIDMLightModule.java @@ -0,0 +1,90 @@ +package org.opendaylight.yang.gen.v1.config.aaa.authn.idmlight.rev151204; + +import org.opendaylight.aaa.api.CredentialAuth; +import org.opendaylight.aaa.api.IDMStoreException; +import org.opendaylight.aaa.api.IIDMStore; +import org.opendaylight.aaa.api.IdMService; +import org.opendaylight.aaa.idm.IdmLightProxy; +import org.opendaylight.aaa.idm.StoreBuilder; +import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceReference; +import org.osgi.framework.ServiceRegistration; +import org.osgi.util.tracker.ServiceTracker; +import org.osgi.util.tracker.ServiceTrackerCustomizer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class AAAIDMLightModule extends org.opendaylight.yang.gen.v1.config.aaa.authn.idmlight.rev151204.AbstractAAAIDMLightModule { + + private static final Logger LOG = LoggerFactory.getLogger(AAAIDMLightModule.class); + private BundleContext bundleContext = null; + private static volatile IIDMStore store = null; + + public AAAIDMLightModule(org.opendaylight.controller.config.api.ModuleIdentifier identifier, org.opendaylight.controller.config.api.DependencyResolver dependencyResolver) { + super(identifier, dependencyResolver); + } + + public AAAIDMLightModule(org.opendaylight.controller.config.api.ModuleIdentifier identifier, org.opendaylight.controller.config.api.DependencyResolver dependencyResolver, org.opendaylight.yang.gen.v1.config.aaa.authn.idmlight.rev151204.AAAIDMLightModule oldModule, java.lang.AutoCloseable oldInstance) { + super(identifier, dependencyResolver, oldModule, oldInstance); + } + + @Override + public void customValidation() { + // add custom validation form module attributes here. + } + + @Override + public java.lang.AutoCloseable createInstance() { + final IdmLightProxy proxy = new IdmLightProxy(); + final ServiceRegistration<?> idmService = bundleContext.registerService(IdMService.class.getName(), proxy, null); + final ServiceRegistration<?> clientAuthService = bundleContext.registerService(CredentialAuth.class.getName(), proxy, null); + + final ServiceTracker<IIDMStore, IIDMStore> storeServiceTracker = new ServiceTracker<>(bundleContext, IIDMStore.class, + new ServiceTrackerCustomizer<IIDMStore, IIDMStore>() { + @Override + public IIDMStore addingService(ServiceReference<IIDMStore> reference) { + store = reference.getBundle().getBundleContext().getService(reference); + LOG.info("IIDMStore service {} was found", store.getClass()); + try { + StoreBuilder.init(store); + } catch (IDMStoreException e) { + LOG.error("Failed to initialize data in store", e); + } + + return store; + } + + @Override + public void modifiedService(ServiceReference<IIDMStore> reference, IIDMStore service) { + } + + @Override + public void removedService(ServiceReference<IIDMStore> reference, IIDMStore service) { + } + }); + + storeServiceTracker.open(); + + LOG.info("AAA IDM Light Module Initialized"); + return new AutoCloseable() { + @Override + public void close() throws Exception { + idmService.unregister(); + clientAuthService.unregister(); + storeServiceTracker.close(); + } + }; + } + + public void setBundleContext(BundleContext b){ + this.bundleContext = b; + } + + public static final IIDMStore getStore(){ + return store; + } + + public static final void setStore(IIDMStore s){ + store = s; + } +} diff --git a/odl-aaa-moon/aaa/aaa-idmlight/src/main/java/org/opendaylight/yang/gen/v1/config/aaa/authn/idmlight/rev151204/AAAIDMLightModuleFactory.java b/odl-aaa-moon/aaa/aaa-idmlight/src/main/java/org/opendaylight/yang/gen/v1/config/aaa/authn/idmlight/rev151204/AAAIDMLightModuleFactory.java new file mode 100644 index 00000000..de277da8 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-idmlight/src/main/java/org/opendaylight/yang/gen/v1/config/aaa/authn/idmlight/rev151204/AAAIDMLightModuleFactory.java @@ -0,0 +1,29 @@ +/* +* Generated file +* +* Generated from: yang module name: aaa-idmlight yang module local name: aaa-idmlight +* Generated by: org.opendaylight.controller.config.yangjmxgenerator.plugin.JMXGenerator +* Generated at: Fri Dec 04 11:37:37 PST 2015 +* +* Do not modify this file unless it is present under src/main directory +*/ +package org.opendaylight.yang.gen.v1.config.aaa.authn.idmlight.rev151204; + +import org.opendaylight.controller.config.api.DependencyResolver; +import org.osgi.framework.BundleContext; + +public class AAAIDMLightModuleFactory extends org.opendaylight.yang.gen.v1.config.aaa.authn.idmlight.rev151204.AbstractAAAIDMLightModuleFactory { + @Override + public AAAIDMLightModule instantiateModule(String instanceName, DependencyResolver dependencyResolver, AAAIDMLightModule oldModule, AutoCloseable oldInstance, BundleContext bundleContext) { + AAAIDMLightModule module = super.instantiateModule(instanceName, dependencyResolver, oldModule, oldInstance, bundleContext); + module.setBundleContext(bundleContext); + return module; + } + + @Override + public AAAIDMLightModule instantiateModule(String instanceName, DependencyResolver dependencyResolver, BundleContext bundleContext) { + AAAIDMLightModule module = super.instantiateModule(instanceName, dependencyResolver, bundleContext); + module.setBundleContext(bundleContext); + return module; + } +} diff --git a/odl-aaa-moon/aaa/aaa-idmlight/src/main/resources/WEB-INF/web.xml b/odl-aaa-moon/aaa/aaa-idmlight/src/main/resources/WEB-INF/web.xml new file mode 100644 index 00000000..facba131 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-idmlight/src/main/resources/WEB-INF/web.xml @@ -0,0 +1,77 @@ +<?xml version="1.0" encoding="ISO-8859-1"?> +<web-app xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" + version="3.0"> + + <servlet> + <servlet-name>IdmLight</servlet-name> + <servlet-class>com.sun.jersey.spi.container.servlet.ServletContainer</servlet-class> + <init-param> + <param-name>javax.ws.rs.Application</param-name> + <param-value>org.opendaylight.aaa.idm.IdmLightApplication</param-value> + </init-param> + <init-param> + <param-name>com.sun.jersey.api.json.POJOMappingFeature</param-name><param-value>true</param-value> + </init-param> + <load-on-startup>1</load-on-startup> + </servlet> + <servlet-mapping> + <servlet-name>IdmLight</servlet-name> + <url-pattern>/*</url-pattern> + </servlet-mapping> + + <context-param> + <param-name>shiroEnvironmentClass</param-name> + <param-value>org.opendaylight.aaa.shiro.web.env.KarafIniWebEnvironment</param-value> + </context-param> + + <listener> + <listener-class>org.apache.shiro.web.env.EnvironmentLoaderListener</listener-class> + </listener> + + <filter> + <filter-name>ShiroFilter</filter-name> + <filter-class>org.opendaylight.aaa.shiro.filters.AAAFilter</filter-class> + </filter> + + <filter-mapping> + <filter-name>ShiroFilter</filter-name> + <url-pattern>/*</url-pattern> + </filter-mapping> + + <filter> + <filter-name>cross-origin-restconf</filter-name> + <filter-class>org.eclipse.jetty.servlets.CrossOriginFilter</filter-class> + <init-param> + <param-name>allowedOrigins</param-name> + <param-value>*</param-value> + </init-param> + <init-param> + <param-name>allowedMethods</param-name> + <param-value>GET,POST,OPTIONS,DELETE,PUT,HEAD</param-value> + </init-param> + <init-param> + <param-name>allowedHeaders</param-name> + <param-value>origin, content-type, accept, authorization, Authorization</param-value> + </init-param> + </filter> + + <filter-mapping> + <filter-name>cross-origin-restconf</filter-name> + <url-pattern>/*</url-pattern> + </filter-mapping> + + <security-constraint> + <web-resource-collection> + <web-resource-name>NB api</web-resource-name> + <url-pattern>/*</url-pattern> + <http-method>POST</http-method> + <http-method>GET</http-method> + <http-method>PUT</http-method> + <http-method>PATCH</http-method> + <http-method>DELETE</http-method> + <http-method>HEAD</http-method> + </web-resource-collection> + </security-constraint> + +</web-app> diff --git a/odl-aaa-moon/aaa/aaa-idmlight/src/main/resources/idmtool.py b/odl-aaa-moon/aaa/aaa-idmlight/src/main/resources/idmtool.py new file mode 100755 index 00000000..b14a8758 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-idmlight/src/main/resources/idmtool.py @@ -0,0 +1,255 @@ +#!/usr/bin/env python + +# +# Copyright (c) 2016 Brocade Communications Systems and others. 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 +# + +''' +idmtool + +Used to manipulate ODL AAA idm on a node-per-node basis. Assumes only one domain (sdn) +since current support in ODL is limited. +''' + +__author__ = "Ryan Goulding" +__copyright__ = "Copyright (c) 2016 Brocade Communications Systems and others" +__credits__ = "Ryan Goulding" +__license__ = "EPL" +__version__ = "1.0" +__maintainer__ = "Ryan Goulding" +__email__ = "ryandgoulding@gmail.com" +__status__ = "Production" + +import argparse, getpass, json, requests, sys + +parser = argparse.ArgumentParser('idmtool') + +user='' +hostname='localhost' +protocol='http' +port='8181' +target_host='{}://{}:{}/'.format(protocol, hostname, port) + +# main program arguments +parser.add_argument('user',help='username for BSC node', nargs=1) +parser.add_argument('--target-host', help="target host node", nargs=1) + +subparsers = parser.add_subparsers(help='sub-command help') + +# users table related +list_users = subparsers.add_parser('list-users', help='list all users') +list_users.set_defaults(func=list_users) +add_user = subparsers.add_parser('add-user', help='add a user') +add_user.set_defaults(func=add_user) +add_user.add_argument('newUser', help='new user name', nargs=1) +change_password = subparsers.add_parser('change-password', help='change a password') +change_password.set_defaults(func=change_password) +change_password.add_argument('userid', help='change the password for a particular userid', nargs=1) +delete_user = subparsers.add_parser('delete-user', help='delete a user') +delete_user.add_argument('userid', help='name@sdn', nargs=1) +delete_user.set_defaults(func=delete_user) + +# domains table related +# only read is defined; this was done on purpose since the "domain" concept +# is mostly unsupported in ODL. +list_domains = subparsers.add_parser('list-domains', help='list all domains') +list_domains.set_defaults(func=list_domains) + +# roles table related +list_roles = subparsers.add_parser('list-roles', help='list all roles') +list_roles.set_defaults(func=list_roles) +add_role = subparsers.add_parser('add-role', help='add a role') +add_role.add_argument('role', help='role name', nargs=1) +add_role.set_defaults(func=add_role) +delete_role = subparsers.add_parser('delete-role', help='delete a role') +delete_role.add_argument('roleid', help='rolename@sdn', nargs=1) +delete_role.set_defaults(func=delete_role) +add_grant = subparsers.add_parser('add-grant', help='add a grant') +add_grant.set_defaults(func=add_grant) +add_grant.add_argument('userid', help="username@sdn", nargs=1) +add_grant.add_argument('roleid', help="role@sdn", nargs=1) +get_grants = subparsers.add_parser('get-grants', help='get grants for userid on sdn') +get_grants.set_defaults(func=get_grants) +get_grants.add_argument('userid', help="username@sdn", nargs=1) +delete_grant = subparsers.add_parser('delete-grant', help='delete a grant') +delete_grant.add_argument('userid', help='username@sdn', nargs=1) +delete_grant.add_argument('roleid', help='role@sdn', nargs=1) +delete_grant.set_defaults(func=delete_grant) + +def process_result(r): + ''' Generic method to print result of a REST call ''' + print '' + sc = r.status_code + if sc >= 200 and sc < 300: + print "command succeeded!" + try: + res = r.json() + if res is not None: + print '\njson:\n', json.dumps(res, indent=4, sort_keys=True) + except(ValueError): + pass + elif sc == 401: + print "Incorrect Credentials Provided" + elif sc == 404: + print "RESTconf is either not installed or not initialized yet" + elif sc >= 500 and sc < 600: + print "Internal Server Error Ocurred" + else: + print "Unknown error; HTTP status code: {}".format(sc) + +def get_request(user, password, url, description, outputResult=True): + if outputResult: + print description + try: + r = requests.get(url, auth=(user,password)) + if outputResult: + process_result(r) + return r + except(requests.exceptions.ConnectionError): + if outputResult: + print "Unable to connect; are you sure the controller is up?" + sys.exit(1) + +def post_request(user, password, url, description, payload, params): + print description + try: + r = requests.post(url, auth=(user,password), data=payload, headers=params) + process_result(r) + except(requests.exceptions.ConnectionError): + print "Unable to connect; are you sure the controller is up?" + sys.exit(1) + +def put_request(user, password, url, description, payload, params): + print description + try: + r = requests.put(url, auth=(user,password), data=payload, headers=params) + process_result(r) + except(requests.exceptions.ConnectionError): + print "Unable to connect; are you sure the controller is up?" + sys.exit(1) + +def delete_request(user, password, url, description, payload='', params={'Content-Type':'application/json'}): + print description + try: + r = requests.delete(url, auth=(user,password), data=payload, headers=params) + process_result(r) + except(requests.exceptions.ConnectionError): + print "Unable to connect; are you sure the controller is up?" + sys.exit(1) + +def poll_new_password(): + new_password = getpass.getpass(prompt="Enter new password: ") + new_password_repeated = getpass.getpass(prompt="Re-enter password: ") + if new_password != new_password_repeated: + print "Passwords did not match; cancelling the add_user request" + sys.exit(1) + return new_password + +def list_users(user, password): + get_request(user, password, target_host + 'auth/v1/users', 'list_users') + +def add_user(user, password, newUser): + new_password = poll_new_password() + description = 'add_user({})'.format(user) + url = target_host + 'auth/v1/users' + payload = {'name':newUser, 'password':new_password, 'description':'', "domainid":"sdn", 'userid':'{}@sdn'.format(newUser), 'email':''} + jsonpayload = json.dumps(payload) + headers={'Content-Type':'application/json'} + post_request(user, password, url, description, jsonpayload, headers) + +def delete_user(user, password, userid): + url = target_host + 'auth/v1/users/{}'.format(userid) + description = 'delete_user({})'.format(userid) + delete_request(user, password, url, description) + +def change_password(user, password, existingUserId): + url = target_host + 'auth/v1/users/{}'.format(existingUserId) + r = get_request(user, password, target_host + 'auth/v1/users/{}'.format(existingUserId), 'list_users', outputResult=False) + try: + existing = r.json() + del existing['salt'] + del existing['password'] + new_password = poll_new_password() + existing['password'] = new_password + description='change_password({})'.format(existingUserId) + headers={'Content-Type':'application/json'} + url = target_host + 'auth/v1/users/{}'.format(existingUserId) + put_request(user, password, url, 'change_password({})'.format(user), json.dumps(existing), headers) + except(AttributeError): + print "Unable to connect; are you sure the controller is up?" + sys.exit(1) + +def list_domains(user, password): + get_request(user, password, target_host + 'auth/v1/domains', 'list_domains') + +def list_roles(user, password): + get_request(user, password, target_host + 'auth/v1/roles', 'list_roles') + +def add_role(user, password, role): + url = target_host + 'auth/v1/roles' + description = 'add_role({})'.format(role) + payload = {"roleid":'{}@sdn'.format(role), 'name':role, 'description':'', 'domainid':'sdn'} + data = json.dumps(payload) + headers={'Content-Type':'application/json'} + post_request(user, password, url, description, data, headers) + +def delete_role(user, password, roleid): + url = target_host + 'auth/v1/roles/{}'.format(roleid) + description = 'delete_role({})'.format(roleid) + delete_request(user, password, url, description) + +def add_grant(user, password, userid, roleid): + description = 'add_grant(userid={},roleid={})'.format(userid, roleid) + payload = {"roleid":roleid, "userid":userid, "grantid":'{}@{}@{}'.format(userid, roleid, "sdn"), "domainid":"sdn"} + url = target_host + 'auth/v1/domains/sdn/users/{}/roles'.format(userid) + data=json.dumps(payload) + headers={'Content-Type':'application/json'} + post_request(user, password, url, description, data, headers) + +def get_grants(user, password, userid): + get_request(user, password, target_host + 'auth/v1/domains/sdn/users/{}/roles'.format(userid), 'get_grants({})'.format(userid)) + +def delete_grant(user, password, userid, roleid): + url = target_host + 'auth/v1/domains/sdn/users/{}/roles/{}'.format(userid, roleid) + print url + description = 'delete_grant(userid={},roleid={})'.format(userid, roleid) + delete_request(user, password, url, description) + +args = parser.parse_args() +command = args.func.prog.split()[1:] +user = args.user[0] +password = getpass.getpass() +temp_host_arr = args.target_host +if temp_host_arr is not None: + temp_host_val = temp_host_arr[0] + if temp_host_val is not None: + target_host = temp_host_val + if not target_host.endswith("/"): + target_host += "/" +if "list-users" in command: + list_users(user,password) +if "list-domains" in command: + list_domains(user,password) +if "list-roles" in command: + list_roles(user,password) +if "add-user" in command: + add_user(user,password, args.newUser[0]) +if "add-grant" in command: + add_grant(user,password, args.userid[0], args.roleid[0]) +if "get-grants" in command: + get_grants(user,password, args.userid[0]) +if "change-password" in command: + change_password(user, password, args.userid[0]) +if "delete-user" in command: + delete_user(user, password, args.userid[0]) +if "delete-role" in command: + delete_role(user, password, args.roleid[0]) +if "add-role" in command: + add_role(user, password, args.role[0]) +if "delete-grant" in command: + delete_grant(user, password, args.userid[0], args.roleid[0]) + diff --git a/odl-aaa-moon/aaa/aaa-idmlight/src/main/resources/initial/08-aaa-idmlight-config.xml b/odl-aaa-moon/aaa/aaa-idmlight/src/main/resources/initial/08-aaa-idmlight-config.xml new file mode 100644 index 00000000..695ce762 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-idmlight/src/main/resources/initial/08-aaa-idmlight-config.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- vi: set et smarttab sw=4 tabstop=4: --> +<!-- + Copyright (c) 2015 Cisco Systems, Inc. and others. 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 +--> +<snapshot> + <configuration> + <data xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"> + <modules xmlns="urn:opendaylight:params:xml:ns:yang:controller:config"> + <module> + <type xmlns:authn="config:aaa:authn:idmlight">authn:aaa-idmlight</type> + <name>aaa-idmlight</name> + </module> + </modules> + </data> + </configuration> + <required-capabilities> + <capability>config:aaa:authn:idmlight?module=aaa-idmlight&revision=2015-12-04</capability> + </required-capabilities> + +</snapshot> + diff --git a/odl-aaa-moon/aaa/aaa-idmlight/src/main/yang/aaa-idmlight.yang b/odl-aaa-moon/aaa/aaa-idmlight/src/main/yang/aaa-idmlight.yang new file mode 100644 index 00000000..4f28d755 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-idmlight/src/main/yang/aaa-idmlight.yang @@ -0,0 +1,28 @@ +module aaa-idmlight { + yang-version 1; + namespace "config:aaa:authn:idmlight"; + prefix "aaa-idmlight"; + organization "OpenDayLight"; + + import config { prefix config; revision-date 2013-04-05; } + import opendaylight-md-sal-binding { prefix mdsal; revision-date 2013-10-28; } + + contact "saichler@gmail.com"; + + revision 2015-12-04 { + description + "Initial revision."; + } + + identity aaa-idmlight { + base config:module-type; + config:java-name-prefix AAAIDMLight; + } + + augment "/config:modules/config:module/config:configuration" { + case aaa-idmlight { + when "/config:modules/config:module/config:type = 'aaa-idmlight'"; + } + } + +} diff --git a/odl-aaa-moon/aaa/aaa-idmlight/src/test/java/org/opendaylight/aaa/idm/persistence/PasswordHashTest.java b/odl-aaa-moon/aaa/aaa-idmlight/src/test/java/org/opendaylight/aaa/idm/persistence/PasswordHashTest.java new file mode 100644 index 00000000..44fadf7a --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-idmlight/src/test/java/org/opendaylight/aaa/idm/persistence/PasswordHashTest.java @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2015 Cisco Systems, Inc. and others. 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.idm.persistence; + +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; +import org.opendaylight.aaa.api.IDMStoreException; +import org.opendaylight.aaa.api.IIDMStore; +import org.opendaylight.aaa.api.PasswordCredentials; +import org.opendaylight.aaa.api.SHA256Calculator; +import org.opendaylight.aaa.api.model.Domain; +import org.opendaylight.aaa.api.model.Grant; +import org.opendaylight.aaa.api.model.Grants; +import org.opendaylight.aaa.api.model.Role; +import org.opendaylight.aaa.api.model.User; +import org.opendaylight.aaa.api.model.Users; +import org.opendaylight.aaa.idm.IdmLightProxy; +import org.opendaylight.yang.gen.v1.config.aaa.authn.idmlight.rev151204.AAAIDMLightModule; + +/* + * @Author - Sharon Aicler (saichler@cisco.com) +*/ +public class PasswordHashTest { + + @Before + public void before() throws IDMStoreException{ + IIDMStore store = Mockito.mock(IIDMStore.class); + AAAIDMLightModule.setStore(store); + Domain domain = new Domain(); + domain.setName("sdn"); + domain.setDomainid("sdn"); + + Mockito.when(store.readDomain("sdn")).thenReturn(domain); + Creds c = new Creds(); + Users users = new Users(); + User user = new User(); + user.setName("admin"); + user.setUserid(c.username()); + user.setDomainid("sdn"); + user.setSalt("ABCD"); + user.setPassword(SHA256Calculator.getSHA256(c.password(),user.getSalt())); + List<User> lu = new LinkedList<>(); + lu.add(user); + users.setUsers(lu); + + Grants grants = new Grants(); + Grant grant = new Grant(); + List<Grant> g = new ArrayList<>(); + g.add(grant); + grant.setDomainid("sdn"); + grant.setRoleid("admin"); + grant.setUserid("admin"); + grants.setGrants(g); + Role role = new Role(); + role.setRoleid("admin"); + role.setName("admin"); + Mockito.when(store.readRole("admin")).thenReturn(role); + Mockito.when(store.getUsers(c.username(), c.domain())).thenReturn(users); + Mockito.when(store.getGrants(c.domain(), c.username())).thenReturn(grants); + } + + @Test + public void testPasswordHash(){ + IdmLightProxy proxy = new IdmLightProxy(); + proxy.authenticate(new Creds()); + } + + private static class Creds implements PasswordCredentials { + @Override + public String username() { + return "admin"; + } + @Override + public String password() { + return "admin"; + } + @Override + public String domain() { + return "sdn"; + } + } +} diff --git a/odl-aaa-moon/aaa/aaa-idmlight/src/test/java/org/opendaylight/aaa/idm/rest/test/DomainHandlerTest.java b/odl-aaa-moon/aaa/aaa-idmlight/src/test/java/org/opendaylight/aaa/idm/rest/test/DomainHandlerTest.java new file mode 100644 index 00000000..a8b964ae --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-idmlight/src/test/java/org/opendaylight/aaa/idm/rest/test/DomainHandlerTest.java @@ -0,0 +1,130 @@ +/* + * Copyright (c) 2016 Inocybe Technologies and others. 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.idm.rest.test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import com.sun.jersey.api.client.ClientResponse; +import com.sun.jersey.api.client.UniformInterfaceException; +import java.util.HashMap; +import java.util.Map; +import javax.ws.rs.core.MediaType; +import org.junit.Test; +import org.opendaylight.aaa.api.model.Domain; +import org.opendaylight.aaa.api.model.Domains; +import org.opendaylight.aaa.api.model.IDMError; +import org.opendaylight.aaa.api.model.Roles; + +public class DomainHandlerTest extends HandlerTest{ + + @Test + public void testDomainHandler() { + //check default domains + Domains domains = resource().path("/v1/domains").get(Domains.class); + assertNotNull(domains); + assertEquals(1, domains.getDomains().size()); + assertTrue(domains.getDomains().get(0).getName().equals("sdn")); + + //check existing domain + Domain domain = resource().path("/v1/domains/0").get(Domain.class); + assertNotNull(domain); + assertTrue(domain.getName().equals("sdn")); + + //check not exist domain + try { + resource().path("/v1/domains/5").get(IDMError.class); + fail("Should failed with 404!"); + } catch (UniformInterfaceException e) { + ClientResponse resp = e.getResponse(); + assertEquals(404, resp.getStatus()); + assertTrue(resp.getEntity(IDMError.class).getMessage().contains("Not found! domain id")); + } + + // check create domain + Map<String, String> domainData = new HashMap<String, String>(); + domainData.put("name","dom1"); + domainData.put("description","test dom"); + domainData.put("domainid","1"); + domainData.put("enabled","true"); + ClientResponse clientResponse = resource().path("/v1/domains").type(MediaType.APPLICATION_JSON).post(ClientResponse.class, domainData); + assertEquals(201, clientResponse.getStatus()); + + // check update domain data + domainData.put("name","dom1Update"); + clientResponse = resource().path("/v1/domains/1").type(MediaType.APPLICATION_JSON).put(ClientResponse.class, domainData); + assertEquals(200, clientResponse.getStatus()); + domain = resource().path("/v1/domains/1").get(Domain.class); + assertNotNull(domain); + assertTrue(domain.getName().equals("dom1Update")); + + // check create grant + Map<String, String> grantData = new HashMap<String, String>(); + grantData.put("roleid","1"); + clientResponse = resource().path("/v1/domains/1/users/0/roles").type(MediaType.APPLICATION_JSON).post(ClientResponse.class, grantData); + assertEquals(201, clientResponse.getStatus()); + + // check create existing grant + clientResponse = resource().path("/v1/domains/1/users/0/roles").type(MediaType.APPLICATION_JSON).post(ClientResponse.class, grantData); + assertEquals(403, clientResponse.getStatus()); + + // check create grant with invalid domain id + clientResponse = resource().path("/v1/domains/5/users/0/roles").type(MediaType.APPLICATION_JSON).post(ClientResponse.class, grantData); + assertEquals(404, clientResponse.getStatus()); + + // check validate user (admin) + Map<String, String> usrPwdData = new HashMap<String, String>(); + usrPwdData.put("username","admin"); + usrPwdData.put("userpwd","admin"); + clientResponse = resource().path("/v1/domains/0/users/roles").type(MediaType.APPLICATION_JSON).post(ClientResponse.class, usrPwdData); + assertEquals(200, clientResponse.getStatus()); + + // check validate user (admin) with wrong password + usrPwdData.put("userpwd","1234"); + clientResponse = resource().path("/v1/domains/0/users/roles").type(MediaType.APPLICATION_JSON).post(ClientResponse.class, usrPwdData); + assertEquals(401, clientResponse.getStatus()); + + // check get user (admin) roles + Roles usrRoles = resource().path("/v1/domains/0/users/0/roles").get(Roles.class); + assertNotNull(usrRoles); + assertTrue(usrRoles.getRoles().size() > 1); + + // check get invalid user roles + try { + resource().path("/v1/domains/0/users/5/roles").get(IDMError.class); + fail("Should failed with 404!"); + } catch (UniformInterfaceException e) { + ClientResponse resp = e.getResponse(); + assertEquals(404, resp.getStatus()); + } + + // check delete grant + clientResponse = resource().path("/v1/domains/0/users/0/roles/0").delete(ClientResponse.class); + assertEquals(204, clientResponse.getStatus()); + + // check delete grant for invalid domain + clientResponse = resource().path("/v1/domains/3/users/0/roles/0").delete(ClientResponse.class); + assertEquals(404, clientResponse.getStatus()); + + // check delete domain + clientResponse = resource().path("/v1/domains/1").delete(ClientResponse.class); + assertEquals(204, clientResponse.getStatus()); + + // check delete not existing domain + try { + resource().path("/v1/domains/1").delete(IDMError.class); + fail("Shoulda failed with 404!"); + } catch (UniformInterfaceException e) { + ClientResponse resp = e.getResponse(); + assertEquals(404, resp.getStatus()); + assertTrue(resp.getEntity(IDMError.class).getMessage().contains("Not found! Domain id")); + } + } +} diff --git a/odl-aaa-moon/aaa/aaa-idmlight/src/test/java/org/opendaylight/aaa/idm/rest/test/HandlerTest.java b/odl-aaa-moon/aaa/aaa-idmlight/src/test/java/org/opendaylight/aaa/idm/rest/test/HandlerTest.java new file mode 100644 index 00000000..7b8eebb4 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-idmlight/src/test/java/org/opendaylight/aaa/idm/rest/test/HandlerTest.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2016 Inocybe Technologies and others. 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.idm.rest.test; + +import com.sun.jersey.spi.container.servlet.WebComponent; +import com.sun.jersey.test.framework.AppDescriptor; +import com.sun.jersey.test.framework.JerseyTest; +import com.sun.jersey.test.framework.WebAppDescriptor; +import org.junit.Before; +import org.opendaylight.aaa.idm.IdmLightApplication; +import org.opendaylight.aaa.idm.StoreBuilder; +import org.opendaylight.yang.gen.v1.config.aaa.authn.idmlight.rev151204.AAAIDMLightModule; + + +public abstract class HandlerTest extends JerseyTest { + + protected IDMTestStore testStore = new IDMTestStore(); + + @Override + protected AppDescriptor configure() { + return new WebAppDescriptor.Builder() + .initParam(WebComponent.RESOURCE_CONFIG_CLASS, IdmLightApplication.class.getName()) + .build(); + } + + @Before + public void setUp() throws Exception { + super.setUp(); + StoreBuilder.init(testStore); + AAAIDMLightModule.setStore(testStore); + } +} diff --git a/odl-aaa-moon/aaa/aaa-idmlight/src/test/java/org/opendaylight/aaa/idm/rest/test/IDMTestStore.java b/odl-aaa-moon/aaa/aaa-idmlight/src/test/java/org/opendaylight/aaa/idm/rest/test/IDMTestStore.java new file mode 100644 index 00000000..0fed2789 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-idmlight/src/test/java/org/opendaylight/aaa/idm/rest/test/IDMTestStore.java @@ -0,0 +1,271 @@ +/* + * Copyright (c) 2016 Inocybe Technologies and others. 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.idm.rest.test; + +import java.util.ArrayList; +import java.util.List; + +import org.opendaylight.aaa.api.IDMStoreException; +import org.opendaylight.aaa.api.IIDMStore; +import org.opendaylight.aaa.api.model.Domain; +import org.opendaylight.aaa.api.model.Domains; +import org.opendaylight.aaa.api.model.Grant; +import org.opendaylight.aaa.api.model.Grants; +import org.opendaylight.aaa.api.model.Role; +import org.opendaylight.aaa.api.model.Roles; +import org.opendaylight.aaa.api.model.User; +import org.opendaylight.aaa.api.model.Users; + +public class IDMTestStore implements IIDMStore { + + private List<Domain> domains = new ArrayList<Domain>(); + private List<Grant> grants = new ArrayList<Grant>(); + private List<Role> roles = new ArrayList<Role>(); + private List<User> users = new ArrayList<User>(); + + public IDMTestStore() { + // TODO Auto-generated constructor stub + } + + @Override + public Domain writeDomain(Domain domain) throws IDMStoreException { + domain.setDomainid(String.valueOf(domains.size())); + domains.add(domain); + return domain; + } + + @Override + public Domain readDomain(String domainid) throws IDMStoreException { + for(Domain dom : domains) { + if (dom.getDomainid().equals(domainid)) { + return dom; + } + } + return null; + } + + @Override + public Domain deleteDomain(String domainid) throws IDMStoreException { + for(Domain dom : domains) { + if (dom.getDomainid().equals(domainid)) { + domains.remove(dom); + return dom; + } + } + return null; + } + + @Override + public Domain updateDomain(Domain domain) throws IDMStoreException { + for(Domain dom : domains) { + if (dom.getDomainid().equals(domain.getDomainid())) { + domains.remove(dom); + domains.add(domain); + return domain; + } + } + return null; + } + + @Override + public Domains getDomains() throws IDMStoreException { + Domains doms = new Domains(); + doms.setDomains(domains); + return doms; + } + + @Override + public Role writeRole(Role role) throws IDMStoreException { + role.setRoleid(String.valueOf(roles.size())); + roles.add(role); + return role; + } + + @Override + public Role readRole(String roleid) throws IDMStoreException { + for (Role role : roles) { + if (role.getRoleid().equals(roleid)) { + return role; + } + } + return null; + } + + @Override + public Role deleteRole(String roleid) throws IDMStoreException { + for (Role role : roles) { + if (role.getRoleid().equals(roleid)) { + roles.remove(role); + return role; + } + } + return null; + } + + @Override + public Role updateRole(Role role) throws IDMStoreException { + for (Role inRole : roles) { + if (inRole.getRoleid().equals(role.getRoleid())) { + roles.remove(inRole); + roles.add(role); + return role; + } + } + return null; + } + + @Override + public Roles getRoles() throws IDMStoreException { + Roles rols = new Roles(); + rols.setRoles(roles); + return rols; + } + + @Override + public User writeUser(User user) throws IDMStoreException { + user.setUserid(String.valueOf(users.size())); + users.add(user); + return user; + } + + @Override + public User readUser(String userid) throws IDMStoreException { + for(User usr : users) { + if (usr.getUserid().equals(userid)) { + return usr; + } + } + return null; + } + + @Override + public User deleteUser(String userid) throws IDMStoreException { + for(User usr : users) { + if (usr.getUserid().equals(userid)) { + users.remove(usr); + return usr; + } + } + return null; + } + + @Override + public User updateUser(User user) throws IDMStoreException { + for(User usr : users) { + if (usr.getUserid().equals(user.getUserid())) { + users.remove(usr); + users.add(user); + return usr; + } + } + return null; + } + + @Override + public Users getUsers() throws IDMStoreException { + Users usrs = new Users(); + usrs.setUsers(users); + return usrs; + } + + @Override + public Users getUsers(String username, String domainId) throws IDMStoreException { + Users usrs = new Users(); + User user = null; + Domain domain = null; + for(User usr : users) { + if (usr.getName().equals(username)) { + user = usr; + break; + } + } + for(Domain dom : domains) { + if (dom.getDomainid().equals(domainId)) { + domain = dom; + break; + } + } + if (user == null || domain == null) + return usrs; + for (Grant grant : grants) { + if (grant.getUserid().equals(user.getUserid()) && grant.getDomainid().equals(domain.getDomainid())) { + List<User> usrList = new ArrayList<User>(); + usrList.add(user); + usrs.setUsers(usrList); + break; + } + } + return usrs; + } + + @Override + public Grant writeGrant(Grant grant) throws IDMStoreException { + grant.setGrantid(String.valueOf(grants.size())); + grants.add(grant); + return grant; + } + + @Override + public Grant readGrant(String grantid) throws IDMStoreException { + for (Grant grant : grants) { + if (grant.getGrantid().equals(grantid)) { + return grant; + } + } + return null; + } + + @Override + public Grant deleteGrant(String grantid) throws IDMStoreException { + for (Grant grant : grants) { + if (grant.getGrantid().equals(grantid)) { + grants.remove(grant); + return grant; + } + } + return null; + } + + @Override + public Grants getGrants(String domainid, String userid) throws IDMStoreException { + Grants usrGrants = new Grants(); + List<Grant> usrGrant = new ArrayList<Grant>(); + for (Grant grant : grants) { + if (grant.getUserid().equals(userid) && grant.getDomainid().equals(domainid)) { + usrGrant.add(grant); + } + } + usrGrants.setGrants(usrGrant); + return usrGrants; + } + + @Override + public Grants getGrants(String userid) throws IDMStoreException { + Grants usrGrants = new Grants(); + List<Grant> usrGrant = new ArrayList<Grant>(); + for (Grant grant : grants) { + if (grant.getUserid().equals(userid)) { + usrGrant.add(grant); + } + } + usrGrants.setGrants(usrGrant); + return usrGrants; + } + + @Override + public Grant readGrant(String domainid, String userid, String roleid) throws IDMStoreException { + for (Grant grant : grants) { + if (grant.getDomainid().equals(domainid) && grant.getUserid().equals(userid) && grant.getRoleid().equals(roleid)) { + return grant; + } + } + return null; + } + +} diff --git a/odl-aaa-moon/aaa/aaa-idmlight/src/test/java/org/opendaylight/aaa/idm/rest/test/RoleHandlerTest.java b/odl-aaa-moon/aaa/aaa-idmlight/src/test/java/org/opendaylight/aaa/idm/rest/test/RoleHandlerTest.java new file mode 100644 index 00000000..baf59558 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-idmlight/src/test/java/org/opendaylight/aaa/idm/rest/test/RoleHandlerTest.java @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2016 Inocybe Technologies and others. 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.idm.rest.test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import com.sun.jersey.api.client.ClientResponse; +import com.sun.jersey.api.client.UniformInterfaceException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import javax.ws.rs.core.MediaType; +import org.junit.Test; +import org.opendaylight.aaa.api.model.IDMError; +import org.opendaylight.aaa.api.model.Role; +import org.opendaylight.aaa.api.model.Roles; + + +public class RoleHandlerTest extends HandlerTest{ + + @Test + public void testRoleHandler() { + //check default roles + Roles roles = resource().path("/v1/roles").get(Roles.class); + assertNotNull(roles); + List<Role> roleList = roles.getRoles(); + assertEquals(2, roleList.size()); + for (Role role : roleList) { + assertTrue(role.getName().equals("admin") || role.getName().equals("user")); + } + + //check existing role + Role role = resource().path("/v1/roles/0").get(Role.class); + assertNotNull(role); + assertTrue(role.getName().equals("admin")); + + //check not exist Role + try { + resource().path("/v1/roles/5").get(IDMError.class); + fail("Should failed with 404!"); + } catch (UniformInterfaceException e) { + ClientResponse resp = e.getResponse(); + assertEquals(404, resp.getStatus()); + assertTrue(resp.getEntity(IDMError.class).getMessage().contains("role not found")); + } + + // check create Role + Map<String, String> roleData = new HashMap<String, String>(); + roleData.put("name","role1"); + roleData.put("description","test Role"); + roleData.put("domainid","0"); + ClientResponse clientResponse = resource().path("/v1/roles").type(MediaType.APPLICATION_JSON).post(ClientResponse.class, roleData); + assertEquals(201, clientResponse.getStatus()); + + // check create Role missing name data + roleData.remove("name"); + try { + clientResponse = resource().path("/v1/roles").type(MediaType.APPLICATION_JSON).post(ClientResponse.class, roleData); + assertEquals(404, clientResponse.getStatus()); + } catch (UniformInterfaceException e) { + ClientResponse resp = e.getResponse(); + assertEquals(500, resp.getStatus()); + } + + // check update Role data + roleData.put("name","role1Update"); + clientResponse = resource().path("/v1/roles/2").type(MediaType.APPLICATION_JSON).put(ClientResponse.class, roleData); + assertEquals(200, clientResponse.getStatus()); + role = resource().path("/v1/roles/2").get(Role.class); + assertNotNull(role); + assertTrue(role.getName().equals("role1Update")); + + // check delete Role + clientResponse = resource().path("/v1/roles/2").delete(ClientResponse.class); + assertEquals(204, clientResponse.getStatus()); + + // check delete not existing Role + try { + resource().path("/v1/roles/2").delete(IDMError.class); + fail("Should failed with 404!"); + } catch (UniformInterfaceException e) { + ClientResponse resp = e.getResponse(); + assertEquals(404, resp.getStatus()); + assertTrue(resp.getEntity(IDMError.class).getMessage().contains("role id not found")); + } + } +} diff --git a/odl-aaa-moon/aaa/aaa-idmlight/src/test/java/org/opendaylight/aaa/idm/rest/test/UserHandlerTest.java b/odl-aaa-moon/aaa/aaa-idmlight/src/test/java/org/opendaylight/aaa/idm/rest/test/UserHandlerTest.java new file mode 100644 index 00000000..115546b6 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-idmlight/src/test/java/org/opendaylight/aaa/idm/rest/test/UserHandlerTest.java @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2016 Inocybe Technologies and others. 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.idm.rest.test; + +import static org.junit.Assert.*; + +import com.sun.jersey.api.client.ClientResponse; +import com.sun.jersey.api.client.UniformInterfaceException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import javax.ws.rs.core.MediaType; +import org.junit.Test; +import org.opendaylight.aaa.api.model.IDMError; +import org.opendaylight.aaa.api.model.User; +import org.opendaylight.aaa.api.model.Users; + +public class UserHandlerTest extends HandlerTest { + + @Test + public void testUserHandler() { + //check default users + Users users = resource().path("/v1/users").get(Users.class); + assertNotNull(users); + List<User> usrList = users.getUsers(); + assertEquals(2, usrList.size()); + for (User usr : usrList) { + assertTrue(usr.getName().equals("admin") || usr.getName().equals("user")); + } + + //check existing user + User usr = resource().path("/v1/users/0").get(User.class); + assertNotNull(usr); + assertTrue(usr.getName().equals("admin")); + + //check not exist user + try { + resource().path("/v1/users/5").get(IDMError.class); + fail("Should failed with 404!"); + } catch (UniformInterfaceException e) { + ClientResponse resp = e.getResponse(); + assertEquals(404, resp.getStatus()); + assertTrue(resp.getEntity(IDMError.class).getMessage().contains("user not found")); + } + + // check create user + Map<String, String> usrData = new HashMap<String, String>(); + usrData.put("name","usr1"); + usrData.put("description","test user"); + usrData.put("enabled","true"); + usrData.put("email","user1@usr.org"); + usrData.put("password","ChangeZbadPa$$w0rd"); + usrData.put("domainid","0"); + ClientResponse clientResponse = resource().path("/v1/users").type(MediaType.APPLICATION_JSON).post(ClientResponse.class, usrData); + assertEquals(201, clientResponse.getStatus()); + + // check create user missing name data + usrData.remove("name"); + try { + clientResponse = resource().path("/v1/users").type(MediaType.APPLICATION_JSON).post(ClientResponse.class, usrData); + assertEquals(400, clientResponse.getStatus()); + } catch (UniformInterfaceException e) { + ClientResponse resp = e.getResponse(); + assertEquals(500, resp.getStatus()); + } + + // check update user data + usrData.put("name","usr1Update"); + clientResponse = resource().path("/v1/users/2").type(MediaType.APPLICATION_JSON).put(ClientResponse.class, usrData); + assertEquals(200, clientResponse.getStatus()); + usr = resource().path("/v1/users/2").get(User.class); + assertNotNull(usr); + assertTrue(usr.getName().equals("usr1Update")); + + // check delete user + clientResponse = resource().path("/v1/users/2").delete(ClientResponse.class); + assertEquals(204, clientResponse.getStatus()); + + // check delete not existing user + try { + resource().path("/v1/users/2").delete(IDMError.class); + fail("Should failed with 404!"); + } catch (UniformInterfaceException e) { + ClientResponse resp = e.getResponse(); + assertEquals(404, resp.getStatus()); + assertTrue(resp.getEntity(IDMError.class).getMessage().contains("Couldn't find user")); + } + } + +} diff --git a/odl-aaa-moon/aaa/aaa-idmlight/tests/cleardb.sh b/odl-aaa-moon/aaa/aaa-idmlight/tests/cleardb.sh new file mode 100755 index 00000000..6385b48d --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-idmlight/tests/cleardb.sh @@ -0,0 +1,5 @@ +sudo service idmlight stop +echo "dropping all tables..." +sleep 3 +sudo sqlite3 /opt/idmlight/dmlight.db < ../sql/idmlight.sql +sudo service idmlight start diff --git a/odl-aaa-moon/aaa/aaa-idmlight/tests/domain.json b/odl-aaa-moon/aaa/aaa-idmlight/tests/domain.json new file mode 100644 index 00000000..4dfd25e9 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-idmlight/tests/domain.json @@ -0,0 +1,5 @@ +{ + "domainid": "1", + "name":"R&D", + "enabled":"true" +} diff --git a/odl-aaa-moon/aaa/aaa-idmlight/tests/domain2.json b/odl-aaa-moon/aaa/aaa-idmlight/tests/domain2.json new file mode 100644 index 00000000..69244b30 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-idmlight/tests/domain2.json @@ -0,0 +1,5 @@ +{ + "domainid": "1", + "name":"ATG", + "enabled":"true" +} diff --git a/odl-aaa-moon/aaa/aaa-idmlight/tests/grant.json b/odl-aaa-moon/aaa/aaa-idmlight/tests/grant.json new file mode 100644 index 00000000..0c4a9e90 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-idmlight/tests/grant.json @@ -0,0 +1,4 @@ +{ + "roleid":"2", + "description":"role grant" +} diff --git a/odl-aaa-moon/aaa/aaa-idmlight/tests/grant2.json b/odl-aaa-moon/aaa/aaa-idmlight/tests/grant2.json new file mode 100644 index 00000000..ad685b7a --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-idmlight/tests/grant2.json @@ -0,0 +1,4 @@ +{ + "roleid":"3", + "description":"role grant" +} diff --git a/odl-aaa-moon/aaa/aaa-idmlight/tests/result.json b/odl-aaa-moon/aaa/aaa-idmlight/tests/result.json new file mode 100644 index 00000000..a3dd995d --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-idmlight/tests/result.json @@ -0,0 +1 @@ +{"domainid":2,"userid":2,"username":"peter","roles":[{"roleid":2,"name":"user","description":"A user role with limited access"},{"roleid":3,"name":"user","description":"A user role with limited access"}]}
\ No newline at end of file diff --git a/odl-aaa-moon/aaa/aaa-idmlight/tests/role-admin.json b/odl-aaa-moon/aaa/aaa-idmlight/tests/role-admin.json new file mode 100644 index 00000000..cf93caae --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-idmlight/tests/role-admin.json @@ -0,0 +1,4 @@ +{ + "name":"admin", + "description":"An admin role with full access" +} diff --git a/odl-aaa-moon/aaa/aaa-idmlight/tests/role-user.json b/odl-aaa-moon/aaa/aaa-idmlight/tests/role-user.json new file mode 100644 index 00000000..78588c9a --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-idmlight/tests/role-user.json @@ -0,0 +1,4 @@ +{ + "name":"user", + "description":"A user role with limited access" +} diff --git a/odl-aaa-moon/aaa/aaa-idmlight/tests/test.sh b/odl-aaa-moon/aaa/aaa-idmlight/tests/test.sh new file mode 100755 index 00000000..3589be58 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-idmlight/tests/test.sh @@ -0,0 +1,308 @@ +# GLOBAL VARS +TARGET="localhost:8282/auth" +TESTCOUNT=0 +PASSCOUNT=0 +FAILCOUNT=0 + +getit() { +((TESTCOUNT++)) +echo '['$TESTCOUNT']' $NAME +echo GET $URL +echo "Desired Result=" $PASSCODE +STATUS=$(curl -X GET -k -s -H Accept:application/json -o result.json -w '%{http_code}' $URL) +if [ $STATUS -eq $PASSCODE ]; then + ((PASSCOUNT++)) + cat result.json | python -mjson.tool + echo "[PASS] Status=" $STATUS +else + cat result.json | python -mjson.tool + echo "[FAIL] Status=" $STATUS + ((FAILCOUNT++)) +fi +echo +} + + +deleteit() { +((TESTCOUNT++)) +echo '['$TESTCOUNT']' $NAME +echo DELETE $URL +echo "Desired Result=" $PASSCODE +STATUS=$(curl -X DELETE -k -s -H Accept:application/json -o result.json -w '%{http_code}' $URL) +if [ $STATUS -eq $PASSCODE ]; then + ((PASSCOUNT++)) + echo "[PASS] Status=" $STATUS +else + cat result.json | python -mjson.tool + echo "[FAIL] Status=" $STATUS + ((FAILCOUNT++)) +fi +echo +} + +postit() { +((TESTCOUNT++)) +echo '['$TESTCOUNT']' $NAME +echo POST $URL +echo "Desired Result=" $PASSCODE +echo "POST File=" $POSTFILE +STATUS=$(curl -X POST -k -s -H "Content-type:application/json" --data-binary "@"$POSTFILE -o result.json -w '%{http_code}' $URL) +if [ $STATUS -eq $PASSCODE ]; then + ((PASSCOUNT++)) + cat result.json | python -mjson.tool + echo "[PASS] Status=" $STATUS +else + cat result.json | python -mjson.tool + echo "[FAIL] Status=" $STATUS + ((FAILCOUNT++)) +fi +echo +} + +putit() { +((TESTCOUNT++)) +echo '['$TESTCOUNT']' $NAME +echo PUT $URL +echo "Desired Result=" $PASSCODE +echo "PUT file=" $PUTFILE +STATUS=$(curl -X PUT -k -s -H "Content-type:application/json" --data-binary "@"$PUTFILE -o result.json -w '%{http_code}' $URL) +if [ $STATUS -eq $PASSCODE ]; then + ((PASSCOUNT++)) + cat result.json | python -mjson.tool + echo "[PASS] Status=" $STATUS +else + cat result.json | python -mjson.tool + echo "[FAIL] Status=" $STATUS + ((FAILCOUNT++)) +fi +echo +} + + +# +# DOMAIN TESTS +# + +NAME="get all domains" +URL="http://$TARGET/v1/domains" +PASSCODE=200 +getit + +NAME="create a new domain" +URL="http://$TARGET/v1/domains" +POSTFILE=domain.json +PASSCODE=201 +postit + +NAME="get domain 1" +URL="http://$TARGET/v1/domains/1" +PASSCODE=200 +getit + +NAME="delete domain 1" +URL="http://$TARGET/v1/domains/1" +PASSCODE=204 +deleteit + +NAME="create a new domain" +URL="http://$TARGET/v1/domains" +POSTFILE=domain.json +PASSCODE=201 +postit + +NAME="get all domains" +URL="http://$TARGET/v1/domains" +PASSCODE=200 +getit + +NAME="update domain 2" +URL="http://$TARGET/v1/domains/2" +PUTFILE=domain.json +PASSCODE=200 +putit + +NAME="create a new domain" +URL="http://$TARGET/v1/domains" +POSTFILE=domain2.json +PASSCODE=201 +postit + +NAME="get all domains" +URL="http://$TARGET/v1/domains" +PASSCODE=200 +getit + +# +# USER TESTS +# + +NAME="get all users" +URL="http://$TARGET/v1/users" +PASSCODE=200 +getit + +NAME="create a new user" +URL="http://$TARGET/v1/users" +POSTFILE=user.json +PASSCODE=201 +postit + +NAME="get all users" +URL="http://$TARGET/v1/users" +PASSCODE=200 +getit + +NAME="get user 1" +URL="http://$TARGET/v1/users/1" +PASSCODE=200 +getit + +NAME="delete user 1" +URL="http://$TARGET/v1/users/1" +PASSCODE=204 +deleteit + +NAME="get all users" +URL="http://$TARGET/v1/users" +PASSCODE=200 +getit + +NAME="create a new user" +URL="http://$TARGET/v1/users" +POSTFILE=user.json +PASSCODE=201 +postit + +NAME="update a user" +URL="http://$TARGET/v1/users/2" +PUTFILE=user.json +PASSCODE=200 +putit + +NAME="create a new user" +URL="http://$TARGET/v1/users" +POSTFILE=user2.json +PASSCODE=201 +postit + +NAME="get all users" +URL="http://$TARGET/v1/users" +PASSCODE=200 +getit + +# ROLE TESTS + +NAME="get all roles" +URL="http://$TARGET/v1/roles" +PASSCODE=200 +getit + +NAME="create a new role" +URL="http://$TARGET/v1/roles" +POSTFILE=role-user.json +PASSCODE=201 +postit + +NAME="get all roles" +URL="http://$TARGET/v1/roles" +PASSCODE=200 +getit + +NAME="get role 1" +URL="http://$TARGET/v1/roles/1" +PASSCODE=200 +getit + +NAME="delete role 1" +URL="http://$TARGET/v1/roles/1" +PASSCODE=204 +deleteit + +NAME="create a new role" +URL="http://$TARGET/v1/roles" +POSTFILE=role-user.json +PASSCODE=201 +postit + +NAME="update role 2" +URL="http://$TARGET/v1/roles/2" +PUTFILE=role-user.json +PASSCODE=200 +putit + +NAME="create a new role" +URL="http://$TARGET/v1/roles" +POSTFILE=role-admin.json +PASSCODE=201 +postit + +NAME="get all roles" +URL="http://$TARGET/v1/roles" +PASSCODE=200 +getit + +# Grant tests + +NAME="grant a role" +URL="http://$TARGET/v1/domains/2/users/2/roles" +POSTFILE=grant.json +PASSCODE=201 +postit + +NAME="try to create a double grant" +URL="http://$TARGET/v1/domains/2/users/2/roles" +POSTFILE=grant.json +PASSCODE=403 +postit + +NAME="get all roles for domain and user" +URL="http://$TARGET/v1/domains/2/users/2/roles" +PASSCODE=200 +getit + +NAME="delete a grant" +URL="http://$TARGET/v1/domains/2/users/2/roles/2" +PASSCODE=204 +deleteit + +NAME="delete a grant" +URL="http://$TARGET/v1/domains/2/users/2/roles/2" +PASSCODE=404 +deleteit + +NAME="get all roles for domain and user" +URL="http://$TARGET/v1/domains/2/users/2/roles" +PASSCODE=200 +getit + +NAME="grant a role" +URL="http://$TARGET/v1/domains/2/users/2/roles" +POSTFILE=grant.json +PASSCODE=201 +postit + +NAME="grant a role" +URL="http://$TARGET/v1/domains/2/users/2/roles" +POSTFILE=grant2.json +PASSCODE=201 +postit + +NAME="get all roles for domain and user" +URL="http://$TARGET/v1/domains/2/users/2/roles" +PASSCODE=200 +getit + +NAME="get all roles for domain, user and pwd" +URL="http://$TARGET/v1/domains/2/users/roles" +POSTFILE=userpwd.json +PASSCODE=200 +postit + + +# +# RESULTS +# +echo "SUMMARY" +echo "======================================" +echo 'TESTS:'$TESTCOUNT 'PASS:'$PASSCOUNT 'FAIL:'$FAILCOUNT + diff --git a/odl-aaa-moon/aaa/aaa-idmlight/tests/user.json b/odl-aaa-moon/aaa/aaa-idmlight/tests/user.json new file mode 100644 index 00000000..6f30d705 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-idmlight/tests/user.json @@ -0,0 +1,7 @@ +{ + "name":"peter", + "description":"peter test user", + "enabled":"true", + "email":"user1@gmail.com", + "password":"foobar" +} diff --git a/odl-aaa-moon/aaa/aaa-idmlight/tests/user2.json b/odl-aaa-moon/aaa/aaa-idmlight/tests/user2.json new file mode 100644 index 00000000..9864cdb2 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-idmlight/tests/user2.json @@ -0,0 +1,7 @@ +{ + "name":"liem", + "description":"liem test user", + "enabled":"true", + "email":"user1@gmail.com", + "password":"foobar" +} diff --git a/odl-aaa-moon/aaa/aaa-idmlight/tests/userpwd.json b/odl-aaa-moon/aaa/aaa-idmlight/tests/userpwd.json new file mode 100644 index 00000000..e5258b98 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-idmlight/tests/userpwd.json @@ -0,0 +1,4 @@ +{ + "username":"peter", + "userpwd":"foobar" +} diff --git a/odl-aaa-moon/aaa/aaa-idp-mapping/pom.xml b/odl-aaa-moon/aaa/aaa-idp-mapping/pom.xml new file mode 100644 index 00000000..d3d37c40 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-idp-mapping/pom.xml @@ -0,0 +1,84 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + <parent> + <groupId>org.opendaylight.aaa</groupId> + <artifactId>aaa-parent</artifactId> + <version>0.3.2-Beryllium-SR2</version> + <relativePath>../parent</relativePath> + </parent> + + <artifactId>aaa-authn-idpmapping</artifactId> + <version>0.3.2-Beryllium-SR2</version> + <packaging>bundle</packaging> + + <properties> + <powermock.version>1.5.2</powermock.version> + </properties> + + <dependencies> + <dependency> + <groupId>org.glassfish</groupId> + <artifactId>javax.json</artifactId> + </dependency> + <dependency> + <groupId>org.osgi</groupId> + <artifactId>org.osgi.core</artifactId> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>org.slf4j</groupId> + <artifactId>slf4j-api</artifactId> + </dependency> + <dependency> + <groupId>org.apache.felix</groupId> + <artifactId>org.apache.felix.dependencymanager</artifactId> + <scope>provided</scope> + </dependency> + + <!-- Test dependencies --> + <dependency> + <groupId>junit</groupId> + <artifactId>junit</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.mockito</groupId> + <artifactId>mockito-all</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.slf4j</groupId> + <artifactId>slf4j-simple</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.powermock</groupId> + <artifactId>powermock-api-mockito</artifactId> + <version>${powermock.version}</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.powermock</groupId> + <artifactId>powermock-module-junit4</artifactId> + <version>${powermock.version}</version> + <scope>test</scope> + </dependency> + </dependencies> + + <build> + <plugins> + <plugin> + <groupId>org.apache.felix</groupId> + <artifactId>maven-bundle-plugin</artifactId> + <extensions>true</extensions> + <configuration> + <instructions> + <Bundle-Activator>org.opendaylight.aaa.idpmapping.Activator</Bundle-Activator> + </instructions> + <manifestLocation>${project.basedir}/META-INF</manifestLocation> + </configuration> + </plugin> + </plugins> + </build> +</project> diff --git a/odl-aaa-moon/aaa/aaa-idp-mapping/src/main/java/org/opendaylight/aaa/idpmapping/Activator.java b/odl-aaa-moon/aaa/aaa-idp-mapping/src/main/java/org/opendaylight/aaa/idpmapping/Activator.java new file mode 100644 index 00000000..7342485e --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-idp-mapping/src/main/java/org/opendaylight/aaa/idpmapping/Activator.java @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2014 Red Hat, Inc. 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 org.apache.felix.dm.DependencyActivatorBase; +import org.apache.felix.dm.DependencyManager; +import org.osgi.framework.BundleContext; + +public class Activator extends DependencyActivatorBase { + + @Override + public void init(BundleContext context, DependencyManager manager) throws Exception { + } + + @Override + public void destroy(BundleContext context, DependencyManager manager) throws Exception { + } + +} diff --git a/odl-aaa-moon/aaa/aaa-idp-mapping/src/main/java/org/opendaylight/aaa/idpmapping/IdpJson.java b/odl-aaa-moon/aaa/aaa-idp-mapping/src/main/java/org/opendaylight/aaa/idpmapping/IdpJson.java new file mode 100644 index 00000000..00328b60 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-idp-mapping/src/main/java/org/opendaylight/aaa/idpmapping/IdpJson.java @@ -0,0 +1,248 @@ +/* + * Copyright (c) 2014 Red Hat, Inc. 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.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +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 <jdennis@redhat.com> + */ + +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 parser.getLong(); + } else { + return parser.getBigDecimal().doubleValue(); + } + } + case VALUE_STRING: { + return parser.getString(); + } + case VALUE_TRUE: { + return Boolean.TRUE; + } + case VALUE_FALSE: { + return 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<Object> loadJsonArray(JsonParser parser, Event event) { + List<Object> list = new ArrayList<Object>(); + + 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<String, Object> loadJsonObject(JsonParser parser, Event event) { + Map<String, Object> map = new LinkedHashMap<String, Object>(); + + 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<String, Object> properties = new HashMap<String, Object>(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<Object> list = (List<Object>) obj; + dumpJsonArray(generator, list); + } else if (obj instanceof Map) { + generator.writeStartObject(); + @SuppressWarnings("unchecked") + Map<String, Object> map = (Map<String, Object>) 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<Object> list) { + for (Object obj : list) { + dumpJsonItem(generator, obj); + } + generator.writeEnd(); + } + + private void dumpJsonObject(JsonGenerator generator, Map<String, Object> map) { + + for (Map.Entry<String, Object> 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<Object> list = (List<Object>) obj; + dumpJsonArray(generator, list); + } else if (obj instanceof Map) { + generator.writeStartObject(key); + @SuppressWarnings("unchecked") + Map<String, Object> map1 = (Map<String, Object>) 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/odl-aaa-moon/aaa/aaa-idp-mapping/src/main/java/org/opendaylight/aaa/idpmapping/InvalidRuleException.java b/odl-aaa-moon/aaa/aaa-idp-mapping/src/main/java/org/opendaylight/aaa/idpmapping/InvalidRuleException.java new file mode 100644 index 00000000..1e42f4f2 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-idp-mapping/src/main/java/org/opendaylight/aaa/idpmapping/InvalidRuleException.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2014 Red Hat, Inc. 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 <jdennis@redhat.com> + */ + +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/odl-aaa-moon/aaa/aaa-idp-mapping/src/main/java/org/opendaylight/aaa/idpmapping/InvalidTypeException.java b/odl-aaa-moon/aaa/aaa-idp-mapping/src/main/java/org/opendaylight/aaa/idpmapping/InvalidTypeException.java new file mode 100644 index 00000000..fb8b132f --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-idp-mapping/src/main/java/org/opendaylight/aaa/idpmapping/InvalidTypeException.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2014 Red Hat, Inc. 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 <jdennis@redhat.com> + */ + +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/odl-aaa-moon/aaa/aaa-idp-mapping/src/main/java/org/opendaylight/aaa/idpmapping/InvalidValueException.java b/odl-aaa-moon/aaa/aaa-idp-mapping/src/main/java/org/opendaylight/aaa/idpmapping/InvalidValueException.java new file mode 100644 index 00000000..2f83c13f --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-idp-mapping/src/main/java/org/opendaylight/aaa/idpmapping/InvalidValueException.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2014 Red Hat, Inc. 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 <jdennis@redhat.com> + */ + +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/odl-aaa-moon/aaa/aaa-idp-mapping/src/main/java/org/opendaylight/aaa/idpmapping/RuleProcessor.java b/odl-aaa-moon/aaa/aaa-idp-mapping/src/main/java/org/opendaylight/aaa/idpmapping/RuleProcessor.java new file mode 100644 index 00000000..0f86fde6 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-idp-mapping/src/main/java/org/opendaylight/aaa/idpmapping/RuleProcessor.java @@ -0,0 +1,1368 @@ +/* + * Copyright (c) 2014 Red Hat, Inc. 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.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +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; + +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 <jdennis@redhat.com> + */ + +public class RuleProcessor { + private static final Logger LOG = LoggerFactory.getLogger(RuleProcessor.class); + + public String ruleIdFormat = "<rule [${rule_number}:\"${rule_name}\"]>"; + public String statementIdFormat = "<rule [${rule_number}:\"${rule_name}\"] block [${block_number}:\"${block_name}\"] statement ${statement_number}>"; + + /* + * 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<Map<String, Object>> rules = null; + boolean success = true; + Map<String, Map<String, Object>> mappings = null; + + public RuleProcessor(java.io.Reader rulesIn, Map<String, Map<String, Object>> mappings) { + this.mappings = mappings; + IdpJson json = new IdpJson(); + @SuppressWarnings("unchecked") + List<Map<String, Object>> loadJson = (List<Map<String, Object>>) json.loadJson(rulesIn); + rules = loadJson; + } + + public RuleProcessor(Path rulesIn, Map<String, Map<String, Object>> mappings) + throws IOException { + this.mappings = mappings; + IdpJson json = new IdpJson(); + @SuppressWarnings("unchecked") + List<Map<String, Object>> loadJson = (List<Map<String, Object>>) json.loadJson(rulesIn); + rules = loadJson; + } + + public RuleProcessor(String rulesIn, Map<String, Map<String, Object>> mappings) { + this.mappings = mappings; + IdpJson json = new IdpJson(); + @SuppressWarnings("unchecked") + List<Map<String, Object>> loadJson = (List<Map<String, Object>>) 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<String, String> regexpGroupMap(String pattern, Matcher matcher) { + Map<String, String> groupMap = new HashMap<String, String>(); + 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<Object> 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<String> regexpGroupList(Matcher matcher) { + List<String> groupList = new ArrayList<String>(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<Object> list = (List<Object>) 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<String, Object> map = (Map<String, Object>) obj; + boolean first = true; + + sw.write('{'); + for (Map.Entry<String, Object> 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<Object> new_list = new ArrayList<Object>(); + @SuppressWarnings("unchecked") + List<Object> list = (List<Object>) obj; + for (Object item : list) { + new_list.add(deepCopy(item)); + } + return new_list; + } else if (obj instanceof Map) { + Map<String, Object> new_map = new LinkedHashMap<String, Object>(); + @SuppressWarnings("unchecked") + Map<String, Object> map = (Map<String, Object>) obj; + for (Map.Entry<String, Object> 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<String, Object> namespace) { + return substituteVariables(ruleIdFormat, namespace); + } + + public String statementId(Map<String, Object> namespace) { + return substituteVariables(statementIdFormat, namespace); + } + + public String substituteVariables(String string, Map<String, Object> 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<String, Object> getMapping(Map<String, Object> namespace, Map<String, Object> rule) { + Map<String, Object> mapping = null; + String mappingName = null; + + try { + @SuppressWarnings("unchecked") + Map<String, Object> map = (Map<String, Object>) 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), e)); + } + 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), e)); + } + 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))); + } + LOG.debug(String.format("using named mapping '%s' from rule %s mapping=%s", mappingName, + this.ruleId(namespace), mapping)); + return mapping; + } + + private String getVerb(List<Object> 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<Object> statement, int index, + Map<String, Object> namespace, Set<TokenStorageType> storageTypes, + Set<TokenType> 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(), e)); + } + + 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<Object> statement, int index, + Map<String, Object> namespace, Set<TokenType> 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(), e)); + } + + 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<Object> statement, int index, + Set<TokenType> 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(), e)); + } + + 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<Object> statement, int index, + Map<String, Object> 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(), e)); + } + + 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<String, Object> process(String assertionJson) { + ProcessResult result; + IdpJson json = new IdpJson(); + @SuppressWarnings("unchecked") + Map<String, Object> assertion = (Map<String, Object>) json.loadJson(assertionJson); + LOG.info("Assertion JSON: {}", json.dumpJson(assertion)); + this.success = true; + + for (int ruleNumber = 0; ruleNumber < this.rules.size(); ruleNumber++) { + Map<String, Object> namespace = new HashMap<String, Object>(); + Map<String, Object> rule = (Map<String, Object>) this.rules.get(ruleNumber); + namespace.put(RULE_NUMBER, Long.valueOf(ruleNumber)); + namespace.put(RULE_NAME, ""); + namespace.put(ASSERTION, deepCopy(assertion)); + + result = processRule(namespace, rule); + + if (result == ProcessResult.RULE_SUCCESS) { + Map<String, Object> mapped = new LinkedHashMap<String, Object>(); + Map<String, Object> mapping = getMapping(namespace, rule); + for (Map.Entry<String, Object> entry : ((Map<String, Object>) 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<String, Object> namespace, Map<String, Object> rule) { + ProcessResult result = ProcessResult.BLOCK_CONTINUE; + @SuppressWarnings("unchecked") + List<List<List<Object>>> statementBlocks = (List<List<List<Object>>>) rule.get("statement_blocks"); + if (statementBlocks == null) { + throw new InvalidRuleException("rule missing 'statement_blocks'"); + + } + for (int blockNumber = 0; blockNumber < statementBlocks.size(); blockNumber++) { + List<List<Object>> block = (List<List<Object>>) statementBlocks.get(blockNumber); + namespace.put(BLOCK_NUMBER, Long.valueOf(blockNumber)); + namespace.put(BLOCK_NAME, ""); + + result = processBlock(namespace, block); + 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<String, Object> namespace, List<List<Object>> block) { + ProcessResult result = ProcessResult.STATEMENT_CONTINUE; + + for (int statementNumber = 0; statementNumber < block.size(); statementNumber++) { + List<Object> statement = (List<Object>) block.get(statementNumber); + namespace.put(STATEMENT_NUMBER, Long.valueOf(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<String, Object> namespace, List<Object> 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<String, Object> namespace, List<Object> statement) { + Token variable = getVariable(verb, statement, 1, namespace); + Token parameter = getParameter(verb, statement, 2, namespace, null); + + variable.set(parameter.getObjectValue()); + this.success = true; + + if (LOG.isDebugEnabled()) { + LOG.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<String, Object> namespace, + List<Object> 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(length); + this.success = true; + + if (LOG.isDebugEnabled()) { + LOG.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<String, Object> namespace, + List<Object> 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 (LOG.isDebugEnabled()) { + LOG.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<String, Object> namespace, + List<Object> 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<Object> 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 (LOG.isDebugEnabled()) { + LOG.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<String, Object> namespace, + List<Object> statement) { + Token variable = getVariable(verb, statement, 1, namespace); + Token array = getParameter(verb, statement, 2, namespace, EnumSet.of(TokenType.ARRAY)); + + List<Object> newValue = new ArrayList<Object>(); + Set<Object> seen = new HashSet<Object>(); + + for (Object member : array.getListValue()) { + if (seen.contains(member)) { + continue; + } else { + newValue.add(member); + seen.add(member); + } + } + + variable.set(newValue); + this.success = true; + + if (LOG.isDebugEnabled()) { + LOG.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<String, Object> namespace, + List<Object> 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<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)); + } + try { + newValue = new ArrayList<String>( + 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 (LOG.isDebugEnabled()) { + LOG.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<String, Object> namespace, + List<Object> 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 (LOG.isDebugEnabled()) { + LOG.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<String, Object> namespace, + List<Object> 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<Object> oldValue = parameter.getListValue(); + List<Object> newValue = new ArrayList<Object>(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(), e)); + } + newItem = oldItem.toLowerCase(); + newValue.add(newItem); + } + variable.set(newValue); + } + break; + case MAP: { + Map<String, Object> oldValue = parameter.getMapValue(); + Map<String, Object> newValue = new LinkedHashMap<String, Object>(oldValue.size()); + + for (Map.Entry<String, Object> 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 (LOG.isDebugEnabled()) { + LOG.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<String, Object> namespace, + List<Object> 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<Object> oldValue = parameter.getListValue(); + List<Object> newValue = new ArrayList<Object>(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(), e)); + } + newItem = oldItem.toUpperCase(); + newValue.add(newItem); + } + variable.set(newValue); + } + break; + case MAP: { + Map<String, Object> oldValue = parameter.getMapValue(); + Map<String, Object> newValue = new LinkedHashMap<String, Object>(oldValue.size()); + + for (Map.Entry<String, Object> 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 (LOG.isDebugEnabled()) { + LOG.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<String, Object> namespace, List<Object> 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 (LOG.isDebugEnabled()) { + LOG.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<String, Object> namespace, + List<Object> 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 (LOG.isDebugEnabled()) { + LOG.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<String, Object> namespace, + List<Object> 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<Object> leftValue = left.getListValue(); + List<Object> rightValue = right.getListValue(); + result = leftValue.equals(rightValue); + } + break; + case MAP: { + Map<String, Object> leftValue = left.getMapValue(); + Map<String, Object> 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 (LOG.isDebugEnabled()) { + LOG.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<String, Object> namespace, + List<Object> 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<Object>()); + namespace.put(REGEXP_MAP_VARIABLE, new HashMap<String, Object>()); + } + + if (LOG.isDebugEnabled()) { + LOG.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<String, Object> namespace, + List<Object> 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 (LOG.isDebugEnabled()) { + LOG.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<String, Object> namespace, + List<Object> 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 (LOG.isDebugEnabled()) { + LOG.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<String, Object> namespace, + List<Object> 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 (LOG.isDebugEnabled()) { + LOG.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/odl-aaa-moon/aaa/aaa-idp-mapping/src/main/java/org/opendaylight/aaa/idpmapping/StatementErrorException.java b/odl-aaa-moon/aaa/aaa-idp-mapping/src/main/java/org/opendaylight/aaa/idpmapping/StatementErrorException.java new file mode 100644 index 00000000..6abab3ee --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-idp-mapping/src/main/java/org/opendaylight/aaa/idpmapping/StatementErrorException.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2014 Red Hat, Inc. 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 <jdennis@redhat.com> + */ + +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/odl-aaa-moon/aaa/aaa-idp-mapping/src/main/java/org/opendaylight/aaa/idpmapping/Token.java b/odl-aaa-moon/aaa/aaa-idp-mapping/src/main/java/org/opendaylight/aaa/idpmapping/Token.java new file mode 100644 index 00000000..402fb064 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-idp-mapping/src/main/java/org/opendaylight/aaa/idpmapping/Token.java @@ -0,0 +1,401 @@ +/* + * Copyright (c) 2014 Red Hat, Inc. 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 <jdennis@redhat.com> + */ + +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 = "(?<!\\\\)\\$" + // non-escaped $ + // sign + "\\{?" + // optional delimiting brace + "([a-zA-Z][a-zA-Z0-9_]*)" + // group 1: variable name + "(\\[" + // group 2: optional index + "([a-zA-Z0-9_]+)" + // group 3: array index + "\\])?" + // end optional index + "\\}?"; // optional delimiting brace + public static final Pattern VARIABLE_RE = Pattern.compile(VARIABLE_PAT); + /* + * Requires only a variable to be present in the string but permits leading + * and trailing whitespace. + */ + private static final String VARIABLE_ONLY_PAT = "^\\s*" + VARIABLE_PAT + "\\s*$"; + public static final Pattern VARIABLE_ONLY_RE = Pattern.compile(VARIABLE_ONLY_PAT); + + private Object value = null; + + public Map<String, Object> namespace = null; + public TokenStorageType storageType = TokenStorageType.UNKNOWN; + public TokenType type = TokenType.UNKNOWN; + public String name = null; + public String index = null; + + Token(Object input, Map<String, Object> 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<Object> list = (List<Object>) 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, e)); + } + } 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, e)); + } + } else if (base instanceof Map) { + @SuppressWarnings("unchecked") + Map<String, Object> map = (Map<String, Object>) 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<Object> list = (List<Object>) 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, e)); + } + } 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, e)); + } + } else if (base instanceof Map) { + @SuppressWarnings("unchecked") + Map<String, Object> map = (Map<String, Object>) 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<Object> getListValue() { + if (this.type == TokenType.ARRAY) { + @SuppressWarnings("unchecked") + List<Object> list = (List<Object>) this.value; + return list; + } else { + throw new InvalidTypeException(String.format("expected %s value but token type is %s", + TokenType.ARRAY, this.type)); + } + } + + public Map<String, Object> getMapValue() { + if (this.type == TokenType.MAP) { + @SuppressWarnings("unchecked") + Map<String, Object> map = (Map<String, Object>) 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/odl-aaa-moon/aaa/aaa-idp-mapping/src/main/java/org/opendaylight/aaa/idpmapping/UndefinedValueException.java b/odl-aaa-moon/aaa/aaa-idp-mapping/src/main/java/org/opendaylight/aaa/idpmapping/UndefinedValueException.java new file mode 100644 index 00000000..7200da3d --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-idp-mapping/src/main/java/org/opendaylight/aaa/idpmapping/UndefinedValueException.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2014 Red Hat, Inc. 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 <jdennis@redhat.com> + */ + +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/odl-aaa-moon/aaa/aaa-idp-mapping/src/test/java/org/opendaylight/aaa/idpmapping/RuleProcessorTest.java b/odl-aaa-moon/aaa/aaa-idp-mapping/src/test/java/org/opendaylight/aaa/idpmapping/RuleProcessorTest.java new file mode 100644 index 00000000..84d403f9 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-idp-mapping/src/test/java/org/opendaylight/aaa/idpmapping/RuleProcessorTest.java @@ -0,0 +1,130 @@ +/* + * Copyright (c) 2016 Red Hat, Inc. 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 static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.powermock.api.mockito.PowerMockito; +import org.powermock.api.support.membermodification.MemberMatcher; +import org.powermock.api.support.membermodification.MemberModifier; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.PowerMockRunner; +import org.powermock.reflect.Whitebox; + +@PrepareForTest(RuleProcessor.class) +@RunWith(PowerMockRunner.class) +public class RuleProcessorTest { + + @Mock + private RuleProcessor ruleProcess; + + @Before + public void setUp() { + ruleProcess = PowerMockito.mock(RuleProcessor.class, Mockito.CALLS_REAL_METHODS); + } + + @Test + public void testJoin() { + List<Object> list = new ArrayList<Object>(); + list.add("str1"); + list.add("str2"); + list.add("str3"); + assertEquals("str1/str2/str3", RuleProcessor.join(list, "/")); + } + + @Test + public void testSubstituteVariables() { + Map<String, Object> namespace = new HashMap<String, Object>() { + { + put("foo1", new HashMap<String, String>() { + { + put("0", "1"); + } + }); + } + }; + String str = "foo1[0]"; + String subVariable = ruleProcess.substituteVariables(str, namespace); + assertNotNull(subVariable); + assertEquals(subVariable, str); + } + + @Test + public void testGetMapping() { + Map<String, Object> namespace = new HashMap<String, Object>() { + { + put("foo1", new HashMap<String, String>() { + { + put("0", "1"); + } + }); + } + }; + final Map<String, Object> item = new HashMap<String, Object>() { + { + put("str", "val"); + } + }; + Map<String, Object> rules = new HashMap<String, Object>() { + { + put("mapping", item); + put("mapping_name", "mapping"); + } + }; + Map<String, Object> mapping = ruleProcess.getMapping(namespace, rules); + assertNotNull(mapping); + assertTrue(mapping.containsKey("str")); + assertEquals("val", mapping.get("str")); + } + + @Test + public void testProcess() throws Exception { + String json = " {\"rules\":[" + "{\"Name\":\"user\", \"Id\":1}," + + "{\"Name\":\"Admin\", \"Id\":2}]} "; + Map<String, Object> mapping = new HashMap<String, Object>() { + { + put("Name", "Admin"); + } + }; + List<Map<String, Object>> internalRules = new ArrayList<Map<String, Object>>(); + Map<String, Object> internalRule = new HashMap<String, Object>() { + { + put("Name", "Admin"); + put("statement_blocks", "user"); + } + }; + internalRules.add(internalRule); + MemberModifier.field(RuleProcessor.class, "rules").set(ruleProcess, internalRules); + PowerMockito.suppress(MemberMatcher.method(RuleProcessor.class, "processRule", Map.class, + Map.class)); + PowerMockito.when(ruleProcess, "processRule", any(Map.class), any(Map.class)).thenReturn( + ProcessResult.RULE_SUCCESS); + PowerMockito.suppress(MemberMatcher.method(RuleProcessor.class, "getMapping", Map.class, + Map.class)); + when(ruleProcess.getMapping(any(Map.class), any(Map.class))).thenReturn(mapping); + Whitebox.invokeMethod(ruleProcess, "process", json); + verify(ruleProcess, times(3)).getMapping(any(Map.class), any(Map.class)); + } + +} diff --git a/odl-aaa-moon/aaa/aaa-idp-mapping/src/test/java/org/opendaylight/aaa/idpmapping/TokenTest.java b/odl-aaa-moon/aaa/aaa-idp-mapping/src/test/java/org/opendaylight/aaa/idpmapping/TokenTest.java new file mode 100644 index 00000000..d6181051 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-idp-mapping/src/test/java/org/opendaylight/aaa/idpmapping/TokenTest.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2016 Red Hat, Inc. 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 static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; +import org.junit.Test; + +public class TokenTest { + + private final Map<String, Object> namespace = new HashMap<String, Object>() { + { + put("foo1", new HashMap<String, String>() { + { + put("0", "1"); + } + }); + } + }; + private Object input = "$foo1[0]"; + private Token token = new Token(input, namespace); + private Token mapToken = new Token(namespace, namespace); + + @Test + public void testToken() { + assertEquals(token.toString(), input); + assertTrue(token.storageType == TokenStorageType.VARIABLE); + assertEquals(mapToken.toString(), "{foo1={0=1}}"); + assertTrue(mapToken.storageType == TokenStorageType.CONSTANT); + } + + @Test + public void testClassify() { + assertEquals(Token.classify(new ArrayList<>()), TokenType.ARRAY); + assertEquals(Token.classify(true), TokenType.BOOLEAN); + assertEquals(Token.classify(new Long(365)), TokenType.INTEGER); + assertEquals(Token.classify(new HashMap<String, Object>()), TokenType.MAP); + assertEquals(Token.classify(null), TokenType.NULL); + assertEquals(Token.classify(365.00), TokenType.REAL); + assertEquals(Token.classify("foo_str"), TokenType.STRING); + } + + @Test + public void testGet() { + assertNotNull(token.get()); + assertTrue(token.get("0") == "1"); + assertNotNull(mapToken.get()); + assertTrue(mapToken.get(0) == namespace); + } + + @Test + public void testGetMapValue() { + assertTrue(mapToken.getMapValue() == namespace); + } +} diff --git a/odl-aaa-moon/aaa/aaa-shiro-act/pom.xml b/odl-aaa-moon/aaa/aaa-shiro-act/pom.xml new file mode 100644 index 00000000..fade2aea --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-shiro-act/pom.xml @@ -0,0 +1,84 @@ +<!-- Copyright (c) 2015 Brocade Communications Systems, Inc. and others. + 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 --> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + <parent> + <groupId>org.opendaylight.aaa</groupId> + <artifactId>aaa-parent</artifactId> + <version>0.3.2-Beryllium-SR2</version> + <relativePath>../parent</relativePath> + </parent> + + <artifactId>aaa-shiro-act</artifactId> + <packaging>bundle</packaging> + + <dependencies> + <dependency> + <groupId>org.opendaylight.aaa</groupId> + <artifactId>aaa-shiro</artifactId> + </dependency> + <dependency> + <groupId>org.apache.felix</groupId> + <artifactId>org.apache.felix.dependencymanager</artifactId> + </dependency> + + <dependency> + <groupId>org.slf4j</groupId> + <artifactId>slf4j-api</artifactId> + </dependency> + <dependency> + <groupId>commons-beanutils</groupId> + <artifactId>commons-beanutils</artifactId> + <version>1.8.3</version> + </dependency> + + <!-- Testing Dependencies --> + <dependency> + <groupId>junit</groupId> + <artifactId>junit</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.mockito</groupId> + <artifactId>mockito-all</artifactId> + <scope>test</scope> + </dependency> + </dependencies> + <build> + <pluginManagement> + <plugins> + <plugin> + <groupId>org.apache.felix</groupId> + <artifactId>maven-bundle-plugin</artifactId> + <version>${bundle.plugin.version}</version> + <extensions>true</extensions> + <configuration> + <instructions> + <Bundle-Name>${project.groupId}.${project.artifactId}</Bundle-Name> + </instructions> + <manifestLocation>${project.basedir}/META-INF</manifestLocation> + </configuration> + </plugin> + </plugins> + </pluginManagement> + <plugins> + <plugin> + <groupId>org.apache.felix</groupId> + <artifactId>maven-bundle-plugin</artifactId> + <extensions>true</extensions> + <configuration> + <instructions> + <Bundle-Activator>org.opendaylight.aaa.shiroact.Activator</Bundle-Activator> + </instructions> + </configuration> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-jar-plugin</artifactId> + </plugin> + </plugins> + </build> +</project> diff --git a/odl-aaa-moon/aaa/aaa-shiro-act/src/main/java/org/opendaylight/aaa/shiroact/Activator.java b/odl-aaa-moon/aaa/aaa-shiro-act/src/main/java/org/opendaylight/aaa/shiroact/Activator.java new file mode 100644 index 00000000..0012a0bd --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-shiro-act/src/main/java/org/opendaylight/aaa/shiroact/Activator.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2015 Brocade Communications Systems, Inc. and others. 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.shiroact; + +import org.apache.felix.dm.DependencyActivatorBase; +import org.apache.felix.dm.DependencyManager; +import org.opendaylight.aaa.shiro.ServiceProxy; +import org.osgi.framework.BundleContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Responsible for activating the aaa-shiro-act bundle. This bundle is primarily + * responsible for enabling AuthN and AuthZ. If this bundle is not installed, + * then AuthN and AuthZ will not take effect. + * + * To ensure that the AAA is enabled for your feature, make sure to include the + * <code>odl-aaa-shiro</code> feature in your feature definition. + * + * Offers contextual <code>DEBUG</code> level clues concerning the activation of + * the <code>aaa-shiro-act</code> bundle. To enable the enhanced debugging issue + * the following line in the karaf shell: + * <code>log:set debug org.opendaylight.aaa.shiroact.Activator</code> + * + * @author Ryan Goulding (ryandgoulding@gmail.com) + */ +public class Activator extends DependencyActivatorBase { + + private static final Logger LOG = LoggerFactory.getLogger(Activator.class); + + @Override + public void destroy(BundleContext bc, DependencyManager dm) + throws Exception { + final String DEBUG_MESSAGE = "Destroying the aaa-shiro-act bundle"; + LOG.debug(DEBUG_MESSAGE); + } + + @Override + public void init(BundleContext bc, DependencyManager dm) throws Exception { + final String DEBUG_MESSAGE = "Initializing the aaa-shiro-act bundle"; + LOG.debug(DEBUG_MESSAGE); + ServiceProxy.getInstance().setEnabled(true); + } + +} diff --git a/odl-aaa-moon/aaa/aaa-shiro-act/src/test/java/org/opendaylight/aaa/shiroact/ActivatorTest.java b/odl-aaa-moon/aaa/aaa-shiro-act/src/test/java/org/opendaylight/aaa/shiroact/ActivatorTest.java new file mode 100644 index 00000000..23eef9db --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-shiro-act/src/test/java/org/opendaylight/aaa/shiroact/ActivatorTest.java @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2016 Brocade Communications Systems, Inc. and others. 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.shiroact; + +import static org.junit.Assert.*; + +import org.junit.Test; +import org.opendaylight.aaa.shiro.ServiceProxy; + +public class ActivatorTest { + + @Test + public void testActivatorEnablesServiceProxy() throws Exception { + // should toggle the ServiceProxy enable status to true + new Activator().init(null, null);; + assertTrue(ServiceProxy.getInstance().getEnabled(null)); + } + +} diff --git a/odl-aaa-moon/aaa/aaa-shiro/pom.xml b/odl-aaa-moon/aaa/aaa-shiro/pom.xml new file mode 100644 index 00000000..ea551532 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-shiro/pom.xml @@ -0,0 +1,169 @@ +<!-- Copyright (c) 2015 Brocade Communications Systems, Inc. and others. + 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 --> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + <parent> + <groupId>org.opendaylight.aaa</groupId> + <artifactId>aaa-parent</artifactId> + <version>0.3.2-Beryllium-SR2</version> + <relativePath>../parent</relativePath> + </parent> + + <artifactId>aaa-shiro</artifactId> + <packaging>bundle</packaging> + + <dependencies> + <!-- jersey client for moon authN --> + <dependency> + <groupId>com.sun.jersey</groupId> + <artifactId>jersey-client</artifactId> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>org.json</groupId> + <artifactId>json</artifactId> + <version>20140107</version> + </dependency> + <!-- OAuth2 dependencies for moon --> + <dependency> + <groupId>org.apache.oltu.oauth2</groupId> + <artifactId>org.apache.oltu.oauth2.authzserver</artifactId> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>org.apache.oltu.oauth2</groupId> + <artifactId>org.apache.oltu.oauth2.common</artifactId> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>org.apache.oltu.oauth2</groupId> + <artifactId>org.apache.oltu.oauth2.resourceserver</artifactId> + <scope>provided</scope> + </dependency> + <!-- end --> + <dependency> + <groupId>org.apache.felix</groupId> + <artifactId>org.apache.felix.dependencymanager</artifactId> + </dependency> + <dependency> + <groupId>org.opendaylight.aaa</groupId> + <artifactId>aaa-authn-sts</artifactId> + </dependency> + <dependency> + <groupId>org.opendaylight.aaa</groupId> + <artifactId>aaa-authn-basic</artifactId> + </dependency> + <dependency> + <groupId>org.apache.shiro</groupId> + <artifactId>shiro-core</artifactId> + </dependency> + <dependency> + <groupId>org.apache.shiro</groupId> + <artifactId>shiro-web</artifactId> + </dependency> + <dependency> + <groupId>org.slf4j</groupId> + <artifactId>slf4j-api</artifactId> + </dependency> + <dependency> + <groupId>commons-beanutils</groupId> + <artifactId>commons-beanutils</artifactId> + <version>1.8.3</version> + </dependency> + <dependency> + <groupId>javax.servlet</groupId> + <artifactId>javax.servlet-api</artifactId> + </dependency> + <dependency> + <groupId>com.google.guava</groupId> + <artifactId>guava</artifactId> + </dependency> + + <!-- Testing Dependencies --> + <dependency> + <groupId>junit</groupId> + <artifactId>junit</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.mockito</groupId> + <artifactId>mockito-all</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>ch.qos.logback</groupId> + <artifactId>logback-core</artifactId> + <version>1.1.6</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>ch.qos.logback</groupId> + <artifactId>logback-classic</artifactId> + <version>1.1.6</version> + <scope>test</scope> + </dependency> + </dependencies> + <build> + <pluginManagement> + <plugins> + <plugin> + <groupId>org.apache.felix</groupId> + <artifactId>maven-bundle-plugin</artifactId> + <version>${bundle.plugin.version}</version> + <extensions>true</extensions> + <configuration> + <instructions> + <Bundle-Name>${project.groupId}.${project.artifactId}</Bundle-Name> + </instructions> + <manifestLocation>${project.basedir}/META-INF</manifestLocation> + </configuration> + </plugin> + </plugins> + </pluginManagement> + <plugins> + <plugin> + <groupId>org.apache.felix</groupId> + <artifactId>maven-bundle-plugin</artifactId> + <extensions>true</extensions> + <configuration> + <instructions> + <Import-Package> + * + </Import-Package> + <Web-ContextPath>/moon</Web-ContextPath> + <Bundle-Activator>org.opendaylight.aaa.shiro.Activator</Bundle-Activator> + </instructions> + </configuration> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-jar-plugin</artifactId> + </plugin> + <plugin> + <groupId>org.codehaus.mojo</groupId> + <artifactId>build-helper-maven-plugin</artifactId> + <executions> + <execution> + <id>attach-artifacts</id> + <phase>package</phase> + <goals> + <goal>attach-artifact</goal> + </goals> + <configuration> + <artifacts> + <artifact> + <file>${project.build.directory}/classes/shiro.ini</file> + <type>cfg</type> + <classifier>configuration</classifier> + </artifact> + </artifacts> + </configuration> + </execution> + </executions> + </plugin> + </plugins> + </build> +</project> diff --git a/odl-aaa-moon/aaa/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/Activator.java b/odl-aaa-moon/aaa/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/Activator.java new file mode 100644 index 00000000..2f1c98f7 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/Activator.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2015 Brocade Communications Systems, Inc. and others. 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.shiro; + +import org.apache.felix.dm.DependencyActivatorBase; +import org.apache.felix.dm.DependencyManager; +import org.osgi.framework.BundleContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This scaffolding allows the use of AAA Filters without AuthN or AuthZ + * enabled. This is done to support workflows such as those included in the + * <code>odl-restconf-noauth</code> feature. + * + * This class is also responsible for offering contextual <code>DEBUG</code> + * level clues concerning the activation of the <code>aaa-shiro</code> bundle. + * To enable these debug messages, issue the following command in the karaf + * shell: <code>log:set debug org.opendaylight.aaa.shiro.Activator</code> + * + * @author Ryan Goulding (ryandgoulding@gmail.com) + */ +public class Activator extends DependencyActivatorBase { + + private static final Logger LOG = LoggerFactory.getLogger(Activator.class); + + @Override + public void destroy(BundleContext bc, DependencyManager dm) throws Exception { + final String DEBUG_MESSAGE = "Destroying the aaa-shiro bundle"; + LOG.debug(DEBUG_MESSAGE); + } + + @Override + public void init(BundleContext bc, DependencyManager dm) throws Exception { + final String DEBUG_MESSAGE = "Initializing the aaa-shiro bundle"; + LOG.debug(DEBUG_MESSAGE); + } + +} diff --git a/odl-aaa-moon/aaa/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/ServiceProxy.java b/odl-aaa-moon/aaa/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/ServiceProxy.java new file mode 100644 index 00000000..e4485d73 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/ServiceProxy.java @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2016 Brocade Communications Systems, Inc. and others. 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.shiro; + +import org.opendaylight.aaa.shiro.filters.AAAFilter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Responsible for enabling and disabling the AAA service. By default, the + * service is disabled; the AAAFilter will not require AuthN or AuthZ. The + * service is enabled through calling + * <code>ServiceProxy.getInstance().setEnabled(true)</code>. AuthN and AuthZ are + * disabled by default in order to support workflows such as the feature + * <code>odl-restconf-noauth</code>. + * + * The AAA service is enabled through installing the <code>odl-aaa-shiro</code> + * feature. The <code>org.opendaylight.aaa.shiroact.Activator()</code> + * constructor calls enables AAA through the ServiceProxy, which in turn enables + * the AAAFilter. + * + * ServiceProxy is a singleton; access to the ServiceProxy is granted through + * the <code>getInstance()</code> function. + * + * @author Ryan Goulding (ryandgoulding@gmail.com) + * @see <a + * href="https://github.com/opendaylight/netconf/blob/master/opendaylight/restconf/sal-rest-connector/src/main/resources/WEB-INF/web.xml">resconf + * web,xml</a> + * @see <code>org.opendaylight.aaa.shiro.Activator</code> + * @see <code>org.opendaylight.aaa.shiro.filters.AAAFilter</code> + */ +public class ServiceProxy { + private static final Logger LOG = LoggerFactory.getLogger(ServiceProxy.class); + + /** + * AuthN and AuthZ are disabled by default to support workflows included in + * features such as <code>odl-restconf-noauth</code> + */ + public static final boolean DEFAULT_AA_ENABLE_STATUS = false; + + private static ServiceProxy instance = new ServiceProxy(); + private volatile boolean enabled = false; + private AAAFilter filter; + + /** + * private for singleton pattern + */ + private ServiceProxy() { + final String INFO_MESSAGE = "Creating the ServiceProxy"; + LOG.info(INFO_MESSAGE); + } + + /** + * @return ServiceProxy, a feature level singleton + */ + public static ServiceProxy getInstance() { + return instance; + } + + /** + * Enables/disables the feature, cascading the state information to the + * AAAFilter. + * + * @param enabled A flag indicating whether to enable the Service. + */ + public synchronized void setEnabled(final boolean enabled) { + this.enabled = enabled; + final String SERVICE_ENABLED_INFO_MESSAGE = "Setting ServiceProxy enabled to " + enabled; + LOG.info(SERVICE_ENABLED_INFO_MESSAGE); + // check for null because of non-determinism in bundle load + if (filter != null) { + filter.setEnabled(enabled); + } + } + + /** + * Extract whether the service is enabled. + * + * @param filter + * register an optional Filter for callback if enable state + * changes + * @return Whether the service is enabled + */ + public synchronized boolean getEnabled(final AAAFilter filter) { + this.filter = filter; + return enabled; + } +} diff --git a/odl-aaa-moon/aaa/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/accounting/Accounter.java b/odl-aaa-moon/aaa/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/accounting/Accounter.java new file mode 100644 index 00000000..e768ea59 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/accounting/Accounter.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2015 Brocade Communications Systems, Inc. and others. 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.shiro.accounting; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Accounter is a common place to output AAA messages. Use this class through + * invoking <code>Logger.output("message")</code>. + * + * @author Ryan Goulding (ryandgoulding@gmail.com) + */ +public class Accounter { + + private static final Logger LOG = LoggerFactory.getLogger(Accounter.class); + + /* + * Essentially makes Accounter a singleton, avoiding the verbosity of + * <code>Accounter.getInstance().output("message")</code>. + */ + private Accounter() { + } + + /** + * Account for a particular <code>message</code> + * + * @param message A message for the aggregated AAA log. + */ + public static void output(final String message) { + LOG.debug(message); + } +} diff --git a/odl-aaa-moon/aaa/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/authorization/DefaultRBACRules.java b/odl-aaa-moon/aaa/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/authorization/DefaultRBACRules.java new file mode 100644 index 00000000..9e84c988 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/authorization/DefaultRBACRules.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2015 Brocade Communications Systems, Inc. and others. 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.shiro.authorization; + +import com.google.common.collect.Sets; +import java.util.Collection; +import java.util.HashSet; + +/** + * A singleton container of default authorization rules that are installed as + * part of Shiro initialization. This class defines an immutable set of rules + * that are needed to provide system-wide security. These include protecting + * certain MD-SAL leaf nodes that contain AAA data from random access. This is + * not a place to define your custom rule set; additional RBAC rules are + * configured through the shiro initialization file: + * <code>$KARAF_HOME/shiro.ini</code> + * + * An important distinction to consider is that Shiro URL rules work to protect + * the system at the Web layer, and <code>AuthzDomDataBroker</code> works to + * protect the system down further at the DOM layer. + * + * @author Ryan Goulding (ryandgoulding@gmail.com) + * + */ +public class DefaultRBACRules { + + private static DefaultRBACRules instance; + + /** + * a collection of the default security rules + */ + private Collection<RBACRule> rbacRules = new HashSet<RBACRule>(); + + /** + * protects the AAA MD-SAL store by preventing access to the leaf nodes to + * non-admin users. + */ + private static final RBACRule PROTECT_AAA_MDSAL = RBACRule.createAuthorizationRule( + "*/authorization/*", Sets.newHashSet("admin")); + + /* + * private for singleton pattern + */ + private DefaultRBACRules() { + // rbacRules.add(PROTECT_AAA_MDSAL); + } + + /** + * + * @return the container instance for the default RBAC Rules + */ + public static final DefaultRBACRules getInstance() { + if (null == instance) { + instance = new DefaultRBACRules(); + } + return instance; + } + + /** + * + * @return a copy of the default rules, so any modifications to the returned + * reference do not affect the <code>DefaultRBACRules</code>. + */ + public final Collection<RBACRule> getRBACRules() { + // Returns a copy of the rbacRules set such that the original set keeps + // its contract of remaining immutable. Calls to rbacRules.add() are + // encapsulated solely in <code>DefaultRBACRules</code>. + // + // Since this method is only called at shiro initialiation time, + // memory consumption of creating a new set is a non-issue. + return Sets.newHashSet(rbacRules); + } +} diff --git a/odl-aaa-moon/aaa/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/authorization/RBACRule.java b/odl-aaa-moon/aaa/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/authorization/RBACRule.java new file mode 100644 index 00000000..0da95eb4 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/authorization/RBACRule.java @@ -0,0 +1,170 @@ +/* + * Copyright (c) 2015 Brocade Communications Systems, Inc. and others. 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.shiro.authorization; + +import com.google.common.base.Preconditions; +import com.google.common.collect.Sets; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashSet; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A container for RBAC Rules. An RBAC Rule is composed of a url pattern which + * may contain asterisk characters (*), and a collection of roles. These are + * represented in shiro.ini in the following format: + * <code>urlPattern=roles[atLeastOneCommaSeperatedRole]</code> + * + * RBACRules are immutable; that is, you cannot change the url pattern or the + * roles after creation. This is done for security purposes. RBACRules are + * created through utilizing a static factory method: + * <code>RBACRule.createRBACRule()</code> + * + * @author Ryan Goulding (ryandgoulding@gmail.com) + * + */ +public class RBACRule { + + private static final Logger LOG = LoggerFactory.getLogger(RBACRule.class); + + /** + * a url pattern that can optional contain asterisk characters (*) + */ + private String urlPattern; + + /** + * a collection of role names, such as "admin" and "user" + */ + private Collection<String> roles = new HashSet<String>(); + + /** + * Creates an RBAC Rule. Made private for static factory method. + * + * @param urlPattern + * Cannot be null or the empty string. + * @param roles + * Must contain at least one role. + * @throws NullPointerException + * if <code>urlPattern</code> or <code>roles</code> is null + * @throws IllegalArgumentException + * if <code>urlPattern</code> is an empty string or + * <code>roles</code> is an empty collection. + */ + private RBACRule(final String urlPattern, final Collection<String> roles) + throws NullPointerException, IllegalArgumentException { + + this.setUrlPattern(urlPattern); + this.setRoles(roles); + } + + /** + * The static factory method used to create RBACRules. + * + * @param urlPattern + * Cannot be null or the empty string. + * @param roles + * Cannot be null or an emtpy collection. + * @return An immutable RBACRule + */ + public static RBACRule createAuthorizationRule(final String urlPattern, + final Collection<String> roles) { + + RBACRule authorizationRule = null; + try { + authorizationRule = new RBACRule(urlPattern, roles); + } catch (Exception e) { + LOG.error("Cannot instantiate the AuthorizationRule", e); + } + return authorizationRule; + } + + /** + * + * @return the urlPattern for the RBACRule + */ + public String getUrlPattern() { + return urlPattern; + } + + /* + * helper to ensure the url pattern is not the empty string + */ + private static void checkUrlPatternLength(final String urlPattern) + throws IllegalArgumentException { + + final String EXCEPTION_MESSAGE = "Empty String is not allowed for urlPattern"; + if (urlPattern.isEmpty()) { + throw new IllegalArgumentException(EXCEPTION_MESSAGE); + } + } + + private void setUrlPattern(final String urlPattern) throws NullPointerException, + IllegalArgumentException { + + Preconditions.checkNotNull(urlPattern); + checkUrlPatternLength(urlPattern); + this.urlPattern = urlPattern; + } + + /** + * + * @return a copy of the rule, so any modifications to the returned + * reference do not affect the immutable <code>RBACRule</code>. + */ + public Collection<String> getRoles() { + // Returns a copy of the roles collection such that the original set + // keeps + // its contract of remaining immutable. + // + // Since this method is only called at shiro initialiation time, + // memory consumption of creating a new set is a non-issue. + return Sets.newHashSet(roles); + } + + /* + * check to ensure the roles collection is not empty + */ + private static void checkRolesCollectionSize(final Collection<String> roles) + throws IllegalArgumentException { + + final String EXCEPTION_MESSAGE = "roles must contain at least 1 role"; + if (roles.isEmpty()) { + throw new IllegalArgumentException(EXCEPTION_MESSAGE); + } + } + + private void setRoles(final Collection<String> roles) throws NullPointerException, + IllegalArgumentException { + + Preconditions.checkNotNull(roles); + checkRolesCollectionSize(roles); + this.roles = roles; + } + + /** + * Generates a string representation of the <code>RBACRule</code> roles in + * shiro form. + * + * @return roles string representation in the form + * <code>roles[roleOne,roleTwo]</code> + */ + public String getRolesInShiroFormat() { + final String ROLES_STRING = "roles"; + return ROLES_STRING + Arrays.toString(roles.toArray()); + } + + /** + * Generates the string representation of the <code>RBACRule</code> in shiro + * form. For example: <code>urlPattern=roles[admin,user]</code> + */ + @Override + public String toString() { + return String.format("%s=%s", urlPattern, getRolesInShiroFormat()); + } +} diff --git a/odl-aaa-moon/aaa/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/filters/AAAFilter.java b/odl-aaa-moon/aaa/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/filters/AAAFilter.java new file mode 100644 index 00000000..47dd9549 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/filters/AAAFilter.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2015 Brocade Communications Systems, Inc. and others. 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.shiro.filters; + +import org.apache.shiro.web.servlet.ShiroFilter; +import org.opendaylight.aaa.shiro.ServiceProxy; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The RESTCONF AAA JAX-RS 1.X Web Filter. This class is also responsible for + * delivering debug information; to enable these debug statements, please issue + * the following in the karaf shell: + * + * <code>log:set debug org.opendaylight.aaa.shiro.filters.AAAFilter</code> + * + * @author Ryan Goulding (ryandgoulding@gmail.com) + * @see <code>javax.servlet.Filter</code> + * @see <code>org.apache.shiro.web.servlet.ShiroFilter</code> + */ +public class AAAFilter extends ShiroFilter { + + private static final Logger LOG = LoggerFactory.getLogger(AAAFilter.class); + + public AAAFilter() { + super(); + final String DEBUG_MESSAGE = "Creating the AAAFilter"; + LOG.debug(DEBUG_MESSAGE); + } + + /* + * (non-Javadoc) + * + * Adds context clues that aid in debugging. Also initializes the enable + * status to correspond with + * <code>ServiceProxy.getInstance.getEnabled()</code>. + * + * @see org.apache.shiro.web.servlet.ShiroFilter#init() + */ + @Override + public void init() throws Exception { + super.init(); + final String DEBUG_MESSAGE = "Initializing the AAAFilter"; + LOG.debug(DEBUG_MESSAGE); + // sets the filter to the startup value. Because of non-determinism in + // bundle loading, this passes an instance of itself along so that if + // the + // enable status changes, then AAAFilter enable status is changed. + setEnabled(ServiceProxy.getInstance().getEnabled(this)); + } + + /* + * (non-Javadoc) + * + * Adds context clues to aid in debugging whether the filter is enabled. + * + * @see + * org.apache.shiro.web.servlet.OncePerRequestFilter#setEnabled(boolean) + */ + @Override + public void setEnabled(boolean enabled) { + super.setEnabled(enabled); + final String DEBUG_MESSAGE = "Setting AAAFilter enabled to " + enabled; + LOG.debug(DEBUG_MESSAGE); + } +} diff --git a/odl-aaa-moon/aaa/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/filters/AAAShiroFilter.java b/odl-aaa-moon/aaa/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/filters/AAAShiroFilter.java new file mode 100644 index 00000000..530acfac --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/filters/AAAShiroFilter.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2016 Brocade Communications Systems, Inc. and others. 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.shiro.filters; + +import org.apache.shiro.web.servlet.ShiroFilter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The default AAA JAX-RS 1.X Web Filter. Unlike AAAFilter, which is aimed towards + * supporting RESTCONF and its existing API mechanisms, AAAShiroFilter is a generic + * <code>ShiroFilter</code> for use with any other ODL Servlets. The main difference + * is that <code>AAAFilter</code> was designed to support the existing noauth + * mechanism, while this filter cannot be disabled. + * + * This class is also responsible for delivering debug information; to enable these + * debug statements, please issue the following in the karaf shell: + * + * <code>log:set debug org.opendaylight.aaa.shiro.filters.AAAShiroFilter</code> + * + * @author Ryan Goulding (ryandgoulding@gmail.com) + * @see <code>javax.servlet.Filter</code> + * @see <code>org.apache.shiro.web.servlet.ShiroFilter</code> + */ +public class AAAShiroFilter extends ShiroFilter { + + private static final Logger LOG = LoggerFactory.getLogger(AAAShiroFilter.class); + + public AAAShiroFilter() { + LOG.debug("Creating the AAAShiroFilter"); + } + + /* + * (non-Javadoc) + * + * Adds context clues that aid in debugging. + * + * @see org.apache.shiro.web.servlet.ShiroFilter#init() + */ + @Override + public void init() throws Exception { + super.init(); + LOG.debug("Initializing the AAAShiroFilter"); + } +} diff --git a/odl-aaa-moon/aaa/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/filters/AuthenticationListener.java b/odl-aaa-moon/aaa/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/filters/AuthenticationListener.java new file mode 100644 index 00000000..080ab114 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/filters/AuthenticationListener.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2016 Brocade Communications Systems, Inc. and others. 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.shiro.filters; + +import org.apache.shiro.authc.AuthenticationException; +import org.apache.shiro.authc.AuthenticationInfo; +import org.apache.shiro.authc.AuthenticationToken; +import org.apache.shiro.subject.PrincipalCollection; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Follows the event-listener pattern; the <code>Authenticator</code> notifies this class about + * authentication attempts. <code>AuthenticationListener</code> logs successful and unsuccessful + * authentication attempts appropriately. Log messages are emitted at the <code>DEBUG</code> log + * level. To enable the messages out of the box, use the following command from karaf: + * <code>log:set DEBUG org.opendaylight.aaa.shiro.authc.AuthenicationListener</code> + * + * @author Ryan Goulding (ryandgoulding@gmail.com) + */ +public class AuthenticationListener implements org.apache.shiro.authc.AuthenticationListener { + + private static final Logger LOG = LoggerFactory.getLogger(AuthenticationListener.class); + + @Override + public void onSuccess(final AuthenticationToken authenticationToken, final AuthenticationInfo authenticationInfo) { + if (LOG.isDebugEnabled()) { + final String successMessage = AuthenticationTokenUtils.generateSuccessfulAuthenticationMessage(authenticationToken); + LOG.debug(successMessage); + } + } + + @Override + public void onFailure(final AuthenticationToken authenticationToken, final AuthenticationException e) { + if (LOG.isDebugEnabled()) { + final String failureMessage = AuthenticationTokenUtils.generateUnsuccessfulAuthenticationMessage(authenticationToken); + LOG.debug(failureMessage); + } + } + + @Override + public void onLogout(final PrincipalCollection principalCollection) { + // Do nothing; AAA is aimed at RESTCONF, which stateless by definition. + // Including this output would very quickly pollute the log. + } +} diff --git a/odl-aaa-moon/aaa/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/filters/AuthenticationTokenUtils.java b/odl-aaa-moon/aaa/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/filters/AuthenticationTokenUtils.java new file mode 100644 index 00000000..a5f0c10d --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/filters/AuthenticationTokenUtils.java @@ -0,0 +1,129 @@ +/* + * Copyright (c) 2016 Brocade Communications Systems, Inc. and others. 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.shiro.filters; + +import com.google.common.base.Preconditions; +import org.apache.shiro.authc.AuthenticationToken; +import org.apache.shiro.authc.UsernamePasswordToken; + +/** + * Utility methods for forming audit trail output based on an <code>AuthenticationToken</code>. + * + * @author Ryan Goulding (ryandgoulding@gmail.com) + */ +public class AuthenticationTokenUtils { + + /** + * default value used in messaging when the "user" field is unparsable from the HTTP REST request + */ + static final String DEFAULT_USERNAME = "an unknown user"; + + /** + * default value used in messaging when the "user" field is not present in the HTTP REST request, implying + * a different implementation of <code>AuthenticationToken</code> such as <code>CasToken</code>. + */ + static final String DEFAULT_TOKEN = "an un-parsable token type"; + + /** + * default value used in messaging when the "host" field cannot be determined. + */ + static final String DEFAULT_HOSTNAME = "an unknown host"; + + private AuthenticationTokenUtils() { + // private to prevent instantiation + } + + /** + * Determines whether the supplied <code>Token</code> is a <code>UsernamePasswordToken</code>. + * + * @param token A generic <code>Token</code>, which might be a <code>UsernamePasswordToken</code> + * @return Whether the supplied <code>Token</code> is a <code>UsernamePasswordToken</code> + */ + public static boolean isUsernamePasswordToken(final AuthenticationToken token) { + return token instanceof UsernamePasswordToken; + } + + /** + * Extracts the username if possible. If the supplied token is a <code>UsernamePasswordToken</code> + * and the username field is not set, <code>DEFAULT_USERNAME</code> is returned. If the supplied + * token is not a <code>UsernamePasswordToken</code> (i.e., a <code>CasToken</code> or other + * implementation of <code>AuthenticationToken</code>), then <code>DEFAULT_TOKEN</code> is + * returned. + * + * @param token An <code>AuthenticationToken</code>, possibly a <code>UsernamePasswordToken</code> + * @return the username, <code>DEFAULT_USERNAME</code> or <code>DEFAULT_TOKEN</code> depending on input + */ + public static String extractUsername(final AuthenticationToken token) { + if (isUsernamePasswordToken(token)) { + final UsernamePasswordToken upt = (UsernamePasswordToken) token; + return extractField(upt.getUsername(), DEFAULT_USERNAME); + } + return DEFAULT_TOKEN; + } + + /** + * Extracts the hostname if possible. If the supplied token is a <code>UsernamePasswordToken</code> + * and the hostname field is not set, <code>DEFAULT_HOSTNAME</code> is returned. If the supplied + * token is not a <code>UsernamePasswordToken</code> (i.e., a <code>CasToken</code> or other + * implementation of <code>AuthenticationToken</code>), then <code>DEFAULT_HOSTNAME</code> is + * returned. + * + * @param token An <code>AuthenticationToken</code>, possibly a <code>UsernamePasswordToken</code> + * @return the hostname, or <code>DEFAULT_USERNAME</code> depending on input + */ + public static String extractHostname(final AuthenticationToken token) { + if (isUsernamePasswordToken(token)) { + final UsernamePasswordToken upt = (UsernamePasswordToken) token; + return extractField(upt.getHost(), DEFAULT_HOSTNAME); + } + return DEFAULT_HOSTNAME; + } + + /** + * Utility method to generate a generic message indicating Authentication was unsuccessful. + * + * @param token An <code>AuthenticationToken</code>, possibly a <code>UsernamePasswordToken</code> + * @return A message indicating authentication was unsuccessful + */ + public static String generateUnsuccessfulAuthenticationMessage(final AuthenticationToken token) { + final String username = extractUsername(token); + final String remoteHostname = extractHostname(token); + return String.format("Unsuccessful authentication attempt by %s from %s", username, remoteHostname); + } + + /** + * Utility method to generate a generic message indicating Authentication was successful. + * + * @param token An <code>AuthenticationToken</code>, possibly a <code>UsernamePasswordToken</code> + * @return A message indicating authentication was successful + */ + public static String generateSuccessfulAuthenticationMessage(final AuthenticationToken token) { + final String username = extractUsername(token); + final String remoteHostname = extractHostname(token); + return String.format("Successful authentication attempt by %s from %s", username, remoteHostname); + } + + /** + * Utility method that returns <code>field</code>, or <code>defaultValue</code> if <code>field</code> is null. + * + * @param field A generic string, which is possibly null. + * @param defaultValue A non-null value returned if <code>field</code> is null + * @return <code>field</code> or <code>defaultValue</code> if field is null + * @throws IllegalArgumentException If <code>defaultValue</code> is null + */ + private static String extractField(final String field, final String defaultValue) + throws IllegalArgumentException { + + Preconditions.checkNotNull(defaultValue, "defaultValue can't be null"); + if (field != null) { + return field; + } + return defaultValue; + } +} diff --git a/odl-aaa-moon/aaa/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/filters/MoonOAuthFilter.java b/odl-aaa-moon/aaa/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/filters/MoonOAuthFilter.java new file mode 100644 index 00000000..241b7c28 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/filters/MoonOAuthFilter.java @@ -0,0 +1,186 @@ +/* + * Copyright (c) 2015 Brocade Communications Systems, Inc. and others. 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.shiro.filters; + +import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST; +import static javax.servlet.http.HttpServletResponse.SC_CREATED; +import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR; +import static javax.servlet.http.HttpServletResponse.SC_UNAUTHORIZED; + +import java.io.IOException; +import java.io.PrintWriter; + +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.oltu.oauth2.as.response.OAuthASResponse; +import org.apache.oltu.oauth2.common.exception.OAuthProblemException; +import org.apache.oltu.oauth2.common.exception.OAuthSystemException; +import org.apache.oltu.oauth2.common.message.OAuthResponse; +import org.apache.oltu.oauth2.common.message.types.TokenType; +import org.apache.shiro.SecurityUtils; +import org.apache.shiro.authc.AuthenticationException; +import org.apache.shiro.authc.AuthenticationToken; +import org.apache.shiro.authc.UsernamePasswordToken; +import org.apache.shiro.subject.Subject; +import org.apache.shiro.web.filter.authc.AuthenticatingFilter; +import org.opendaylight.aaa.AuthenticationBuilder; +import org.opendaylight.aaa.ClaimBuilder; +import org.opendaylight.aaa.api.Authentication; +import org.opendaylight.aaa.api.Claim; +import org.opendaylight.aaa.shiro.moon.MoonPrincipal; +import org.opendaylight.aaa.sts.OAuthRequest; +import org.opendaylight.aaa.sts.ServiceLocator; + +/** + * MoonOAuthFilter filters oauth1 requests form token based authentication + * @author Alioune BA alioune.ba@orange.com + * + */ +public class MoonOAuthFilter extends AuthenticatingFilter{ + + private static final String DOMAIN_SCOPE_REQUIRED = "Domain scope required"; + private static final String NOT_IMPLEMENTED = "not_implemented"; + private static final String UNAUTHORIZED = "unauthorized"; + private static final String UNAUTHORIZED_CREDENTIALS = "Unauthorized: Login/Password incorrect"; + + static final String TOKEN_GRANT_ENDPOINT = "/token"; + static final String TOKEN_REVOKE_ENDPOINT = "/revoke"; + static final String TOKEN_VALIDATE_ENDPOINT = "/validate"; + + @Override + protected UsernamePasswordToken createToken(ServletRequest request, ServletResponse response) throws Exception { + // TODO Auto-generated method stub + HttpServletRequest httpRequest = (HttpServletRequest) request; + OAuthRequest oauthRequest = new OAuthRequest(httpRequest); + return new UsernamePasswordToken(oauthRequest.getUsername(),oauthRequest.getPassword()); + } + + @Override + protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception { + // TODO Auto-generated method stub + Subject currentUser = SecurityUtils.getSubject(); + return executeLogin(request, response); + } + + protected boolean onLoginSuccess(AuthenticationToken token, Subject subject, + ServletRequest request, ServletResponse response) throws Exception { + HttpServletResponse httpResponse= (HttpServletResponse) response; + MoonPrincipal principal = (MoonPrincipal) subject.getPrincipals().getPrimaryPrincipal(); + Claim claim = principal.principalToClaim(); + oauthAccessTokenResponse(httpResponse,claim,"",principal.getToken()); + return true; + } + + protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, + ServletRequest request, ServletResponse response) { + HttpServletResponse resp = (HttpServletResponse) response; + error(resp, SC_BAD_REQUEST, UNAUTHORIZED_CREDENTIALS); + return false; + } + + protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception { + + HttpServletRequest req= (HttpServletRequest) request; + HttpServletResponse resp = (HttpServletResponse) response; + try { + if (req.getServletPath().equals(TOKEN_GRANT_ENDPOINT)) { + UsernamePasswordToken token = createToken(request, response); + if (token == null) { + String msg = "A valid non-null AuthenticationToken " + + "must be created in order to execute a login attempt."; + throw new IllegalStateException(msg); + } + try { + Subject subject = getSubject(request, response); + subject.login(token); + return onLoginSuccess(token, subject, request, response); + } catch (AuthenticationException e) { + return onLoginFailure(token, e, request, response); + } + } else if (req.getServletPath().equals(TOKEN_REVOKE_ENDPOINT)) { + //TODO: deleteAccessToken(req, resp); + } else if (req.getServletPath().equals(TOKEN_VALIDATE_ENDPOINT)) { + //TODO: validateToken(req, resp); + } + } catch (AuthenticationException e) { + error(resp, SC_UNAUTHORIZED, e.getMessage()); + } catch (OAuthProblemException oe) { + error(resp, oe); + } catch (Exception e) { + error(resp, e); + } + return false; + } + + private void oauthAccessTokenResponse(HttpServletResponse resp, Claim claim, String clientId, String token) + throws OAuthSystemException, IOException { + if (claim == null) { + throw new AuthenticationException(UNAUTHORIZED); + } + + // Cache this token... + Authentication auth = new AuthenticationBuilder(new ClaimBuilder(claim).setClientId( + clientId).build()).setExpiration(tokenExpiration()).build(); + ServiceLocator.getInstance().getTokenStore().put(token, auth); + + OAuthResponse r = OAuthASResponse.tokenResponse(SC_CREATED).setAccessToken(token) + .setTokenType(TokenType.BEARER.toString()) + .setExpiresIn(Long.toString(auth.expiration())) + .buildJSONMessage(); + write(resp, r); + } + + private void write(HttpServletResponse resp, OAuthResponse r) throws IOException { + resp.setStatus(r.getResponseStatus()); + PrintWriter pw = resp.getWriter(); + pw.print(r.getBody()); + pw.flush(); + pw.close(); + } + + private long tokenExpiration() { + return ServiceLocator.getInstance().getTokenStore().tokenExpiration(); + } + + // Emit an error OAuthResponse with the given HTTP code + private void error(HttpServletResponse resp, int httpCode, String error) { + try { + OAuthResponse r = OAuthResponse.errorResponse(httpCode).setError(error) + .buildJSONMessage(); + write(resp, r); + } catch (Exception e1) { + // Nothing to do here + } + } + + private void error(HttpServletResponse resp, OAuthProblemException e) { + try { + OAuthResponse r = OAuthResponse.errorResponse(SC_BAD_REQUEST).error(e) + .buildJSONMessage(); + write(resp, r); + } catch (Exception e1) { + // Nothing to do here + } + } + + private void error(HttpServletResponse resp, Exception e) { + try { + OAuthResponse r = OAuthResponse.errorResponse(SC_INTERNAL_SERVER_ERROR) + .setError(e.getClass().getName()) + .setErrorDescription(e.getMessage()).buildJSONMessage(); + write(resp, r); + } catch (Exception e1) { + // Nothing to do here + } + } + +} diff --git a/odl-aaa-moon/aaa/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/filters/ODLHttpAuthenticationFilter.java b/odl-aaa-moon/aaa/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/filters/ODLHttpAuthenticationFilter.java new file mode 100644 index 00000000..90b0101e --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/filters/ODLHttpAuthenticationFilter.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2015 Brocade Communications Systems, Inc. and others. 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.shiro.filters; + +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; + +import org.apache.shiro.codec.Base64; +import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter; +import org.apache.shiro.web.util.WebUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Extends <code>BasicHttpAuthenticationFilter</code> to include ability to + * authenticate OAuth2 tokens, which is needed for backwards compatibility with + * <code>TokenAuthFilter</code>. + * + * This behavior is enabled by default for backwards compatibility. To disable + * OAuth2 functionality, just comment out the following line from the + * <code>etc/shiro.ini</code> file: + * <code>authcBasic = org.opendaylight.aaa.shiro.filters.ODLHttpAuthenticationFilter</code> + * then restart the karaf container. + * + * @author Ryan Goulding (ryandgoulding@gmail.com) + * + */ +public class ODLHttpAuthenticationFilter extends BasicHttpAuthenticationFilter { + + private static final Logger LOG = LoggerFactory.getLogger(ODLHttpAuthenticationFilter.class); + + // defined in lower-case for more efficient string comparison + protected static final String BEARER_SCHEME = "bearer"; + + protected static final String OPTIONS_HEADER = "OPTIONS"; + + public ODLHttpAuthenticationFilter() { + super(); + LOG.info("Creating the ODLHttpAuthenticationFilter"); + } + + @Override + protected String[] getPrincipalsAndCredentials(String scheme, String encoded) { + final String decoded = Base64.decodeToString(encoded); + // attempt to decode username/password; otherwise decode as token + if (decoded.contains(":")) { + return decoded.split(":"); + } + return new String[] { encoded }; + } + + @Override + protected boolean isLoginAttempt(String authzHeader) { + final String authzScheme = getAuthzScheme().toLowerCase(); + final String authzHeaderLowerCase = authzHeader.toLowerCase(); + return authzHeaderLowerCase.startsWith(authzScheme) + || authzHeaderLowerCase.startsWith(BEARER_SCHEME); + } + + @Override + protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, + Object mappedValue) { + final HttpServletRequest httpRequest = WebUtils.toHttp(request); + final String httpMethod = httpRequest.getMethod(); + if (OPTIONS_HEADER.equalsIgnoreCase(httpMethod)) { + return true; + } else { + return super.isAccessAllowed(httpRequest, response, mappedValue); + } + } +} diff --git a/odl-aaa-moon/aaa/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/moon/MoonPrincipal.java b/odl-aaa-moon/aaa/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/moon/MoonPrincipal.java new file mode 100644 index 00000000..9dd2fd4f --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/moon/MoonPrincipal.java @@ -0,0 +1,160 @@ +/* + * Copyright (c) 2015 Brocade Communications Systems, Inc. and others. 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.shiro.moon; + +import com.google.common.collect.ImmutableSet; + +import java.io.Serializable; +import java.util.Set; + +import org.opendaylight.aaa.api.Claim; + +/** + * MoonPrincipal contains all user's information returned by moon on successful authentication + * @author Alioune BA alioune.ba@orange.com + * + */ +public class MoonPrincipal { + + private final String username; + private final String domain; + private final String userId; + private final Set<String> roles; + private final String token; + + + public MoonPrincipal(String username, String domain, String userId, Set<String> roles, String token) { + this.username = username; + this.domain = domain; + this.userId = userId; + this.roles = roles; + this.token = token; + } + + public MoonPrincipal createODLPrincipal(String username, String domain, + String userId, Set<String> roles, String token) { + + return new MoonPrincipal(username, domain, userId, roles,token); + } + + public Claim principalToClaim (){ + return new MoonClaim("", this.getUserId(), this.getUsername(), this.getDomain(), this.getRoles()); + } + + public String getUsername() { + return this.username; + } + + public String getDomain() { + return this.domain; + } + + public String getUserId() { + return this.userId; + } + + public Set<String> getRoles() { + return this.roles; + } + + public String getToken(){ + return this.token; + } + + public class MoonClaim implements Claim, Serializable { + private static final long serialVersionUID = -8115027645190209125L; + private int hashCode = 0; + private String clientId; + private String userId; + private String user; + private String domain; + private ImmutableSet<String> roles; + + public MoonClaim(String clientId, String userId, String user, String domain, Set<String> roles) { + this.clientId = clientId; + this.userId = userId; + this.user = user; + this.domain = domain; + this.roles = ImmutableSet.<String> builder().addAll(roles).build(); + + if (userId.isEmpty() || user.isEmpty() || roles.isEmpty() || roles.contains("")) { + throw new IllegalStateException("The Claim is missing one or more of the required fields."); + } + } + + @Override + public String clientId() { + return clientId; + } + + @Override + public String userId() { + return userId; + } + + @Override + public String user() { + return user; + } + + @Override + public String domain() { + return domain; + } + + @Override + public Set<String> roles() { + return roles; + } + public String getClientId() { + return clientId; + } + + public void setClientId(String clientId) { + this.clientId = clientId; + } + + public String getUserId() { + return userId; + } + + public void setUserId(String userId) { + this.userId = userId; + } + + public String getUser() { + return user; + } + + public void setUser(String user) { + this.user = user; + } + + public String getDomain() { + return domain; + } + + public void setDomain(String domain) { + this.domain = domain; + } + + public ImmutableSet<String> getRoles() { + return roles; + } + + public void setRoles(ImmutableSet<String> roles) { + this.roles = roles; + } + + @Override + public String toString() { + return "clientId:" + clientId + "," + "userId:" + userId + "," + "userName:" + user + + "," + "domain:" + domain + "," + "roles:" + roles ; + } + } +}
\ No newline at end of file diff --git a/odl-aaa-moon/aaa/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/moon/MoonTokenEndpoint.java b/odl-aaa-moon/aaa/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/moon/MoonTokenEndpoint.java new file mode 100644 index 00000000..a954a606 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/moon/MoonTokenEndpoint.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2015 Brocade Communications Systems, Inc. and others. 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.shiro.moon; + + +import java.io.IOException; + +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class MoonTokenEndpoint extends HttpServlet{ + + private static final long serialVersionUID = 4980356362831585417L; + private static final Logger LOG = LoggerFactory.getLogger(MoonTokenEndpoint.class); + + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException { + LOG.debug("MoonTokenEndpoint Servlet doPost"); + } + +}
\ No newline at end of file diff --git a/odl-aaa-moon/aaa/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/realm/MoonRealm.java b/odl-aaa-moon/aaa/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/realm/MoonRealm.java new file mode 100644 index 00000000..9ebbb4d7 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/realm/MoonRealm.java @@ -0,0 +1,99 @@ +/* + * Copyright (c) 2015 Brocade Communications Systems, Inc. and others. 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.shiro.realm; + +import com.sun.jersey.api.client.Client; +import com.sun.jersey.api.client.ClientResponse; +import com.sun.jersey.api.client.WebResource; +import com.sun.jersey.api.client.config.ClientConfig; +import com.sun.jersey.api.client.config.DefaultClientConfig; + +import java.util.LinkedHashSet; +import java.util.Set; + +import org.apache.shiro.authc.AuthenticationException; +import org.apache.shiro.authc.AuthenticationInfo; +import org.apache.shiro.authc.AuthenticationToken; +import org.apache.shiro.authc.SimpleAuthenticationInfo; +import org.apache.shiro.authc.UsernamePasswordToken; +import org.apache.shiro.authz.AuthorizationInfo; +import org.apache.shiro.realm.AuthorizingRealm; +import org.apache.shiro.subject.PrincipalCollection; +import org.json.JSONException; +import org.json.JSONObject; +import org.json.JSONTokener; +import org.opendaylight.aaa.shiro.moon.MoonPrincipal; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +/** + * MoonRealm is a Shiro Realm that authenticates users from OPNFV/moon platform + * @author Alioune BA alioune.ba@orange.com + * + */ +public class MoonRealm extends AuthorizingRealm{ + + private static final Logger LOG = LoggerFactory.getLogger(MoonRealm.class); + @Override + protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection arg0) { + // TODO Auto-generated method stub + return null; + } + + @Override + protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { + // TODO Auto-generated method stub + String username = ""; + String password = ""; + String domain = "sdn"; + username = (String) authenticationToken.getPrincipal(); + final UsernamePasswordToken upt = (UsernamePasswordToken) authenticationToken; + password = new String(upt.getPassword()); + final MoonPrincipal moonPrincipal = moonAuthenticate(username,password,domain); + if (moonPrincipal!=null){ + return new SimpleAuthenticationInfo(moonPrincipal, password.toCharArray(),getName()); + }else{ + return null; + } + } + + public MoonPrincipal moonAuthenticate(String username, String password, String domain){ + + String output = ""; + ClientConfig config = new DefaultClientConfig(); + Client client = Client.create(config); + JSONTokener tokener; + JSONObject object =null; + Set<String> UserRoles = new LinkedHashSet<>(); + + String server = System.getenv("MOON_SERVER_ADDR"); + String port = System.getenv("MOON_SERVER_PORT"); + String URL = "http://" +server+ ":" +port+ "/moon/auth/tokens"; + LOG.debug("Moon server is at: {} ", server); + WebResource webResource = client.resource(URL); + String input = "{\"username\": \""+ username + "\"," + "\"password\":" + "\"" + password + "\"," + "\"project\":" + "\"" + domain + "\"" + "}";; + ClientResponse response = webResource.type("application/json").post(ClientResponse.class, input); + output = response.getEntity(String.class); + tokener = new JSONTokener(output); + object = new JSONObject(tokener); + try { + if (object.getString("token")!=null){ + String token = object.getString("token"); + String userID = username+"@"+domain; + for (int i=0; i< object.getJSONArray("roles").length(); i++){ + UserRoles.add((String) object.getJSONArray("roles").get(i)); + } + MoonPrincipal principal = new MoonPrincipal(username,domain,userID,UserRoles,token); + return principal; + } + }catch (JSONException e){ + throw new IllegalStateException("Authentication Error : "+ object.getJSONObject("error").getString("title")); + } + return null; + } + +} diff --git a/odl-aaa-moon/aaa/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/realm/ODLJndiLdapRealm.java b/odl-aaa-moon/aaa/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/realm/ODLJndiLdapRealm.java new file mode 100644 index 00000000..7d0bafd7 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/realm/ODLJndiLdapRealm.java @@ -0,0 +1,315 @@ +/* + * Copyright (c) 2015, 2016 Brocade Communications Systems, Inc. and others. 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.shiro.realm; + +import java.util.Collection; +import java.util.LinkedHashSet; +import java.util.Set; + +import javax.naming.NamingEnumeration; +import javax.naming.NamingException; +import javax.naming.directory.Attribute; +import javax.naming.directory.Attributes; +import javax.naming.directory.SearchControls; +import javax.naming.directory.SearchResult; +import javax.naming.ldap.LdapContext; + +import org.apache.shiro.authc.AuthenticationException; +import org.apache.shiro.authc.AuthenticationInfo; +import org.apache.shiro.authc.AuthenticationToken; +import org.apache.shiro.authz.AuthorizationInfo; +import org.apache.shiro.authz.SimpleAuthorizationInfo; +import org.apache.shiro.realm.ldap.JndiLdapRealm; +import org.apache.shiro.realm.ldap.LdapContextFactory; +import org.apache.shiro.realm.ldap.LdapUtils; +import org.apache.shiro.subject.PrincipalCollection; +import org.apache.shiro.util.Nameable; +import org.opendaylight.aaa.shiro.accounting.Accounter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * An extended implementation of + * <code>org.apache.shiro.realm.ldap.JndiLdapRealm</code> which includes + * additional Authorization capabilities. To enable this Realm, add the + * following to <code>shiro.ini</code>: + * + *<code>#ldapRealm = org.opendaylight.aaa.shiro.realm.ODLJndiLdapRealmAuthNOnly + *#ldapRealm.userDnTemplate = uid={0},ou=People,dc=DOMAIN,dc=TLD + *#ldapRealm.contextFactory.url = ldap://URL:389 + *#ldapRealm.searchBase = dc=DOMAIN,dc=TLD + *#ldapRealm.ldapAttributeForComparison = objectClass + *# The CSV list of enabled realms. In order to enable a realm, add it to the + *# list below: + * securityManager.realms = $tokenAuthRealm, $ldapRealm</code> + * + * The values above are specific to the deployed LDAP domain. If the defaults + * are not sufficient, alternatives can be derived through enabling + * <code>TRACE</code> level logging. To enable <code>TRACE</code> level + * logging, issue the following command in the karaf shell: + * <code>log:set TRACE org.opendaylight.aaa.shiro.realm.ODLJndiLdapRealm</code> + * + * @author Ryan Goulding (ryandgoulding@gmail.com) + * @see <code>org.apache.shiro.realm.ldap.JndiLdapRealm</code> + * @see <a + * href="https://shiro.apache.org/static/1.2.3/apidocs/org/apache/shiro/realm/ldap/JndiLdapRealm.html">Shiro + * documentation</a> + */ +public class ODLJndiLdapRealm extends JndiLdapRealm implements Nameable { + + private static final Logger LOG = LoggerFactory.getLogger(ODLJndiLdapRealm.class); + + /** + * When an LDAP Authorization lookup is made for a user account, a list of + * attributes are returned. The attributes are used to determine LDAP + * grouping, which is equivalent to ODL role(s). The default value is + * set to "objectClass", which is common attribute for LDAP systems. + * The actual value may be configured through setting + * <code>ldapAttributeForComparison</code>. + */ + private static final String DEFAULT_LDAP_ATTRIBUTE_FOR_COMPARISON = "objectClass"; + + /** + * The LDAP nomenclature for user ID, which is used in the authorization query process. + */ + private static final String UID = "uid"; + + /** + * The searchBase for the ldap query, which indicates the LDAP realms to + * search. By default, this is set to the + * <code>super.getUserDnSuffix()</code>. + */ + private String searchBase = super.getUserDnSuffix(); + + /** + * When an LDAP Authorization lookup is made for a user account, a list of + * attributes is returned. The attributes are used to determine LDAP + * grouping, which is equivalent to ODL role(s). The default is set to + * <code>DEFAULT_LDAP_ATTRIBUTE_FOR_COMPARISON</code>. + */ + private String ldapAttributeForComparison = DEFAULT_LDAP_ATTRIBUTE_FOR_COMPARISON; + + /* + * Adds debugging information surrounding creation of ODLJndiLdapRealm + */ + public ODLJndiLdapRealm() { + super(); + final String DEBUG_MESSAGE = "Creating ODLJndiLdapRealm"; + LOG.debug(DEBUG_MESSAGE); + } + + /* + * (non-Javadoc) Overridden to expose important audit trail information for + * accounting. + * + * @see + * org.apache.shiro.realm.ldap.JndiLdapRealm#doGetAuthenticationInfo(org + * .apache.shiro.authc.AuthenticationToken) + */ + @Override + protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) + throws AuthenticationException { + + // Delegates all AuthN lookup responsibility to the super class + try { + final String username = getUsername(token); + logIncomingConnection(username); + return super.doGetAuthenticationInfo(token); + } catch (ClassCastException e) { + LOG.info("Couldn't service the LDAP connection", e); + } + return null; + } + + /** + * Logs an incoming LDAP connection + * + * @param username + * the requesting user + */ + protected void logIncomingConnection(final String username) { + LOG.info("AAA LDAP connection from {}", username); + Accounter.output("AAA LDAP connection from " + username); + } + + /** + * Extracts the username from <code>token</code> + * + * @param token Encoded token which could contain a username + * @return The extracted username + * @throws ClassCastException + * The incoming token is not username/password (i.e., X.509 + * certificate) + */ + public static String getUsername(AuthenticationToken token) throws ClassCastException { + if (null == token) { + return null; + } + return (String) token.getPrincipal(); + } + + @Override + protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { + + AuthorizationInfo ai = null; + try { + ai = this.queryForAuthorizationInfo(principals, getContextFactory()); + } catch (NamingException e) { + LOG.error("Unable to query for AuthZ info", e); + } + return ai; + } + + /** + * extracts a username from <code>principals</code> + * + * @param principals A single principal extracted for the username + * @return The username if possible + * @throws ClassCastException + * the PrincipalCollection contains an element that is not in + * username/password form (i.e., X.509 certificate) + */ + protected String getUsername(final PrincipalCollection principals) throws ClassCastException { + + if (null == principals) { + return null; + } + return (String) getAvailablePrincipal(principals); + } + + /* + * (non-Javadoc) + * + * This method is only called if doGetAuthenticationInfo(...) completes successfully AND + * the requested endpoint has an RBAC restriction. To add an RBAC restriction, edit the + * etc/shiro.ini file and add a url to the url section. E.g., + * + * <code>/** = authcBasic, roles[person]</code> + * + * @see org.apache.shiro.realm.ldap.JndiLdapRealm#queryForAuthorizationInfo(org.apache.shiro.subject.PrincipalCollection, org.apache.shiro.realm.ldap.LdapContextFactory) + */ + @Override + protected AuthorizationInfo queryForAuthorizationInfo(PrincipalCollection principals, + LdapContextFactory ldapContextFactory) throws NamingException { + + AuthorizationInfo authorizationInfo = null; + try { + final String username = getUsername(principals); + final LdapContext ldapContext = ldapContextFactory.getSystemLdapContext(); + final Set<String> roleNames; + + try { + roleNames = getRoleNamesForUser(username, ldapContext); + authorizationInfo = buildAuthorizationInfo(roleNames); + } finally { + LdapUtils.closeContext(ldapContext); + } + } catch (ClassCastException e) { + LOG.error("Unable to extract a valid user", e); + } + return authorizationInfo; + } + + public static AuthorizationInfo buildAuthorizationInfo(final Set<String> roleNames) { + if (null == roleNames) { + return null; + } + return new SimpleAuthorizationInfo(roleNames); + } + + /** + * extracts the Set of roles associated with a user based on the username + * and ldap context (server). + * + * @param username The username for the request + * @param ldapContext The specific system context provided by <code>shiro.ini</code> + * @return A set of roles + * @throws NamingException If the ldap search fails + */ + protected Set<String> getRoleNamesForUser(final String username, final LdapContext ldapContext) + throws NamingException { + + // Stores the role names, which are equivalent to the set of group names extracted + // from the LDAP query. + final Set<String> roleNames = new LinkedHashSet<String>(); + + final SearchControls searchControls = createSearchControls(); + + LOG.debug("Asking the configured LDAP about which groups uid=\"{}\" belongs to using " + + "searchBase=\"{}\" ldapAttributeForComparison=\"{}\"", + username, searchBase, ldapAttributeForComparison); + final NamingEnumeration<SearchResult> answer = ldapContext.search(searchBase, + String.format("%s=%s", UID, username), searchControls); + + // Cursor based traversal over the LDAP query result + while (answer.hasMoreElements()) { + final SearchResult searchResult = answer.next(); + final Attributes attrs = searchResult.getAttributes(); + if (attrs != null) { + // Extract the attributes from the LDAP search. + // attrs.getAttr(String) was not chosen, since all attributes should be exposed + // in trace logging should the operator wish to use an alternate attribute. + final NamingEnumeration<? extends Attribute> ae = attrs.getAll(); + while (ae.hasMore()) { + final Attribute attr = ae.next(); + LOG.trace("LDAP returned \"{}\" attribute for \"{}\"", attr.getID(), username); + if (attr.getID().equals(ldapAttributeForComparison)) { + // Stresses the point that LDAP groups are EQUIVALENT to ODL role names + // TODO make this configurable via a Strategy pattern so more interesting mappings can be made + final Collection<String> groupNamesExtractedFromLdap = LdapUtils.getAllAttributeValues(attr); + final Collection<String> roleNamesFromLdapGroups = groupNamesExtractedFromLdap; + if (LOG.isTraceEnabled()) { + for (String roleName : roleNamesFromLdapGroups) { + LOG.trace("Mapped the \"{}\" LDAP group to ODL role for \"{}\"", roleName, username); + } + } + roleNames.addAll(roleNamesFromLdapGroups); + } + } + } + } + return roleNames; + } + + /** + * A utility method to help create the search controls for the LDAP lookup + * + * @return A generic set of search controls for LDAP scoped to subtree + */ + protected static SearchControls createSearchControls() { + SearchControls searchControls = new SearchControls(); + searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE); + return searchControls; + } + + @Override + public String getUserDnSuffix() { + return super.getUserDnSuffix(); + } + + /** + * Injected from <code>shiro.ini</code> configuration. + * + * @param searchBase The desired value for searchBase + */ + public void setSearchBase(final String searchBase) { + // public for injection reasons + this.searchBase = searchBase; + } + + /** + * Injected from <code>shiro.ini</code> configuration. + * + * @param ldapAttributeForComparison The attribute from which groups are extracted + */ + public void setLdapAttributeForComparison(final String ldapAttributeForComparison) { + // public for injection reasons + this.ldapAttributeForComparison = ldapAttributeForComparison; + } +} diff --git a/odl-aaa-moon/aaa/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/realm/ODLJndiLdapRealmAuthNOnly.java b/odl-aaa-moon/aaa/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/realm/ODLJndiLdapRealmAuthNOnly.java new file mode 100644 index 00000000..978266c5 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/realm/ODLJndiLdapRealmAuthNOnly.java @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2016 Brocade Communications Systems, Inc. and others. 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.shiro.realm; + +import org.apache.shiro.authc.AuthenticationException; +import org.apache.shiro.authc.AuthenticationInfo; +import org.apache.shiro.authc.AuthenticationToken; +import org.apache.shiro.realm.ldap.JndiLdapRealm; +import org.opendaylight.aaa.shiro.accounting.Accounter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Wrapper class for <code>org.apache.shiro.realm.ldap.JndiLdapRealm</code>. + * This implementation disables Authorization so any LDAP user is able to access + * server resources. This is particularly useful for quickly prototyping ODL + * without worrying about resolving LDAP attributes (groups) to OpenDaylight + * roles. + * + * The motivation for subclassing Shiro's implementation is two-fold: 1) Enhance + * the default logging of Shiro. This allows us to more easily log incoming + * connections, providing some security auditing. 2) Provide a common package in + * the classpath for ODL supported Realm implementations (i.e., + * <code>org.opendaylight.aaa.shiro.realm</code>), which consolidates the number + * of <code>Import-Package</code> statements consumers need to enumerate. For + * example, the netconf project only needs to import + * <code>org.opendaylight.aaa.shiro.realm</code>, and does not need to worry + * about importing Shiro packages. + * + * @author Ryan Goulding (ryandgoulding@gmail.com) + * + */ +public class ODLJndiLdapRealmAuthNOnly extends JndiLdapRealm { + + private static final Logger LOG = LoggerFactory.getLogger(ODLJndiLdapRealmAuthNOnly.class); + + private static final String LDAP_CONNECTION_MESSAGE = "AAA LDAP connection from "; + + /* + * Adds debugging information surrounding creation of ODLJndiLdapRealm + */ + public ODLJndiLdapRealmAuthNOnly() { + super(); + LOG.debug("Creating ODLJndiLdapRealmAuthNOnly"); + } + + /* + * (non-Javadoc) Overridden to expose important audit trail information for + * accounting. + * + * @see + * org.apache.shiro.realm.ldap.JndiLdapRealm#doGetAuthenticationInfo(org + * .apache.shiro.authc.AuthenticationToken) + */ + @Override + protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) + throws AuthenticationException { + + try { + final String username = getUsername(token); + logIncomingConnection(username); + return super.doGetAuthenticationInfo(token); + } catch (ClassCastException e) { + LOG.info("Couldn't service the LDAP connection", e); + } + return null; + } + + /** + * Logs an incoming LDAP connection + * + * @param username + * the requesting user + */ + protected void logIncomingConnection(final String username) { + final String message = LDAP_CONNECTION_MESSAGE + username; + LOG.info(message); + Accounter.output(message); + } + + /** + * Extracts the username from <code>token</code> + * + * @param token Which possibly contains a username + * @return the username if it can be extracted + * @throws ClassCastException + * The incoming token is not username/password (i.e., X.509 + * certificate) + */ + public static String getUsername(AuthenticationToken token) throws ClassCastException { + if (null == token) { + return null; + } + return (String) token.getPrincipal(); + } +} diff --git a/odl-aaa-moon/aaa/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/realm/RadiusRealm.java b/odl-aaa-moon/aaa/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/realm/RadiusRealm.java new file mode 100644 index 00000000..51d4bfbf --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/realm/RadiusRealm.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2015 Brocade Communications Systems, Inc. and others. 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.shiro.realm; + +import org.apache.shiro.authc.AuthenticationException; +import org.apache.shiro.authc.AuthenticationInfo; +import org.apache.shiro.authc.AuthenticationToken; +import org.apache.shiro.authz.AuthorizationInfo; +import org.apache.shiro.realm.AuthorizingRealm; +import org.apache.shiro.subject.PrincipalCollection; + +/** + * Implementation of a Radius AuthorizingRealm. + * + * @author Ryan Goulding (ryandgoulding@gmail.com) + */ +public class RadiusRealm extends AuthorizingRealm { + + @Override + protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection arg0) { + // TODO use JRadius to extract Authorization Info + return null; + } + + @Override + protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken arg0) + throws AuthenticationException { + // TODO use JRadius to extract Authentication Info + return null; + } + +} diff --git a/odl-aaa-moon/aaa/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/realm/TACACSRealm.java b/odl-aaa-moon/aaa/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/realm/TACACSRealm.java new file mode 100644 index 00000000..38d7d91a --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/realm/TACACSRealm.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2015 Brocade Communications Systems, Inc. and others. 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.shiro.realm; + +import org.apache.shiro.authc.AuthenticationException; +import org.apache.shiro.authc.AuthenticationInfo; +import org.apache.shiro.authc.AuthenticationToken; +import org.apache.shiro.authz.AuthorizationInfo; +import org.apache.shiro.realm.AuthorizingRealm; +import org.apache.shiro.subject.PrincipalCollection; + +/** + * + * @author Ryan Goulding (ryandgoulding@gmail.com) + * + */ +public class TACACSRealm extends AuthorizingRealm { + + @Override + protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection arg0) { + // TODO Extract AuthorizationInfo using JNetLib + return null; + } + + @Override + protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken arg0) + throws AuthenticationException { + // TODO Extract AuthenticationInfo using JNetLib + return null; + } + +} diff --git a/odl-aaa-moon/aaa/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/realm/TokenAuthRealm.java b/odl-aaa-moon/aaa/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/realm/TokenAuthRealm.java new file mode 100644 index 00000000..f9ae5051 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/realm/TokenAuthRealm.java @@ -0,0 +1,369 @@ +/* + * Copyright (c) 2015 Brocade Communications Systems, Inc. and others. 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.shiro.realm; + +import com.google.common.base.Strings; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.apache.shiro.authc.AuthenticationException; +import org.apache.shiro.authc.AuthenticationInfo; +import org.apache.shiro.authc.AuthenticationToken; +import org.apache.shiro.authc.SimpleAuthenticationInfo; +import org.apache.shiro.authc.UsernamePasswordToken; +import org.apache.shiro.authz.AuthorizationInfo; +import org.apache.shiro.authz.SimpleAuthorizationInfo; +import org.apache.shiro.codec.Base64; +import org.apache.shiro.realm.AuthorizingRealm; +import org.apache.shiro.subject.PrincipalCollection; +import org.opendaylight.aaa.api.Authentication; +import org.opendaylight.aaa.api.TokenAuth; +import org.opendaylight.aaa.basic.HttpBasicAuth; +import org.opendaylight.aaa.sts.ServiceLocator; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * TokenAuthRealm is an adapter between the AAA shiro subsystem and the existing + * <code>TokenAuth</code> mechanisms. Thus, one can enable use of + * <code>IDMStore</code> and <code>IDMMDSALStore</code>. + * + * @author Ryan Goulding (ryandgoulding@gmail.com) + */ +public class TokenAuthRealm extends AuthorizingRealm { + + private static final String USERNAME_DOMAIN_SEPARATOR = "@"; + + /** + * The unique identifying name for <code>TokenAuthRealm</code> + */ + private static final String TOKEN_AUTH_REALM_DEFAULT_NAME = "TokenAuthRealm"; + + /** + * The message that is displayed if no <code>TokenAuth</code> interface is + * available yet + */ + private static final String AUTHENTICATION_SERVICE_UNAVAILABLE_MESSAGE = "{\"error\":\"Authentication service unavailable\"}"; + + /** + * The message that is displayed if credentials are missing or malformed + */ + private static final String FATAL_ERROR_DECODING_CREDENTIALS = "{\"error\":\"Unable to decode credentials\"}"; + + /** + * The message that is displayed if non-Basic Auth is attempted + */ + private static final String FATAL_ERROR_BASIC_AUTH_ONLY = "{\"error\":\"Only basic authentication is supported by TokenAuthRealm\"}"; + + /** + * The purposefully generic message displayed if <code>TokenAuth</code> is + * unable to validate the given credentials + */ + private static final String UNABLE_TO_AUTHENTICATE = "{\"error\":\"Could not authenticate\"}"; + + private static final Logger LOG = LoggerFactory.getLogger(TokenAuthRealm.class); + + public TokenAuthRealm() { + super(); + super.setName(TOKEN_AUTH_REALM_DEFAULT_NAME); + } + + /* + * (non-Javadoc) + * + * Roles are derived from <code>TokenAuth.authenticate()</code>. Shiro roles + * are identical to existing IDM roles. + * + * @see + * org.apache.shiro.realm.AuthorizingRealm#doGetAuthorizationInfo(org.apache + * .shiro.subject.PrincipalCollection) + */ + @Override + protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { + final Object primaryPrincipal = principalCollection.getPrimaryPrincipal(); + final ODLPrincipal odlPrincipal; + try { + odlPrincipal = (ODLPrincipal) primaryPrincipal; + return new SimpleAuthorizationInfo(odlPrincipal.getRoles()); + } catch(ClassCastException e) { + LOG.error("Couldn't decode authorization request", e); + } + return new SimpleAuthorizationInfo(); + } + + /** + * Bridge new to old style <code>TokenAuth</code> interface. + * + * @param username The request username + * @param password The request password + * @param domain The request domain + * @return <code>username:password:domain</code> + */ + static String getUsernamePasswordDomainString(final String username, final String password, + final String domain) { + return username + HttpBasicAuth.AUTH_SEP + password + HttpBasicAuth.AUTH_SEP + domain; + } + + /** + * + * @param credentialToken + * @return Base64 encoded token + */ + static String getEncodedToken(final String credentialToken) { + return Base64.encodeToString(credentialToken.getBytes()); + } + + /** + * + * @param encodedToken + * @return Basic <code>encodedToken</code> + */ + static String getTokenAuthHeader(final String encodedToken) { + return HttpBasicAuth.BASIC_PREFIX + encodedToken; + } + + /** + * + * @param tokenAuthHeader + * @return a map with the basic auth header + */ + Map<String, List<String>> formHeadersWithToken(final String tokenAuthHeader) { + final Map<String, List<String>> headers = new HashMap<String, List<String>>(); + final List<String> headerValue = new ArrayList<String>(); + headerValue.add(tokenAuthHeader); + headers.put(HttpBasicAuth.AUTH_HEADER, headerValue); + return headers; + } + + /** + * Adapter between basic authentication mechanism and existing + * <code>TokenAuth</code> interface. + * + * @param username Username from the request + * @param password Password from the request + * @param domain Domain from the request + * @return input map for <code>TokenAuth.validate()</code> + */ + Map<String, List<String>> formHeaders(final String username, final String password, + final String domain) { + String usernamePasswordToken = getUsernamePasswordDomainString(username, password, domain); + String encodedToken = getEncodedToken(usernamePasswordToken); + String tokenAuthHeader = getTokenAuthHeader(encodedToken); + return formHeadersWithToken(tokenAuthHeader); + } + + /** + * Adapter to check for available <code>TokenAuth<code> implementations. + * + * @return + */ + boolean isTokenAuthAvailable() { + return ServiceLocator.getInstance().getAuthenticationService() != null; + } + + /* + * (non-Javadoc) + * + * Authenticates against any <code>TokenAuth</code> registered with the + * <code>ServiceLocator</code> + * + * @see + * org.apache.shiro.realm.AuthenticatingRealm#doGetAuthenticationInfo(org + * .apache.shiro.authc.AuthenticationToken) + */ + @Override + protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) + throws AuthenticationException { + + String username = ""; + String password = ""; + String domain = HttpBasicAuth.DEFAULT_DOMAIN; + + try { + final String qualifiedUser = extractUsername(authenticationToken); + if (qualifiedUser.contains(USERNAME_DOMAIN_SEPARATOR)) { + final String [] qualifiedUserArray = qualifiedUser.split(USERNAME_DOMAIN_SEPARATOR); + try { + username = qualifiedUserArray[0]; + domain = qualifiedUserArray[1]; + } catch (ArrayIndexOutOfBoundsException e) { + LOG.trace("Couldn't parse domain from {}; trying without one", + qualifiedUser, e); + } + } else { + username = qualifiedUser; + } + password = extractPassword(authenticationToken); + + } catch (NullPointerException e) { + throw new AuthenticationException(FATAL_ERROR_DECODING_CREDENTIALS, e); + } catch (ClassCastException e) { + throw new AuthenticationException(FATAL_ERROR_BASIC_AUTH_ONLY, e); + } + + // check to see if there are TokenAuth implementations available + if (!isTokenAuthAvailable()) { + throw new AuthenticationException(AUTHENTICATION_SERVICE_UNAVAILABLE_MESSAGE); + } + + // if the password is empty, this is an OAuth2 request, not a Basic HTTP + // Auth request + if (!Strings.isNullOrEmpty(password)) { + if (ServiceLocator.getInstance().getAuthenticationService().isAuthEnabled()) { + Map<String, List<String>> headers = formHeaders(username, password, domain); + // iterate over <code>TokenAuth</code> implementations and + // attempt to + // authentication with each one + final List<TokenAuth> tokenAuthCollection = ServiceLocator.getInstance() + .getTokenAuthCollection(); + for (TokenAuth ta : tokenAuthCollection) { + try { + LOG.debug("Authentication attempt using {}", ta.getClass().getName()); + final Authentication auth = ta.validate(headers); + if (auth != null) { + LOG.debug("Authentication attempt successful"); + ServiceLocator.getInstance().getAuthenticationService().set(auth); + final ODLPrincipal odlPrincipal = ODLPrincipal.createODLPrincipal(auth); + return new SimpleAuthenticationInfo(odlPrincipal, password.toCharArray(), + getName()); + } + } catch (AuthenticationException ae) { + LOG.debug("Authentication attempt unsuccessful"); + throw new AuthenticationException(UNABLE_TO_AUTHENTICATE, ae); + } + } + } + } + + // extract the authentication token and attempt validation of the token + final String token = extractUsername(authenticationToken); + final Authentication auth; + try { + auth = validate(token); + if (auth != null) { + final ODLPrincipal odlPrincipal = ODLPrincipal.createODLPrincipal(auth); + return new SimpleAuthenticationInfo(odlPrincipal, "", getName()); + } + } catch (AuthenticationException e) { + LOG.debug("Unknown OAuth2 Token Access Request", e); + } + + LOG.debug("Authentication failed: exhausted TokenAuth resources"); + return null; + } + + private Authentication validate(final String token) { + Authentication auth = ServiceLocator.getInstance().getTokenStore().get(token); + if (auth == null) { + throw new AuthenticationException("Could not validate the token " + token); + } else { + ServiceLocator.getInstance().getAuthenticationService().set(auth); + } + return auth; + } + + /** + * extract the username from an <code>AuthenticationToken</code> + * + * @param authenticationToken + * @return + * @throws ClassCastException + * @throws NullPointerException + */ + static String extractUsername(final AuthenticationToken authenticationToken) + throws ClassCastException, NullPointerException { + + return (String) authenticationToken.getPrincipal(); + } + + /** + * extract the password from an <code>AuthenticationToken</code> + * + * @param authenticationToken + * @return + * @throws ClassCastException + * @throws NullPointerException + */ + static String extractPassword(final AuthenticationToken authenticationToken) + throws ClassCastException, NullPointerException { + + final UsernamePasswordToken upt = (UsernamePasswordToken) authenticationToken; + return new String(upt.getPassword()); + } + + /** + * Since <code>TokenAuthRealm</code> is an <code>AuthorizingRealm</code>, it supports + * individual steps for authentication and authorization. In ODL's existing <code>TokenAuth</code> + * mechanism, authentication and authorization are currently done in a single monolithic step. + * <code>ODLPrincipal</code> is abstracted as a DTO between the two steps. It fulfills the + * responsibility of a <code>Principal</code>, since it contains identification information + * but no credential information. + * + * @author Ryan Goulding (ryandgoulding@gmail.com) + */ + private static class ODLPrincipal { + + private final String username; + private final String domain; + private final String userId; + private final Set<String> roles; + + private ODLPrincipal(final String username, final String domain, final String userId, final Set<String> roles) { + this.username = username; + this.domain = domain; + this.userId = userId; + this.roles = roles; + } + + /** + * A static factory method to create <code>ODLPrincipal</code> instances. + * + * @param username The authenticated user + * @param domain The domain <code>username</code> belongs to. + * @param userId The unique key for <code>username</code> + * @param roles The roles associated with <code>username</code>@<code>domain</code> + * @return A Principal for the given session; essentially a DTO. + */ + static ODLPrincipal createODLPrincipal(final String username, final String domain, + final String userId, final Set<String> roles) { + + return new ODLPrincipal(username, domain, userId, roles); + } + + /** + * A static factory method to create <code>ODLPrincipal</code> instances. + * + * @param auth Contains identifying information for the particular request. + * @return A Principal for the given session; essentially a DTO. + */ + static ODLPrincipal createODLPrincipal(final Authentication auth) { + return createODLPrincipal(auth.user(), auth.domain(), auth.userId(), auth.roles()); + } + + String getUsername() { + return this.username; + } + + String getDomain() { + return this.domain; + } + + String getUserId() { + return this.userId; + } + + Set<String> getRoles() { + return this.roles; + } + } +} diff --git a/odl-aaa-moon/aaa/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/web/env/KarafIniWebEnvironment.java b/odl-aaa-moon/aaa/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/web/env/KarafIniWebEnvironment.java new file mode 100644 index 00000000..acf4022c --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/web/env/KarafIniWebEnvironment.java @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2015 Brocade Communications Systems, Inc. and others. 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.shiro.web.env; + +import java.io.File; +import java.io.FileNotFoundException; +import java.util.Collection; +import org.apache.shiro.config.Ini; +import org.apache.shiro.config.Ini.Section; +import org.apache.shiro.web.env.IniWebEnvironment; +import org.opendaylight.aaa.shiro.accounting.Accounter; +import org.opendaylight.aaa.shiro.authorization.DefaultRBACRules; +import org.opendaylight.aaa.shiro.authorization.RBACRule; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Identical to <code>IniWebEnvironment</code> except the Ini is loaded from + * <code>$KARAF_HOME/etc/shiro.ini</code>. + * + * @author Ryan Goulding (ryandgoulding@gmail.com) + * + */ +public class KarafIniWebEnvironment extends IniWebEnvironment { + + private static final Logger LOG = LoggerFactory.getLogger(KarafIniWebEnvironment.class); + public static final String DEFAULT_SHIRO_INI_FILE = "etc/shiro.ini"; + public static final String SHIRO_FILE_PREFIX = "file:/"; + + public KarafIniWebEnvironment() { + } + + @Override + public void init() { + // Initialize the Shiro environment from etc/shiro.ini then delegate to + // the parent class + Ini ini; + try { + ini = createDefaultShiroIni(); + // appendCustomIniRules(ini); + setIni(ini); + } catch (FileNotFoundException e) { + final String ERROR_MESSAGE = "Could not find etc/shiro.ini"; + LOG.error(ERROR_MESSAGE, e); + } + super.init(); + } + + /** + * A hook for installing custom default RBAC rules for security purposes. + * + * @param ini + */ + private void appendCustomIniRules(final Ini ini) { + final String INSTALL_MESSAGE = "Installing the RBAC rule: %s"; + Section urlSection = getOrCreateUrlSection(ini); + Collection<RBACRule> rbacRules = DefaultRBACRules.getInstance().getRBACRules(); + for (RBACRule rbacRule : rbacRules) { + urlSection.put(rbacRule.getUrlPattern(), rbacRule.getRolesInShiroFormat()); + Accounter.output(String.format(INSTALL_MESSAGE, rbacRule)); + } + } + + /** + * Extracts the url section of the Ini file, or creates one if it doesn't + * already exist + * + * @param ini + * @return + */ + private Section getOrCreateUrlSection(final Ini ini) { + final String URL_SECTION_TITLE = "urls"; + Section urlSection = ini.getSection(URL_SECTION_TITLE); + if (urlSection == null) { + LOG.debug("shiro.ini does not contain a [urls] section; creating one"); + urlSection = ini.addSection(URL_SECTION_TITLE); + } else { + LOG.debug("shiro.ini contains a [urls] section; appending rules to existing"); + } + return urlSection; + } + + /** + * + * @return Ini associated with <code>$KARAF_HOME/etc/shiro.ini</code> + * @throws FileNotFoundException + */ + static Ini createDefaultShiroIni() throws FileNotFoundException { + return createShiroIni(DEFAULT_SHIRO_INI_FILE); + } + + /** + * + * @param path + * the file path, which is either absolute or relative to + * <code>$KARAF_HOME</code> + * @return Ini loaded from <code>path</code> + */ + static Ini createShiroIni(final String path) throws FileNotFoundException { + File f = new File(path); + Ini ini = new Ini(); + final String fileBasedIniPath = createFileBasedIniPath(f.getAbsolutePath()); + ini.loadFromPath(fileBasedIniPath); + return ini; + } + + /** + * + * @param path + * the file path, which is either absolute or relative to + * <code>$KARAF_HOME</code> + * @return <code>file:/$KARAF_HOME/etc/shiro.ini</code> + */ + static String createFileBasedIniPath(final String path) { + String fileBasedIniPath = SHIRO_FILE_PREFIX + path; + LOG.debug(fileBasedIniPath); + return fileBasedIniPath; + } +} diff --git a/odl-aaa-moon/aaa/aaa-shiro/src/main/resources/WEB-INF/web.xml b/odl-aaa-moon/aaa/aaa-shiro/src/main/resources/WEB-INF/web.xml new file mode 100644 index 00000000..63288c23 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-shiro/src/main/resources/WEB-INF/web.xml @@ -0,0 +1,48 @@ +<?xml version="1.0" encoding="ISO-8859-1"?> +<web-app xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" + version="3.0"> + + <servlet> + <servlet-name>MOON</servlet-name> + <servlet-class>org.opendaylight.aaa.shiro.moon.MoonTokenEndpoint</servlet-class> + <load-on-startup>1</load-on-startup> + </servlet> + + <servlet-mapping> + <servlet-name>MOON</servlet-name> + <url-pattern>/token</url-pattern> + </servlet-mapping> + <servlet-mapping> + <servlet-name>MOON</servlet-name> + <url-pattern>/revoke</url-pattern> + </servlet-mapping> + <servlet-mapping> + <servlet-name>MOON</servlet-name> + <url-pattern>/validate</url-pattern> + </servlet-mapping> + <servlet-mapping> + <servlet-name>MOON</servlet-name> + <url-pattern>/*</url-pattern> + </servlet-mapping> + + <!-- Shiro Filter --> + <context-param> + <param-name>shiroEnvironmentClass</param-name> + <param-value>org.opendaylight.aaa.shiro.web.env.KarafIniWebEnvironment</param-value> + </context-param> + + <listener> + <listener-class>org.apache.shiro.web.env.EnvironmentLoaderListener</listener-class> + </listener> + + <filter> + <filter-name>ShiroFilter</filter-name> + <filter-class>org.opendaylight.aaa.shiro.filters.AAAFilter</filter-class> + </filter> + + <filter-mapping> + <filter-name>ShiroFilter</filter-name> + <url-pattern>/*</url-pattern> + </filter-mapping> +</web-app>
\ No newline at end of file diff --git a/odl-aaa-moon/aaa/aaa-shiro/src/main/resources/shiro.ini b/odl-aaa-moon/aaa/aaa-shiro/src/main/resources/shiro.ini new file mode 100644 index 00000000..b48abe96 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-shiro/src/main/resources/shiro.ini @@ -0,0 +1,106 @@ +# +# Copyright (c) 2015 Brocade Communications Systems, Inc. and others. 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 +# + +############################################################################### +# shiro.ini # +# # +# Configuration of OpenDaylight's aaa-shiro feature. Provided Realm # +# implementations include: # +# - TokenAuthRealm (enabled by default) # +# - ODLJndiLdapRealm (disabled by default) # +# - ODLJndiLdapRealmAuthNOnly (disabled by default) # +# Basic user configuration through shiro.ini is disabled for security # +# purposes. # +############################################################################### + + + +[main] +############################################################################### +# realms # +# # +# This section is dedicated to setting up realms for OpenDaylight. Realms # +# are essentially different methods for providing AAA. ODL strives to provide# +# highly-configurable AAA by providing pluggable infrastructure. By deafult, # +# TokenAuthRealm is enabled out of the box (which bridges to the existing AAA # +# mechanisms). More than one realm can be enabled, and the realms are # +# tried Round-Robin until: # +# 1) a realm successfully authenticates the incoming request # +# 2) all realms are exhausted, and 401 is returned # +############################################################################### + +# ODL provides a few LDAP implementations, which are disabled out of the box. +# ODLJndiLdapRealm includes authorization functionality based on LDAP elements +# extracted through and LDAP search. This requires a bit of knowledge about +# how your LDAP system is setup. An example is provided below: +#ldapRealm = org.opendaylight.aaa.shiro.realm.ODLJndiLdapRealm +#ldapRealm.userDnTemplate = uid={0},ou=People,dc=DOMAIN,dc=TLD +#ldapRealm.contextFactory.url = ldap://<URL>:389 +#ldapRealm.searchBase = dc=DOMAIN,dc=TLD +#ldapRealm.ldapAttributeForComparison = objectClass + +# ODL also provides ODLJndiLdapRealmAuthNOnly. Essentially, this allows +# access through AAAFilter to any user that can authenticate against the +# provided LDAP server. +#ldapRealm = org.opendaylight.aaa.shiro.realm.ODLJndiLdapRealmAuthNOnly +#ldapRealm.userDnTemplate = uid={0},ou=People,dc=DOMAIN,dc=TLD +#ldapRealm.contextFactory.url = ldap://<URL>:389 + +# Bridge to existing h2/idmlight/mdsal authentication/authorization mechanisms. +# This realm is enabled by default, and utilizes h2-store by default. +#tokenAuthRealm = org.opendaylight.aaa.shiro.realm.TokenAuthRealm +# Defining moon realm +moonAuthRealm = org.opendaylight.aaa.shiro.realm.MoonRealm + +# The CSV list of enabled realms. In order to enable a realm, add it to the +# list below: +#securityManager.realms = $tokenAuthRealm +# Configure the Shiro Security Manager to use Moon Realm +securityManager.realms = $moonAuthRealm + +# adds a custom AuthenticationFilter to support OAuth2 for backwards +# compatibility. To disable OAuth2 access, just comment out the next line +# and authcBasic will default to BasicHttpAuthenticationFilter, a +# Shiro-provided class. +authcBasic = org.opendaylight.aaa.shiro.filters.ODLHttpAuthenticationFilter +# OAuth2 Filer for moon token AuthN +rest = org.opendaylight.aaa.shiro.filters.MoonOAuthFilter + +# add in AuthenticationListener, a Listener that records whether +# authentication attempts are successful or unsuccessful. This audit +# information is disabled by default, to avoid log flooding. To enable, +# issue the following in karaf: +# >log:set DEBUG org.opendaylight.aaa.shiro.filters.AuthenticationListener +accountingListener = org.opendaylight.aaa.shiro.filters.AuthenticationListener +securityManager.authenticator.authenticationListeners = $accountingListener + + + +[urls] +############################################################################### +# url authorization section # +# # +# This section is dedicated to defining url-based authorization according to: # +# http://shiro.apache.org/web.html # +############################################################################### + +# Restrict AAA endpoints to users w/ admin role +/v1/users/** = authcBasic +/v1/domains/** = authcBasic +/v1/roles/** = authcBasic + +#Filter OAuth2 request$ +/token = rest + +# General access through AAAFilter requires valid credentials (AuthN only). +/** = authcBasic + +# Access to the credential store is limited to the valid users who have the +# admin role. The following line is only needed if the mdsal store is enabled +#(the mdsal store is disabled by default). +/config/aaa-authn-model** = authcBasic,roles[admin] diff --git a/odl-aaa-moon/aaa/aaa-shiro/src/test/java/org/opendaylight/aaa/shiro/ServiceProxyTest.java b/odl-aaa-moon/aaa/aaa-shiro/src/test/java/org/opendaylight/aaa/shiro/ServiceProxyTest.java new file mode 100644 index 00000000..2d9c8976 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-shiro/src/test/java/org/opendaylight/aaa/shiro/ServiceProxyTest.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2015 Brocade Communications Systems, Inc. and others. 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.shiro; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; +import org.opendaylight.aaa.shiro.filters.AAAFilter; + +/** + * @author Ryan Goulding (ryandgoulding@gmail.com) + */ +public class ServiceProxyTest { + + @Test + public void testGetInstance() { + // ensures that singleton pattern is working + assertNotNull(ServiceProxy.getInstance()); + } + + @Test + public void testGetSetEnabled() { + // combines set and get tests. These are important in this instance, + // because getEnabled allows an optional callback Filter. + ServiceProxy.getInstance().setEnabled(true); + assertTrue(ServiceProxy.getInstance().getEnabled(null)); + + AAAFilter testFilter = new AAAFilter(); + // register the filter + ServiceProxy.getInstance().getEnabled(testFilter); + assertTrue(testFilter.isEnabled()); + + ServiceProxy.getInstance().setEnabled(false); + assertFalse(ServiceProxy.getInstance().getEnabled(testFilter)); + assertFalse(testFilter.isEnabled()); + } +} diff --git a/odl-aaa-moon/aaa/aaa-shiro/src/test/java/org/opendaylight/aaa/shiro/TestAppender.java b/odl-aaa-moon/aaa/aaa-shiro/src/test/java/org/opendaylight/aaa/shiro/TestAppender.java new file mode 100644 index 00000000..ec9375dc --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-shiro/src/test/java/org/opendaylight/aaa/shiro/TestAppender.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2016 Brocade Communications Systems, Inc. and others. 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.shiro; + +import ch.qos.logback.classic.spi.LoggingEvent; +import ch.qos.logback.core.AppenderBase; + +import java.util.List; +import java.util.Vector; + +/** + * A custom slf4j <code>Appender</code> which stores <code>LoggingEvent</code>(s) in memory + * for future retrieval. This is useful from inside test resources. This class is specified + * within <code>logback-test.xml</code>. + * + * @author Ryan Goulding (ryandgoulding@gmail.com) + */ +public class TestAppender extends AppenderBase<LoggingEvent> { + + /** + * stores all log events in memory, instead of file + */ + private List<LoggingEvent> events = new Vector<>(); + + /** + * Since junit maven & junit instantiate the logging appender (as provided + * by logback-test.xml), singleton is not possible. The next best thing is to track the + * current instance so it can be retrieved by Test instances. + */ + private static volatile TestAppender currentInstance; + + /** + * keeps track of the current instance + */ + public TestAppender() { + currentInstance = this; + } + + @Override + protected void append(final LoggingEvent e) { + events.add(e); + } + + /** + * Extract the log. + * + * @return the in-memory representation of <code>LoggingEvent</code>(s) + */ + public List<LoggingEvent> getEvents() { + return events; + } + + /** + * A way to extract the appender from Test instances. + * + * @return <code>this</code> + */ + public static TestAppender getCurrentInstance() { + return currentInstance; + } +} diff --git a/odl-aaa-moon/aaa/aaa-shiro/src/test/java/org/opendaylight/aaa/shiro/authorization/DefaultRBACRulesTest.java b/odl-aaa-moon/aaa/aaa-shiro/src/test/java/org/opendaylight/aaa/shiro/authorization/DefaultRBACRulesTest.java new file mode 100644 index 00000000..38658f0c --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-shiro/src/test/java/org/opendaylight/aaa/shiro/authorization/DefaultRBACRulesTest.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2015 Brocade Communications Systems, Inc. and others. 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.shiro.authorization; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +import com.google.common.collect.Sets; +import java.util.Collection; +import org.junit.Test; + +/** + * A few basic test cases for the DefualtRBACRules singleton container. + * + * @author Ryan Goulding (ryandgoulding@gmail.com) + * + */ +public class DefaultRBACRulesTest { + + @Test + public void testGetInstance() { + assertNotNull(DefaultRBACRules.getInstance()); + assertEquals(DefaultRBACRules.getInstance(), DefaultRBACRules.getInstance()); + } + + @Test + public void testGetRBACRules() { + Collection<RBACRule> rbacRules = DefaultRBACRules.getInstance().getRBACRules(); + assertNotNull(rbacRules); + + // check that a copy was returned + int originalSize = rbacRules.size(); + rbacRules.add(RBACRule.createAuthorizationRule("fakeurl/*", Sets.newHashSet("admin"))); + assertEquals(originalSize, DefaultRBACRules.getInstance().getRBACRules().size()); + } + +} diff --git a/odl-aaa-moon/aaa/aaa-shiro/src/test/java/org/opendaylight/aaa/shiro/authorization/RBACRuleTest.java b/odl-aaa-moon/aaa/aaa-shiro/src/test/java/org/opendaylight/aaa/shiro/authorization/RBACRuleTest.java new file mode 100644 index 00000000..825fe626 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-shiro/src/test/java/org/opendaylight/aaa/shiro/authorization/RBACRuleTest.java @@ -0,0 +1,106 @@ +/* + * Copyright (c) 2015 Brocade Communications Systems, Inc. and others. 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.shiro.authorization; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import com.google.common.collect.Sets; +import java.util.Collection; +import java.util.HashSet; +import org.junit.Test; + +public class RBACRuleTest { + + private static final String BASIC_RBAC_RULE_URL_PATTERN = "/*"; + private static final Collection<String> BASIC_RBAC_RULE_ROLES = Sets.newHashSet("admin"); + private RBACRule basicRBACRule = RBACRule.createAuthorizationRule(BASIC_RBAC_RULE_URL_PATTERN, + BASIC_RBAC_RULE_ROLES); + + private static final String COMPLEX_RBAC_RULE_URL_PATTERN = "/auth/v1/"; + private static final Collection<String> COMPLEX_RBAC_RULE_ROLES = Sets.newHashSet("admin", + "user"); + private RBACRule complexRBACRule = RBACRule.createAuthorizationRule( + COMPLEX_RBAC_RULE_URL_PATTERN, COMPLEX_RBAC_RULE_ROLES); + + @Test + public void testCreateAuthorizationRule() { + // positive test cases + assertNotNull(RBACRule.createAuthorizationRule(BASIC_RBAC_RULE_URL_PATTERN, + BASIC_RBAC_RULE_ROLES)); + assertNotNull(RBACRule.createAuthorizationRule(COMPLEX_RBAC_RULE_URL_PATTERN, + COMPLEX_RBAC_RULE_ROLES)); + + // negative test cases + // both null + assertNull(RBACRule.createAuthorizationRule(null, null)); + + // url pattern is null + assertNull(RBACRule.createAuthorizationRule(null, BASIC_RBAC_RULE_ROLES)); + // url pattern is empty string + assertNull(RBACRule.createAuthorizationRule("", BASIC_RBAC_RULE_ROLES)); + + // roles is null + assertNull(RBACRule.createAuthorizationRule(BASIC_RBAC_RULE_URL_PATTERN, null)); + // roles is empty collection + assertNull(RBACRule.createAuthorizationRule(COMPLEX_RBAC_RULE_URL_PATTERN, + new HashSet<String>())); + } + + @Test + public void testGetUrlPattern() { + assertEquals(BASIC_RBAC_RULE_URL_PATTERN, basicRBACRule.getUrlPattern()); + assertEquals(COMPLEX_RBAC_RULE_URL_PATTERN, complexRBACRule.getUrlPattern()); + } + + @Test + public void testGetRoles() { + assertTrue(BASIC_RBAC_RULE_ROLES.containsAll(basicRBACRule.getRoles())); + basicRBACRule.getRoles().clear(); + // test that getRoles() produces a new object + assertFalse(basicRBACRule.getRoles().isEmpty()); + assertTrue(basicRBACRule.getRoles().containsAll(BASIC_RBAC_RULE_ROLES)); + + assertTrue(COMPLEX_RBAC_RULE_ROLES.containsAll(complexRBACRule.getRoles())); + complexRBACRule.getRoles().add("newRole"); + // test that getRoles() produces a new object + assertFalse(complexRBACRule.getRoles().contains("newRole")); + assertTrue(complexRBACRule.getRoles().containsAll(COMPLEX_RBAC_RULE_ROLES)); + } + + @Test + public void testGetRolesInShiroFormat() { + final String BASIC_RBAC_RULE_EXPECTED_SHIRO_FORMAT = "roles[admin]"; + assertEquals(BASIC_RBAC_RULE_EXPECTED_SHIRO_FORMAT, basicRBACRule.getRolesInShiroFormat()); + + // set ordering is not predictable, so both formats must be considered + final String COMPLEX_RBAC_RULE_EXPECTED_SHIRO_FORMAT_1 = "roles[admin, user]"; + final String COMPLEX_RBAC_RULE_EXPECTED_SHIRO_FORMAT_2 = "roles[user, admin]"; + assertTrue(COMPLEX_RBAC_RULE_EXPECTED_SHIRO_FORMAT_1.equals(complexRBACRule + .getRolesInShiroFormat()) + || COMPLEX_RBAC_RULE_EXPECTED_SHIRO_FORMAT_2.equals(complexRBACRule + .getRolesInShiroFormat())); + } + + @Test + public void testToString() { + final String BASIC_RBAC_RULE_EXPECTED_SHIRO_FORMAT = "/*=roles[admin]"; + assertEquals(BASIC_RBAC_RULE_EXPECTED_SHIRO_FORMAT, basicRBACRule.toString()); + + // set ordering is not predictable,s o both formats must be considered + final String COMPLEX_RBAC_RULE_EXPECTED_SHIRO_FORMAT_1 = "/auth/v1/=roles[admin, user]"; + final String COMPLEX_RBAC_RULE_EXPECTED_SHIRO_FORMAT_2 = "/auth/v1/=roles[user, admin]"; + assertTrue(COMPLEX_RBAC_RULE_EXPECTED_SHIRO_FORMAT_1.equals(complexRBACRule.toString()) + || COMPLEX_RBAC_RULE_EXPECTED_SHIRO_FORMAT_2.equals(complexRBACRule.toString())); + } + +} diff --git a/odl-aaa-moon/aaa/aaa-shiro/src/test/java/org/opendaylight/aaa/shiro/filters/AuthenticationListenerTest.java b/odl-aaa-moon/aaa/aaa-shiro/src/test/java/org/opendaylight/aaa/shiro/filters/AuthenticationListenerTest.java new file mode 100644 index 00000000..1c823525 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-shiro/src/test/java/org/opendaylight/aaa/shiro/filters/AuthenticationListenerTest.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2016 Brocade Communications Systems, Inc. and others. 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.shiro.filters; + +import static org.junit.Assert.*; + +import ch.qos.logback.classic.spi.LoggingEvent; + +import java.util.List; + +import org.apache.shiro.authc.AuthenticationException; +import org.apache.shiro.authc.SimpleAuthenticationInfo; +import org.apache.shiro.authc.UsernamePasswordToken; +import org.junit.Test; +import org.opendaylight.aaa.shiro.TestAppender; +import org.opendaylight.aaa.shiro.filters.AuthenticationListener; + +/** + * Test AuthenticationListener, which is responsible for logging Accounting events. + * + * @author Ryan Goulding (ryandgoulding@gmail.com) + */ +public class AuthenticationListenerTest { + + @Test + public void testOnSuccess() throws Exception { + // sets up a successful authentication attempt + final AuthenticationListener authenticationListener = new AuthenticationListener(); + final UsernamePasswordToken authenticationToken = new UsernamePasswordToken(); + authenticationToken.setUsername("successfulUser1"); + authenticationToken.setHost("successfulHost1"); + final SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(); + // the following call produces accounting output + authenticationListener.onSuccess(authenticationToken, simpleAuthenticationInfo); + + // grab the latest log output and make sure it is in line with what is expected + final List<LoggingEvent> loggingEvents = TestAppender.getCurrentInstance().getEvents(); + // the latest logging event is the one we need to inspect + final int whichLoggingEvent = loggingEvents.size() - 1; + final LoggingEvent latestLoggingEvent = loggingEvents.get(whichLoggingEvent); + final String latestLogMessage = latestLoggingEvent.getMessage(); + assertEquals("Successful authentication attempt by successfulUser1 from successfulHost1", + latestLogMessage); + } + + @Test + public void testOnFailure() throws Exception { + // variables for an unsucessful authentication attempt + final AuthenticationListener authenticationListener = new AuthenticationListener(); + final UsernamePasswordToken authenticationToken = new UsernamePasswordToken(); + authenticationToken.setUsername("unsuccessfulUser1"); + authenticationToken.setHost("unsuccessfulHost1"); + final AuthenticationException authenticationException = + new AuthenticationException("test auth exception"); + // produces unsuccessful authentication attempt output + authenticationListener.onFailure(authenticationToken, authenticationException); + + // grab the latest log output and ensure it is in line with what is expected + final List<LoggingEvent> loggingEvents = TestAppender.getCurrentInstance().getEvents(); + final int whichLoggingEvent = loggingEvents.size() - 1; + final LoggingEvent latestLoggingEvent = loggingEvents.get(whichLoggingEvent); + final String latestLogMessage = latestLoggingEvent.getMessage(); + assertEquals("Unsuccessful authentication attempt by unsuccessfulUser1 from unsuccessfulHost1", + latestLogMessage); + } +}
\ No newline at end of file diff --git a/odl-aaa-moon/aaa/aaa-shiro/src/test/java/org/opendaylight/aaa/shiro/filters/AuthenticationTokenUtilsTest.java b/odl-aaa-moon/aaa/aaa-shiro/src/test/java/org/opendaylight/aaa/shiro/filters/AuthenticationTokenUtilsTest.java new file mode 100644 index 00000000..09331c52 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-shiro/src/test/java/org/opendaylight/aaa/shiro/filters/AuthenticationTokenUtilsTest.java @@ -0,0 +1,124 @@ +/* + * Copyright (c) 2016 Brocade Communications Systems, Inc. and others. 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.shiro.filters; + +import static org.junit.Assert.*; + +import org.apache.shiro.authc.AuthenticationToken; +import org.apache.shiro.authc.UsernamePasswordToken; +import org.junit.Test; +import org.opendaylight.aaa.shiro.filters.AuthenticationTokenUtils; + +/** + * Tests authentication token output utilities. + * + * @author Ryan Goulding (ryandgoulding@gmail.com) + */ +public class AuthenticationTokenUtilsTest { + + /** + * A sample non-UsernamePasswordToken implementation for testing. + */ + private final class NotUsernamePasswordToken implements AuthenticationToken { + + @Override + public Object getPrincipal() { + return null; + } + + @Override + public Object getCredentials() { + return null; + } + } + + @Test + public void testIsUsernamePasswordToken() throws Exception { + // null test + final AuthenticationToken nullUsernamePasswordToken = null; + assertFalse(AuthenticationTokenUtils.isUsernamePasswordToken(nullUsernamePasswordToken)); + + // alternate implementation of AuthenticationToken + final AuthenticationToken notUsernamePasswordToken = new NotUsernamePasswordToken(); + assertFalse(AuthenticationTokenUtils.isUsernamePasswordToken(notUsernamePasswordToken)); + + // positive test case + final AuthenticationToken positiveUsernamePasswordToken = new UsernamePasswordToken(); + assertTrue(AuthenticationTokenUtils.isUsernamePasswordToken(positiveUsernamePasswordToken)); + + } + + @Test + public void testExtractUsername() throws Exception { + // null test + final AuthenticationToken nullAuthenticationToken = null; + assertEquals(AuthenticationTokenUtils.DEFAULT_TOKEN, + AuthenticationTokenUtils.extractUsername(nullAuthenticationToken)); + + // non-UsernamePasswordToken test + final AuthenticationToken notUsernamePasswordToken = new NotUsernamePasswordToken(); + assertEquals(AuthenticationTokenUtils.DEFAULT_TOKEN, + AuthenticationTokenUtils.extractUsername(notUsernamePasswordToken)); + + // null username test + final UsernamePasswordToken nullUsername = new UsernamePasswordToken(); + nullUsername.setUsername(null); + assertEquals(AuthenticationTokenUtils.DEFAULT_USERNAME, + AuthenticationTokenUtils.extractUsername(nullUsername)); + + // positive test + final UsernamePasswordToken positiveUsernamePasswordToken = new UsernamePasswordToken(); + final String testUsername = "testUser1"; + positiveUsernamePasswordToken.setUsername(testUsername); + assertEquals(testUsername, AuthenticationTokenUtils.extractUsername(positiveUsernamePasswordToken)); + } + + @Test + public void testExtractHostname() throws Exception { + // null test + final AuthenticationToken nullAuthenticationToken = null; + assertEquals(AuthenticationTokenUtils.DEFAULT_HOSTNAME, + AuthenticationTokenUtils.extractHostname(nullAuthenticationToken)); + + // non-UsernamePasswordToken test + final AuthenticationToken notUsernamePasswordToken = new NotUsernamePasswordToken(); + assertEquals(AuthenticationTokenUtils.DEFAULT_HOSTNAME, + AuthenticationTokenUtils.extractHostname(notUsernamePasswordToken)); + + // null hostname test + final UsernamePasswordToken nullHostname = new UsernamePasswordToken(); + nullHostname.setHost(null); + assertEquals(AuthenticationTokenUtils.DEFAULT_HOSTNAME, + AuthenticationTokenUtils.extractHostname(nullHostname)); + + // positive test + final UsernamePasswordToken positiveUsernamePasswordToken = new UsernamePasswordToken(); + final String testUsername = "testHostname1"; + positiveUsernamePasswordToken.setHost(testUsername); + assertEquals(testUsername, AuthenticationTokenUtils.extractHostname(positiveUsernamePasswordToken)); + } + + @Test + public void testGenerateUnsuccessfulAuthenticationMessage() throws Exception { + final UsernamePasswordToken unsuccessfulToken = new UsernamePasswordToken(); + unsuccessfulToken.setUsername("unsuccessfulUser1"); + unsuccessfulToken.setHost("unsuccessfulHost1"); + assertEquals("Unsuccessful authentication attempt by unsuccessfulUser1 from unsuccessfulHost1", + AuthenticationTokenUtils.generateUnsuccessfulAuthenticationMessage(unsuccessfulToken)); + } + + @Test + public void testGenerateSuccessfulAuthenticationMessage() throws Exception { + final UsernamePasswordToken successfulToken = new UsernamePasswordToken(); + successfulToken.setUsername("successfulUser1"); + successfulToken.setHost("successfulHost1"); + assertEquals("Successful authentication attempt by successfulUser1 from successfulHost1", + AuthenticationTokenUtils.generateSuccessfulAuthenticationMessage(successfulToken)); + } +} diff --git a/odl-aaa-moon/aaa/aaa-shiro/src/test/java/org/opendaylight/aaa/shiro/realm/ODLJndiLdapRealmTest.java b/odl-aaa-moon/aaa/aaa-shiro/src/test/java/org/opendaylight/aaa/shiro/realm/ODLJndiLdapRealmTest.java new file mode 100644 index 00000000..22ce203f --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-shiro/src/test/java/org/opendaylight/aaa/shiro/realm/ODLJndiLdapRealmTest.java @@ -0,0 +1,246 @@ +/* + * Copyright (c) 2015 Brocade Communications Systems, Inc. and others. 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.shiro.realm; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Set; +import java.util.Vector; +import javax.naming.NamingEnumeration; +import javax.naming.NamingException; +import javax.naming.directory.BasicAttributes; +import javax.naming.directory.SearchControls; +import javax.naming.directory.SearchResult; +import javax.naming.ldap.LdapContext; +import org.apache.shiro.authc.AuthenticationToken; +import org.apache.shiro.authc.UsernamePasswordToken; +import org.apache.shiro.authz.AuthorizationInfo; +import org.apache.shiro.realm.ldap.LdapContextFactory; +import org.apache.shiro.subject.PrincipalCollection; +import org.junit.Test; + +/** + * @author Ryan Goulding (ryandgoulding@gmail.com) + */ +public class ODLJndiLdapRealmTest { + + /** + * throw-away anonymous test class + */ + class TestNamingEnumeration implements NamingEnumeration<SearchResult> { + + /** + * state variable + */ + boolean first = true; + + /** + * returned the first time <code>next()</code> or + * <code>nextElement()</code> is called. + */ + SearchResult searchResult = new SearchResult("testuser", null, new BasicAttributes( + "objectClass", "engineering")); + + /** + * returns true the first time, then false for subsequent calls + */ + @Override + public boolean hasMoreElements() { + return first; + } + + /** + * returns <code>searchResult</code> then null for subsequent calls + */ + @Override + public SearchResult nextElement() { + if (first) { + first = false; + return searchResult; + } + return null; + } + + /** + * does nothing because close() doesn't require any special behavior + */ + @Override + public void close() throws NamingException { + } + + /** + * returns true the first time, then false for subsequent calls + */ + @Override + public boolean hasMore() throws NamingException { + return first; + } + + /** + * returns <code>searchResult</code> then null for subsequent calls + */ + @Override + public SearchResult next() throws NamingException { + if (first) { + first = false; + return searchResult; + } + return null; + } + }; + + /** + * throw away test class + * + * @author ryan + */ + class TestPrincipalCollection implements PrincipalCollection { + /** + * + */ + private static final long serialVersionUID = -1236759619455574475L; + + Vector<String> collection = new Vector<String>(); + + public TestPrincipalCollection(String element) { + collection.add(element); + } + + @Override + public Iterator<String> iterator() { + return collection.iterator(); + } + + @Override + public List<String> asList() { + return collection; + } + + @Override + public Set<String> asSet() { + HashSet<String> set = new HashSet<String>(); + set.addAll(collection); + return set; + } + + @Override + public <T> Collection<T> byType(Class<T> arg0) { + return null; + } + + @Override + public Collection<String> fromRealm(String arg0) { + return collection; + } + + @Override + public Object getPrimaryPrincipal() { + return collection.firstElement(); + } + + @Override + public Set<String> getRealmNames() { + return null; + } + + @Override + public boolean isEmpty() { + return collection.isEmpty(); + } + + @Override + public <T> T oneByType(Class<T> arg0) { + // TODO Auto-generated method stub + return null; + } + }; + + @Test + public void testGetUsernameAuthenticationToken() { + AuthenticationToken authenticationToken = null; + assertNull(ODLJndiLdapRealm.getUsername(authenticationToken)); + AuthenticationToken validAuthenticationToken = new UsernamePasswordToken("test", + "testpassword"); + assertEquals("test", ODLJndiLdapRealm.getUsername(validAuthenticationToken)); + } + + @Test + public void testGetUsernamePrincipalCollection() { + PrincipalCollection pc = null; + assertNull(new ODLJndiLdapRealm().getUsername(pc)); + TestPrincipalCollection tpc = new TestPrincipalCollection("testuser"); + String username = new ODLJndiLdapRealm().getUsername(tpc); + assertEquals("testuser", username); + } + + @Test + public void testQueryForAuthorizationInfoPrincipalCollectionLdapContextFactory() + throws NamingException { + LdapContext ldapContext = mock(LdapContext.class); + // emulates an ldap search and returns the mocked up test class + when( + ldapContext.search((String) any(), (String) any(), + (SearchControls) any())).thenReturn(new TestNamingEnumeration()); + LdapContextFactory ldapContextFactory = mock(LdapContextFactory.class); + when(ldapContextFactory.getSystemLdapContext()).thenReturn(ldapContext); + AuthorizationInfo authorizationInfo = new ODLJndiLdapRealm().queryForAuthorizationInfo( + new TestPrincipalCollection("testuser"), ldapContextFactory); + assertNotNull(authorizationInfo); + assertFalse(authorizationInfo.getRoles().isEmpty()); + assertTrue(authorizationInfo.getRoles().contains("engineering")); + } + + @Test + public void testBuildAuthorizationInfo() { + assertNull(ODLJndiLdapRealm.buildAuthorizationInfo(null)); + Set<String> roleNames = new HashSet<String>(); + roleNames.add("engineering"); + AuthorizationInfo authorizationInfo = ODLJndiLdapRealm.buildAuthorizationInfo(roleNames); + assertNotNull(authorizationInfo); + assertFalse(authorizationInfo.getRoles().isEmpty()); + assertTrue(authorizationInfo.getRoles().contains("engineering")); + } + + @Test + public void testGetRoleNamesForUser() throws NamingException { + ODLJndiLdapRealm ldapRealm = new ODLJndiLdapRealm(); + LdapContext ldapContext = mock(LdapContext.class); + + // emulates an ldap search and returns the mocked up test class + when( + ldapContext.search((String) any(), (String) any(), + (SearchControls) any())).thenReturn(new TestNamingEnumeration()); + + // extracts the roles for "testuser" and ensures engineering is returned + Set<String> roles = ldapRealm.getRoleNamesForUser("testuser", ldapContext); + assertFalse(roles.isEmpty()); + assertTrue(roles.iterator().next().equals("engineering")); + } + + @Test + public void testCreateSearchControls() { + SearchControls searchControls = ODLJndiLdapRealm.createSearchControls(); + assertNotNull(searchControls); + int expectedSearchScope = SearchControls.SUBTREE_SCOPE; + int actualSearchScope = searchControls.getSearchScope(); + assertEquals(expectedSearchScope, actualSearchScope); + } + +} diff --git a/odl-aaa-moon/aaa/aaa-shiro/src/test/java/org/opendaylight/aaa/shiro/realm/TokenAuthRealmTest.java b/odl-aaa-moon/aaa/aaa-shiro/src/test/java/org/opendaylight/aaa/shiro/realm/TokenAuthRealmTest.java new file mode 100644 index 00000000..f2eb92b5 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-shiro/src/test/java/org/opendaylight/aaa/shiro/realm/TokenAuthRealmTest.java @@ -0,0 +1,139 @@ +/* + * Copyright (c) 2015 Brocade Communications Systems, Inc. and others. 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.shiro.realm; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.google.common.collect.Lists; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.apache.shiro.authc.AuthenticationToken; +import org.junit.Test; + +/** + * + * @author Ryan Goulding (ryandgoulding@gmail.com) + * + */ +public class TokenAuthRealmTest extends TokenAuthRealm { + + private TokenAuthRealm testRealm = new TokenAuthRealm(); + + @Test + public void testTokenAuthRealm() { + assertEquals("TokenAuthRealm", testRealm.getName()); + } + + @Test(expected = NullPointerException.class) + public void testDoGetAuthorizationInfoPrincipalCollectionNullCacheToken() { + testRealm.doGetAuthorizationInfo(null); + } + + @Test + public void testGetUsernamePasswordDomainString() { + final String username = "user"; + final String password = "password"; + final String domain = "domain"; + final String expectedUsernamePasswordString = "user:password:domain"; + assertEquals(expectedUsernamePasswordString, getUsernamePasswordDomainString(username, password, domain)); + } + + @Test + public void testGetEncodedToken() { + final String stringToEncode = "admin1:admin1"; + final byte[] bytesToEncode = stringToEncode.getBytes(); + final String expectedToken = org.apache.shiro.codec.Base64.encodeToString(bytesToEncode); + assertEquals(expectedToken, getEncodedToken(stringToEncode)); + } + + @Test + public void testGetTokenAuthHeader() { + final String encodedCredentials = getEncodedToken(getUsernamePasswordDomainString("user1", + "password", "sdn")); + final String expectedTokenAuthHeader = "Basic " + encodedCredentials; + assertEquals(expectedTokenAuthHeader, getTokenAuthHeader(encodedCredentials)); + } + + @Test + public void testFormHeadersWithToken() { + final String authHeader = getEncodedToken(getTokenAuthHeader(getUsernamePasswordDomainString( + "user1", "password", "sdn"))); + final Map<String, List<String>> expectedHeaders = new HashMap<String, List<String>>(); + expectedHeaders.put("Authorization", Lists.newArrayList(authHeader)); + final Map<String, List<String>> actualHeaders = formHeadersWithToken(authHeader); + List<String> value; + for (String key : expectedHeaders.keySet()) { + value = expectedHeaders.get(key); + assertTrue(actualHeaders.get(key).equals(value)); + } + } + + @Test + public void testFormHeaders() { + final String username = "basicUser"; + final String password = "basicPassword"; + final String domain = "basicDomain"; + final String authHeader = getTokenAuthHeader(getEncodedToken(getUsernamePasswordDomainString( + username, password, domain))); + final Map<String, List<String>> expectedHeaders = new HashMap<String, List<String>>(); + expectedHeaders.put("Authorization", Lists.newArrayList(authHeader)); + final Map<String, List<String>> actualHeaders = formHeaders(username, password, domain); + List<String> value; + for (String key : expectedHeaders.keySet()) { + value = expectedHeaders.get(key); + assertTrue(actualHeaders.get(key).equals(value)); + } + } + + @Test + public void testIsTokenAuthAvailable() { + assertFalse(testRealm.isTokenAuthAvailable()); + } + + @Test(expected = org.apache.shiro.authc.AuthenticationException.class) + public void testDoGetAuthenticationInfoAuthenticationToken() { + testRealm.doGetAuthenticationInfo(null); + } + + @Test + public void testExtractUsernameNullUsername() { + AuthenticationToken at = mock(AuthenticationToken.class); + when(at.getPrincipal()).thenReturn(null); + assertNull(extractUsername(at)); + } + + @Test(expected = ClassCastException.class) + public void testExtractPasswordNullPassword() { + AuthenticationToken at = mock(AuthenticationToken.class); + when(at.getPrincipal()).thenReturn("username"); + when(at.getCredentials()).thenReturn(null); + extractPassword(at); + } + + @Test(expected = ClassCastException.class) + public void testExtractUsernameBadUsernameClass() { + AuthenticationToken at = mock(AuthenticationToken.class); + when(at.getPrincipal()).thenReturn(new Integer(1)); + extractUsername(at); + } + + @Test(expected = ClassCastException.class) + public void testExtractPasswordBadPasswordClass() { + AuthenticationToken at = mock(AuthenticationToken.class); + when(at.getPrincipal()).thenReturn("username"); + when(at.getCredentials()).thenReturn(new Integer(1)); + extractPassword(at); + } +} diff --git a/odl-aaa-moon/aaa/aaa-shiro/src/test/java/org/opendaylight/aaa/shiro/web/env/KarafIniWebEnvironmentTest.java b/odl-aaa-moon/aaa/aaa-shiro/src/test/java/org/opendaylight/aaa/shiro/web/env/KarafIniWebEnvironmentTest.java new file mode 100644 index 00000000..141d0ce5 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-shiro/src/test/java/org/opendaylight/aaa/shiro/web/env/KarafIniWebEnvironmentTest.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2015 Brocade Communications Systems, Inc. and others. 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.shiro.web.env; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import org.apache.shiro.config.Ini; +import org.apache.shiro.config.Ini.Section; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; + +/** + * @author Ryan Goulding (ryandgoulding@gmail.com) + */ +public class KarafIniWebEnvironmentTest { + private static File iniFile; + + @BeforeClass + public static void setup() throws IOException { + iniFile = createShiroIniFile(); + assertTrue(iniFile.exists()); + } + + @AfterClass + public static void teardown() { + iniFile.delete(); + } + + private static String createFakeShiroIniContents() { + return "[users]\n" + "admin=admin, ROLE_ADMIN \n" + "[roles]\n" + "ROLE_ADMIN = *\n" + + "[urls]\n" + "/** = authcBasic"; + } + + private static File createShiroIniFile() throws IOException { + File shiroIni = File.createTempFile("shiro", "ini"); + FileWriter writer = new FileWriter(shiroIni); + writer.write(createFakeShiroIniContents()); + writer.flush(); + writer.close(); + return shiroIni; + } + + @Test + public void testCreateShiroIni() throws IOException { + Ini ini = KarafIniWebEnvironment.createShiroIni(iniFile.getAbsolutePath()); + assertNotNull(ini); + assertNotNull(ini.getSection("users")); + assertNotNull(ini.getSection("roles")); + assertNotNull(ini.getSection("urls")); + Section usersSection = ini.getSection("users"); + assertTrue(usersSection.containsKey("admin")); + assertTrue(usersSection.get("admin").contains("admin")); + assertTrue(usersSection.get("admin").contains("ROLE_ADMIN")); + } + + @Test + public void testCreateFileBasedIniPath() { + String testPath = "/shiro.ini"; + String expectedFileBasedIniPath = KarafIniWebEnvironment.SHIRO_FILE_PREFIX + testPath; + String actualFileBasedIniPath = KarafIniWebEnvironment.createFileBasedIniPath(testPath); + assertEquals(expectedFileBasedIniPath, actualFileBasedIniPath); + } + +} diff --git a/odl-aaa-moon/aaa/aaa-shiro/src/test/resources/logback-test.xml b/odl-aaa-moon/aaa/aaa-shiro/src/test/resources/logback-test.xml new file mode 100644 index 00000000..68ceeabc --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-shiro/src/test/resources/logback-test.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="UTF-8"?> +<configuration> + + <appender name="TEST-APPENDER" class="org.opendaylight.aaa.shiro.TestAppender"> + <layout class="ch.qos.logback.classic.PatternLayout"> + <Pattern> + %d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n + </Pattern> + </layout> + </appender> + + <logger name="org.opendaylight.aaa.shiro.authc" level="debug" + additivity="false"> + <appender-ref ref="TEST-APPENDER" /> + </logger> + + <root level="debug"> + <appender-ref ref="TEST-APPENDER" /> + </root> + +</configuration> diff --git a/odl-aaa-moon/aaa/artifacts/pom.xml b/odl-aaa-moon/aaa/artifacts/pom.xml new file mode 100644 index 00000000..3f811507 --- /dev/null +++ b/odl-aaa-moon/aaa/artifacts/pom.xml @@ -0,0 +1,231 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- vi: set et smarttab sw=4 tabstop=4: --> +<!-- + Copyright (c) 2013 Robert Varga. 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 +--> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + + <parent> + <groupId>org.opendaylight.odlparent</groupId> + <artifactId>odlparent-lite</artifactId> + <version>1.6.2-Beryllium-SR2</version> + <relativePath/> + </parent> + + <groupId>org.opendaylight.aaa</groupId> + <artifactId>aaa-artifacts</artifactId> + <version>0.3.2-Beryllium-SR2</version> + <packaging>pom</packaging> + + <dependencyManagement> + <dependencies> + <dependency> + <groupId>${project.groupId}</groupId> + <artifactId>aaa-authn</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>${project.groupId}</groupId> + <artifactId>aaa-authn</artifactId> + <version>${project.version}</version> + <type>cfg</type> + <classifier>config</classifier> + </dependency> + <dependency> + <groupId>${project.groupId}</groupId> + <artifactId>aaa-authn-api</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>${project.groupId}</groupId> + <artifactId>aaa-authn-basic</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>${project.groupId}</groupId> + <artifactId>aaa-authn-federation</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>${project.groupId}</groupId> + <artifactId>aaa-authn-federation</artifactId> + <version>${project.version}</version> + <type>cfg</type> + <classifier>config</classifier> + </dependency> + <dependency> + <groupId>${project.groupId}</groupId> + <artifactId>aaa-authn-keystone</artifactId> + <version>${project.version}</version> + </dependency> + + <dependency> + <groupId>${project.groupId}</groupId> + <artifactId>aaa-authn-mdsal-api</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>${project.groupId}</groupId> + <artifactId>aaa-authn-mdsal-store-impl</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>${project.groupId}</groupId> + <artifactId>aaa-authn-mdsal-config</artifactId> + <version>${project.version}</version> + <type>xml</type> + <classifier>config</classifier> + </dependency> + <dependency> + <groupId>${project.groupId}</groupId> + <artifactId>aaa-shiro</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>${project.groupId}</groupId> + <artifactId>aaa-shiro-act</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>${project.groupId}</groupId> + <artifactId>aaa-authn-sssd</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>${project.groupId}</groupId> + <artifactId>aaa-authn-store</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>${project.groupId}</groupId> + <artifactId>aaa-authn-store</artifactId> + <version>${project.version}</version> + <type>cfg</type> + <classifier>config</classifier> + </dependency> + <dependency> + <groupId>${project.groupId}</groupId> + <artifactId>aaa-authn-sts</artifactId> + <version>${project.version}</version> + </dependency> + + <dependency> + <groupId>${project.groupId}</groupId> + <artifactId>aaa-authz-model</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>${project.groupId}</groupId> + <artifactId>aaa-authz-service</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>${project.groupId}</groupId> + <artifactId>authz-service-config</artifactId> + <version>${project.version}</version> + <type>xml</type> + <classifier>config</classifier> + </dependency> + <dependency> + <groupId>${project.groupId}</groupId> + <artifactId>authz-restconf-config</artifactId> + <version>${project.version}</version> + <type>xml</type> + <classifier>config</classifier> + </dependency> + + <dependency> + <groupId>${project.groupId}</groupId> + <artifactId>aaa-credential-store-api</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>${project.groupId}</groupId> + <artifactId>aaa-idmlight</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>${project.groupId}</groupId> + <artifactId>aaa-idmlight</artifactId> + <version>${project.version}</version> + <type>xml</type> + <classifier>config</classifier> + </dependency> + <dependency> + <groupId>${project.groupId}</groupId> + <artifactId>aaa-authn-idpmapping</artifactId> + <version>${project.version}</version> + </dependency> + + <dependency> + <groupId>${project.groupId}</groupId> + <artifactId>features-aaa-api</artifactId> + <version>${project.version}</version> + <classifier>features</classifier> + <type>xml</type> + </dependency> + <dependency> + <groupId>${project.groupId}</groupId> + <artifactId>features-aaa-authn</artifactId> + <version>${project.version}</version> + <classifier>features</classifier> + <type>xml</type> + </dependency> + <dependency> + <groupId>${project.groupId}</groupId> + <artifactId>features-aaa-authz</artifactId> + <version>${project.version}</version> + <classifier>features</classifier> + <type>xml</type> + </dependency> + <dependency> + <groupId>${project.groupId}</groupId> + <artifactId>aaa-h2-store</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>${project.groupId}</groupId> + <artifactId>aaa-h2-store</artifactId> + <version>${project.version}</version> + <classifier>config</classifier> + <type>xml</type> + </dependency> + <dependency> + <groupId>${project.groupId}</groupId> + <artifactId>features-aaa-shiro</artifactId> + <version>${project.version}</version> + <classifier>features</classifier> + <type>xml</type> + </dependency> + <dependency> + <groupId>${project.groupId}</groupId> + <artifactId>features-aaa</artifactId> + <version>${project.version}</version> + <classifier>features</classifier> + <type>xml</type> + </dependency> + </dependencies> + </dependencyManagement> + + <properties> + <nexusproxy>http://nexus.opendaylight.org/content</nexusproxy> + </properties> + + <distributionManagement> + <!-- OpenDayLight Released artifact --> + <repository> + <id>opendaylight-release</id> + <url>${nexusproxy}/repositories/opendaylight.release/</url> + </repository> + <!-- OpenDayLight Snapshot artifact --> + <snapshotRepository> + <id>opendaylight-snapshot</id> + <url>${nexusproxy}/repositories/opendaylight.snapshot/</url> + </snapshotRepository> + </distributionManagement> +</project> diff --git a/odl-aaa-moon/aaa/commons/docs/AuthNusecases.vsd b/odl-aaa-moon/aaa/commons/docs/AuthNusecases.vsd Binary files differnew file mode 100644 index 00000000..ddd59fb3 --- /dev/null +++ b/odl-aaa-moon/aaa/commons/docs/AuthNusecases.vsd diff --git a/odl-aaa-moon/aaa/commons/docs/direct_authn.png b/odl-aaa-moon/aaa/commons/docs/direct_authn.png Binary files differnew file mode 100644 index 00000000..f63f038e --- /dev/null +++ b/odl-aaa-moon/aaa/commons/docs/direct_authn.png diff --git a/odl-aaa-moon/aaa/commons/docs/federated_authn1.png b/odl-aaa-moon/aaa/commons/docs/federated_authn1.png Binary files differnew file mode 100644 index 00000000..199f6f4d --- /dev/null +++ b/odl-aaa-moon/aaa/commons/docs/federated_authn1.png diff --git a/odl-aaa-moon/aaa/commons/docs/federated_authn2.png b/odl-aaa-moon/aaa/commons/docs/federated_authn2.png Binary files differnew file mode 100644 index 00000000..b71e9aa7 --- /dev/null +++ b/odl-aaa-moon/aaa/commons/docs/federated_authn2.png diff --git a/odl-aaa-moon/aaa/commons/federation/README b/odl-aaa-moon/aaa/commons/federation/README new file mode 100644 index 00000000..dd9cdbf0 --- /dev/null +++ b/odl-aaa-moon/aaa/commons/federation/README @@ -0,0 +1,271 @@ +README +=============================================================================== +Federated AAA is deployed using several config files. This file explains a +simple scenario utilizing two servers: +a) ipa.example.com + - Runs the IPA Server Software +b) odl.example.com + - Runs the IPA Client Software + - Runs an Apache proxy frontend (AuthN through mod_lookup_identity.so) + - Runs ODL + +This setup for this scenario is illustrated in Figure 1 below: + + ----------------------- + | odl.example.com | + | (Fedora 20 Linux) | + | | + | ------------------- | + | | ODL Jetty Server | | + | | (Port 8181 & 8383)| | + | ------------------- | + | ^ . | + | . (Apache . | SSSD Requests/Responses + | . Reverse . | / + | . Proxy) . | / + | . v | / + | ------------------- | | ------------------ + | | Apache |<|..................| ipa.example.com | + | | (Port 80) |.|.................>| (FreeIPA | + | ------------------- | | Kerberos And | + | ______________________| | LDAP) | + ------------------ +Figure 1: Shows the setup for a simple Federated AAA use case utilizing +FreeIPA as an identity provider. + + +These instructions were written for Fedora 20, since SSSD is unique to RHEL based +distributions. SSSD is NOT a requirement for Federation though; you can use +any supported linux flavor. At this time, SSSD is the only Filter available +with regards to capturing IdP attributes that can be used in making advanced mapping +decisions (such as IdP group membership information). + + + +1) Install FreeIPA Server on ipa.example.com. This is achieved through running: +# yum install freeipa-server bind bind-dyndb-ldap +# ipa-server-intall + + + +2) Add a FreeIPA user called testuser: +$ kinit admin@EXAMPLE.COM +$ ipa group-add odl_users --desc "ODL Users" +$ ipa group-add odl_admin --desc "ODL Admin" +$ ipa user-add testuser --first Test --last USER --email test.user@example.com +$ ipa group-add-member odl_users --user testuser +$ ipa group-add-member odl_admin --user testuser + + + +3) Install FreeIPA Client on odl.example.com. This is achieved through running: +# yum install freeipa-client +# ipa-client-install + + + +4) Set up Client keytab for HTTP access on odl.example.com: +# ipa-getkeytab -p HTTP/odl.brcd-sssd-tb.com@BRCD-SSSD-TB.COM \ + -s freeipa.brcd-sssd-tb.com -k /etc/krb5.keytab +# chmod 644 /etc/krb5.keytab +NOTE: The second command allows Apache to read the keytab. There are more +secure methods to support such access through SELINUX, but they are outside +the scope of this tutorial. + + + +5) Install Apache on odl.example.com. This is achieved through running: +# yum install httpd + + + +6) Create an Apache application to broker federation between ODL and FreeIPA. +Create the following file on odl.example.com: + +[root@odl /]# cat /etc/httpd/conf.d/my_app.conf +<Location "/*"> + AuthType Kerberos + AuthName "Kerberos Login" + KrbMethodNegotiate On + KrbMethodK5Passwd on + KrbAuthRealms EXAMPLE.COM + Krb5KeyTab /etc/krb5.keytab + require valid-user +</Location> + + +<LocationMatch "/*"> + + RequestHeader set X-SSSD-REMOTE_USER expr=%{REMOTE_USER} + RequestHeader set X-SSSD-AUTH_TYPE expr=%{AUTH_TYPE} + RequestHeader set X-SSSD-REMOTE_HOST expr=%{REMOTE_HOST} + RequestHeader set X-SSSD-REMOTE_ADDR expr=%{REMOTE_ADDR} + LookupUserAttr mail REMOTE_USER_EMAIL + RequestHeader set X-SSSD-REMOTE_USER_EMAIL %{REMOTE_USER_EMAIL}e + LookupUserAttr givenname REMOTE_USER_FIRSTNAME + RequestHeader set X-SSSD-REMOTE_USER_FIRSTNAME %{REMOTE_USER_FIRSTNAME}e + LookupUserAttr sn REMOTE_USER_LASTNAME + RequestHeader set X-SSSD-REMOTE_USER_LASTNAME %{REMOTE_USER_LASTNAME}e + LookupUserGroups REMOTE_USER_GROUPS ":" + RequestHeader set X-SSSD-REMOTE_USER_GROUPS %{REMOTE_USER_GROUPS}e +</LocationMatch> + +ProxyPass / http://localhost:8383/ +ProxyPassReverse / http://localhost:8383/ + + + +7) Install the ODL distribution in the /opt folder on odl.example.com. + + + +8) Add a federation connector to the jetty server hosting ODL on +odl.example.com: + +[user@odl distribution]$ cat etc/jetty.xml +<?xml version="1.0"?> +<!DOCTYPE Configure PUBLIC "-//Mort Bay Consulting// +DTD Configure//EN" "http://jetty.mortbay.org/configure.dtd"> + +<Configure class="org.eclipse.jetty.server.Server"> + + <!-- =========================================================== --> + <!-- Set connectors --> + <!-- =========================================================== --> + <!-- One of each type! --> + <!-- =========================================================== --> + + <!-- Use this connector for many frequently idle connections and for + threadless continuations. --> + <Call name="addConnector"> + <Arg> + <New class="org.eclipse.jetty.server.nio.SelectChannelConnector"> + <Set name="host"> + <Property name="jetty.host" /> + </Set> + <Set name="port"> + <Property name="jetty.port" default="8181" /> + </Set> + <Set name="maxIdleTime">300000</Set> + <Set name="Acceptors">2</Set> + <Set name="statsOn">false</Set> + <Set name="confidentialPort">8443</Set> + <Set name="lowResourcesConnections">20000</Set> + <Set name="lowResourcesMaxIdleTime">5000</Set> + </New> + </Arg> + </Call> + <!-- Trusted Authentication Federation proxy connection --> + <Call name="addConnector"> + <Arg> + <New class="org.eclipse.jetty.server.nio.SelectChannelConnector"> + <Set name="host">127.0.0.1</Set> + <Set name="port">8383</Set> + <Set name="maxIdleTime">300000</Set> + <Set name="Acceptors">2</Set> + <Set name="statsOn">false</Set> + <Set name="confidentialPort">8445</Set> + <Set name="name">federationConn</Set> + <Set name="lowResourcesConnections">20000</Set> + <Set name="lowResourcesMaxIdleTime">5000</Set> + </New> + </Arg> + </Call> + <!-- =========================================================== --> + <!-- Configure Authentication Realms --> + <!-- Realms may be configured for the entire server here, or --> + <!-- they can be configured for a specific web app in a context --> + <!-- configuration (see $(jetty.home)/contexts/test.xml for an --> + <!-- example). --> + <!-- =========================================================== --> + <Call name="addBean"> + <Arg> + <New class="org.eclipse.jetty.plus.jaas.JAASLoginService"> + <Set name="name">karaf</Set> + <Set name="loginModuleName">karaf</Set> + <Set name="roleClassNames"> + <Array type="java.lang.String"> + <Item>org.apache.karaf.jaas.boot.principal.RolePrincipal + </Item> + </Array> + </Set> + </New> + </Arg> + </Call> + <Call name="addBean"> + <Arg> + <New class="org.eclipse.jetty.plus.jaas.JAASLoginService"> + <Set name="name">default</Set> + <Set name="loginModuleName">karaf</Set> + <Set name="roleClassNames"> + <Array type="java.lang.String"> + <Item>org.apache.karaf.jaas.boot.principal.RolePrincipal + </Item> + </Array> + </Set> + </New> + </Arg> + </Call> +</Configure> + + + +9) Add the idp_mapping rules file on odl.example.com + +[user@odl distribution]$ cat etc/idp_mapping_rules.json +[ + { + "mapping":{ + "ClientId":"1", + "UserId":"1", + "User":"admin", + "Domain":"BRCD-SSSD-TB.COM", + "roles":"$roles" + }, + "statement_blocks":[ + [ + [ + "set", + "$groups", + [ + + ] + ], + [ + "set", + "$roles", + [ + "admin", + "user" + ] + ] + ] + ] + } +] + +NOTE: This is a very basic mapping example in which all federated users are +mapped into the default "admin" account. + + + +10) Start ODL and install the following features on odl.example.com: +# bin/karaf +karaf> feature:install odl-aaa-authn-sssd-no-cluster odl-restconf + + + +11) Get a refresh_token on odl.example.com through Apache proxy port (80 forwarded to 8383): +[user@odl distribution]$ kinit testuser +[user@odl distribution]$ curl -s --negotiate -u : -X POST http://odl.example.com/oauth2/federation/ + + + +12) Obtain an access_token on odl.example.com through normal port (8181): +[user@odl distribution]$ curl -s -d 'grant_type=refresh_token&refresh_token=<PUT RESULT FROM ABOVE STEP HERE>&scope=sdn' http://odl.example.com:8181/oauth2/token + + + +13) Use the access_token to make authenticated rest calls from odl.example.com through normal port (8181): +[user@odl distribution]$ curl -s -H 'Authorization: Bearer <PUT RESULT FROM ABOVE STEP HERE>' http://odl.brcd-sssd-tb.com:8181/restconf/streams/ + diff --git a/odl-aaa-moon/aaa/commons/federation/idp_mapping_rules.json.example b/odl-aaa-moon/aaa/commons/federation/idp_mapping_rules.json.example new file mode 100644 index 00000000..98bacb0a --- /dev/null +++ b/odl-aaa-moon/aaa/commons/federation/idp_mapping_rules.json.example @@ -0,0 +1,30 @@ +[ + { + "mapping":{ + "ClientId":"1", + "UserId":"1", + "User":"admin", + "Domain":"BRCD-SSSD-TB.COM", + "roles":"$roles" + }, + "statement_blocks":[ + [ + [ + "set", + "$groups", + [ + + ] + ], + [ + "set", + "$roles", + [ + "admin", + "user" + ] + ] + ] + ] + } +] diff --git a/odl-aaa-moon/aaa/commons/federation/jetty.xml.example b/odl-aaa-moon/aaa/commons/federation/jetty.xml.example new file mode 100644 index 00000000..c4cb2a7d --- /dev/null +++ b/odl-aaa-moon/aaa/commons/federation/jetty.xml.example @@ -0,0 +1,85 @@ +<?xml version="1.0"?> +<!DOCTYPE Configure PUBLIC "-//Mort Bay Consulting// +DTD Configure//EN" "http://jetty.mortbay.org/configure.dtd"> + +<Configure class="org.eclipse.jetty.server.Server"> + + <!-- =========================================================== --> + <!-- Set connectors --> + <!-- =========================================================== --> + <!-- One of each type! --> + <!-- =========================================================== --> + + <!-- Use this connector for many frequently idle connections and for + threadless continuations. --> + <Call name="addConnector"> + <Arg> + <New class="org.eclipse.jetty.server.nio.SelectChannelConnector"> + <Set name="host"> + <Property name="jetty.host" /> + </Set> + <Set name="port"> + <Property name="jetty.port" default="8181" /> + </Set> + <Set name="maxIdleTime">300000</Set> + <Set name="Acceptors">2</Set> + <Set name="statsOn">false</Set> + <Set name="confidentialPort">8443</Set> + <Set name="lowResourcesConnections">20000</Set> + <Set name="lowResourcesMaxIdleTime">5000</Set> + </New> + </Arg> + </Call> + <!-- Trusted Authentication Federation proxy connection --> + <Call name="addConnector"> + <Arg> + <New class="org.eclipse.jetty.server.nio.SelectChannelConnector"> + <Set name="host">127.0.0.1</Set> + <Set name="port">8383</Set> + <Set name="maxIdleTime">300000</Set> + <Set name="Acceptors">2</Set> + <Set name="statsOn">false</Set> + <Set name="confidentialPort">8445</Set> + <Set name="name">federationConn</Set> + <Set name="lowResourcesConnections">20000</Set> + <Set name="lowResourcesMaxIdleTime">5000</Set> + </New> + </Arg> + </Call> + <!-- =========================================================== --> + <!-- Configure Authentication Realms --> + <!-- Realms may be configured for the entire server here, or --> + <!-- they can be configured for a specific web app in a context --> + <!-- configuration (see $(jetty.home)/contexts/test.xml for an --> + <!-- example). --> + <!-- =========================================================== --> + <Call name="addBean"> + <Arg> + <New class="org.eclipse.jetty.plus.jaas.JAASLoginService"> + <Set name="name">karaf</Set> + <Set name="loginModuleName">karaf</Set> + <Set name="roleClassNames"> + <Array type="java.lang.String"> + <Item>org.apache.karaf.jaas.boot.principal.RolePrincipal + </Item> + </Array> + </Set> + </New> + </Arg> + </Call> + <Call name="addBean"> + <Arg> + <New class="org.eclipse.jetty.plus.jaas.JAASLoginService"> + <Set name="name">default</Set> + <Set name="loginModuleName">karaf</Set> + <Set name="roleClassNames"> + <Array type="java.lang.String"> + <Item>org.apache.karaf.jaas.boot.principal.RolePrincipal + </Item> + </Array> + </Set> + </New> + </Arg> + </Call> +</Configure> + diff --git a/odl-aaa-moon/aaa/commons/federation/my_app.conf.example b/odl-aaa-moon/aaa/commons/federation/my_app.conf.example new file mode 100644 index 00000000..71c8ad87 --- /dev/null +++ b/odl-aaa-moon/aaa/commons/federation/my_app.conf.example @@ -0,0 +1,31 @@ +LoadModule lookup_identity_module modules/mod_lookup_identity.so + +<Location "/*"> + AuthType Kerberos + AuthName "Kerberos Login" + KrbMethodNegotiate On + KrbMethodK5Passwd on + KrbAuthRealms EXAMPLE.COM + Krb5KeyTab /etc/krb5.keytab + require valid-user +</Location> + + +<LocationMatch "/*"> + + RequestHeader set X-SSSD-REMOTE_USER expr=%{REMOTE_USER} + RequestHeader set X-SSSD-AUTH_TYPE expr=%{AUTH_TYPE} + RequestHeader set X-SSSD-REMOTE_HOST expr=%{REMOTE_HOST} + RequestHeader set X-SSSD-REMOTE_ADDR expr=%{REMOTE_ADDR} + LookupUserAttr mail REMOTE_USER_EMAIL + RequestHeader set X-SSSD-REMOTE_USER_EMAIL %{REMOTE_USER_EMAIL}e + LookupUserAttr givenname REMOTE_USER_FIRSTNAME + RequestHeader set X-SSSD-REMOTE_USER_FIRSTNAME %{REMOTE_USER_FIRSTNAME}e + LookupUserAttr sn REMOTE_USER_LASTNAME + RequestHeader set X-SSSD-REMOTE_USER_LASTNAME %{REMOTE_USER_LASTNAME}e + LookupUserGroups REMOTE_USER_GROUPS ":" + RequestHeader set X-SSSD-REMOTE_USER_GROUPS %{REMOTE_USER_GROUPS}e +</LocationMatch> + +ProxyPass / http://localhost:8383/ +ProxyPassReverse / http://localhost:8383/ diff --git a/odl-aaa-moon/aaa/commons/postman_examples/AAA_AuthZ_MDSAL.json.postman_collection b/odl-aaa-moon/aaa/commons/postman_examples/AAA_AuthZ_MDSAL.json.postman_collection new file mode 100644 index 00000000..15193a70 --- /dev/null +++ b/odl-aaa-moon/aaa/commons/postman_examples/AAA_AuthZ_MDSAL.json.postman_collection @@ -0,0 +1,77 @@ +{ + "id": "273974a1-2df8-b0a6-57f9-1397cd1628d7", + "name": "AAA AuthZ MDSAL", + "description": "This Postman collection contains some of the common operations that are necessary to \"provision\" authorization services on top of ODL.", + "order": [ + "7959a1f4-703a-417a-9d4c-70ab56c0e57f", + "262c9b05-04a6-8dfa-5eb3-c9f9f90b3c4a", + "4df58109-fd50-dbdf-b982-7e59d3475544" + ], + "folders": [], + "timestamp": 1439405060911, + "owner": 0, + "remoteLink": "", + "public": false, + "requests": [ + { + "id": "262c9b05-04a6-8dfa-5eb3-c9f9f90b3c4a", + "headers": "Authorization: Basic YWRtaW46YWRtaW4=\n", + "url": "http://localhost:8181/restconf/config/authorization-schema:simple-authorization/policies/RestConfService/", + "pathVariables": {}, + "preRequestScript": "", + "method": "GET", + "collectionId": "273974a1-2df8-b0a6-57f9-1397cd1628d7", + "data": [], + "dataMode": "raw", + "name": "Get configuration authorization schema with admin role", + "description": "", + "descriptionFormat": "html", + "time": 1439405954342, + "version": 2, + "responses": [], + "tests": "", + "currentHelper": "normal", + "helperAttributes": {}, + "rawModeData": "" + }, + { + "id": "4df58109-fd50-dbdf-b982-7e59d3475544", + "headers": "Authorization: Basic dXNlcjp1c2Vy\n", + "url": "http://localhost:8181/restconf/config/authorization-schema:simple-authorization/policies/RestConfService/", + "preRequestScript": "", + "pathVariables": {}, + "method": "GET", + "data": [], + "dataMode": "params", + "version": 2, + "tests": "", + "currentHelper": "normal", + "helperAttributes": {}, + "time": 1439406616859, + "name": "Get configuration authorization schema with user role", + "description": "", + "collectionId": "273974a1-2df8-b0a6-57f9-1397cd1628d7", + "responses": [] + }, + { + "id": "7959a1f4-703a-417a-9d4c-70ab56c0e57f", + "headers": "Authorization: Basic YWRtaW46YWRtaW4=\nContent-Type: application/json\n", + "url": "http://localhost:8181/restconf/config/authorization-schema:simple-authorization/policies/RestConfService/", + "preRequestScript": "", + "pathVariables": {}, + "method": "PUT", + "data": [], + "dataMode": "raw", + "version": 2, + "tests": "", + "currentHelper": "normal", + "helperAttributes": {}, + "time": 1439405844861, + "name": "Secure RestConfService for admin role", + "description": "", + "collectionId": "273974a1-2df8-b0a6-57f9-1397cd1628d7", + "responses": [], + "rawModeData": "{\n \"policies\": {\n \"resource\": \"*\",\n \"service\":\"RestConfService\",\n \"role\": \"admin\"\n }\n}" + } + ] +}
\ No newline at end of file diff --git a/odl-aaa-moon/aaa/distribution-karaf/pom.xml b/odl-aaa-moon/aaa/distribution-karaf/pom.xml new file mode 100644 index 00000000..7f5c9287 --- /dev/null +++ b/odl-aaa-moon/aaa/distribution-karaf/pom.xml @@ -0,0 +1,291 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + <parent> + <groupId>org.opendaylight.aaa</groupId> + <artifactId>aaa-parent</artifactId> + <version>0.3.2-Beryllium-SR2</version> + <relativePath>../parent</relativePath> + </parent> + + <artifactId>distribution-karaf</artifactId> + <packaging>pom</packaging> + <prerequisites> + <maven>3.0</maven> + </prerequisites> + + <dependencies> + <!-- Basic Karaf dependencies --> + <dependency> + <groupId>org.apache.karaf.features</groupId> + <artifactId>framework</artifactId> + <version>${karaf.version}</version> + <type>kar</type> + </dependency> + <dependency> + <groupId>org.apache.karaf.features</groupId> + <artifactId>standard</artifactId> + <version>${karaf.version}</version> + <classifier>features</classifier> + <type>xml</type> + <scope>runtime</scope> + </dependency> + + <!-- ODL Branding --> + <dependency> + <groupId>org.opendaylight.controller</groupId> + <artifactId>karaf.branding</artifactId> + <version>${karaf.branding.version}</version> + <scope>compile</scope> + </dependency> + + <!-- ODL Resources needed for karaf --> + <dependency> + <groupId>org.opendaylight.controller</groupId> + <artifactId>opendaylight-karaf-resources</artifactId> + <version>${karaf.resources.version}</version> + </dependency> + + <!-- Project local feautures --> + <dependency> + <groupId>org.opendaylight.aaa</groupId> + <artifactId>features-aaa-api</artifactId> + <classifier>features</classifier> + <version>${project.version}</version> + <type>xml</type> + <scope>runtime</scope> + </dependency> + <dependency> + <groupId>org.opendaylight.aaa</groupId> + <artifactId>features-aaa</artifactId> + <classifier>features</classifier> + <version>${project.version}</version> + <type>xml</type> + <scope>runtime</scope> + </dependency> + <dependency> + <groupId>org.opendaylight.aaa</groupId> + <artifactId>features-aaa-authz</artifactId> + <classifier>features</classifier> + <version>${project.version}</version> + <type>xml</type> + <scope>runtime</scope> + </dependency> + <dependency> + <groupId>org.opendaylight.aaa</groupId> + <artifactId>features-aaa-shiro</artifactId> + <classifier>features</classifier> + <version>${project.version}</version> + <type>xml</type> + <scope>runtime</scope> + </dependency> + </dependencies> + + <build> + <pluginManagement> + <plugins> + <plugin> + <groupId>org.eclipse.m2e</groupId> + <artifactId>lifecycle-mapping</artifactId> + <version>1.0.0</version> + <configuration> + <lifecycleMappingMetadata> + <pluginExecutions> + <pluginExecution> + <pluginExecutionFilter> + <groupId>org.apache.felix</groupId> + <artifactId>maven-bundle-plugin</artifactId> + <versionRange>[0,)</versionRange> + <goals> + <goal>cleanVersions</goal> + </goals> + </pluginExecutionFilter> + <action> + <ignore></ignore> + </action> + </pluginExecution> + <pluginExecution> + <pluginExecutionFilter> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-dependency-plugin</artifactId> + <versionRange>[0,)</versionRange> + <goals> + <goal>copy</goal> + <goal>unpack</goal> + </goals> + </pluginExecutionFilter> + <action> + <ignore></ignore> + </action> + </pluginExecution> + <pluginExecution> + <pluginExecutionFilter> + <groupId>org.apache.karaf.tooling</groupId> + <artifactId>karaf-maven-plugin</artifactId> + <versionRange>[0,)</versionRange> + <goals> + <goal>commands-generate-help</goal> + </goals> + </pluginExecutionFilter> + <action> + <ignore></ignore> + </action> + </pluginExecution> + <pluginExecution> + <pluginExecutionFilter> + <groupId>org.fusesource.scalate</groupId> + <artifactId>maven-scalate-plugin</artifactId> + <versionRange>[0,)</versionRange> + <goals> + <goal>sitegen</goal> + </goals> + </pluginExecutionFilter> + <action> + <ignore></ignore> + </action> + </pluginExecution> + <pluginExecution> + <pluginExecutionFilter> + <groupId>org.apache.servicemix.tooling</groupId> + <artifactId>depends-maven-plugin</artifactId> + <versionRange>[0,)</versionRange> + <goals> + <goal>generate-depends-file</goal> + </goals> + </pluginExecutionFilter> + <action> + <ignore></ignore> + </action> + </pluginExecution> + </pluginExecutions> + </lifecycleMappingMetadata> + </configuration> + </plugin> + </plugins> + </pluginManagement> + <plugins> + <plugin> + <groupId>org.apache.karaf.tooling</groupId> + <artifactId>karaf-maven-plugin</artifactId> + <extensions>true</extensions> + <configuration> + <bootFeatures> + <feature>standard</feature> + <!-- Optional TODO: Add entries here for the features + you want in your local distro Note: odl-restconf is a separate feature from + odl-mdsal-broker. If you want restconf, you need to list it here explicitely. + Examples: <feature>odl-toaster</feature> <feature>odl-restconf</feature> --> + <!-- Final TODO: Remove TODO Comments ;) --> + </bootFeatures> + </configuration> + <executions> + <execution> + <id>process-resources</id> + <goals> + <goal>install-kars</goal> + </goals> + <phase>process-resources</phase> + </execution> + <execution> + <id>package</id> + <goals> + <goal>instance-create-archive</goal> + </goals> + </execution> + </executions> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-dependency-plugin</artifactId> + <version>2.6</version> + <executions> + <execution> + <id>copy</id> + <goals> + <goal>copy</goal> + </goals> + <phase>generate-resources</phase> + <configuration> + <artifactItems> + <artifactItem> + <groupId>org.opendaylight.controller</groupId> + <artifactId>karaf.branding</artifactId> + <version>${karaf.branding.version}</version> + <outputDirectory>target/assembly/lib</outputDirectory> + <destFileName>karaf.branding-${karaf.branding.version}.jar</destFileName> + </artifactItem> + </artifactItems> + </configuration> + </execution> + <execution> + <id>unpack-karaf-resources</id> + <goals> + <goal>unpack-dependencies</goal> + </goals> + <phase>prepare-package</phase> + <configuration> + <outputDirectory>${project.build.directory}/assembly</outputDirectory> + <groupId>org.opendaylight.controller</groupId> + <includeArtifactIds>opendaylight-karaf-resources</includeArtifactIds> + <excludes>META-INF\/**</excludes> + <excludeTransitive>true</excludeTransitive> + <ignorePermissions>false</ignorePermissions> + </configuration> + </execution> + </executions> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-antrun-plugin</artifactId> + <executions> + <execution> + <phase>prepare-package</phase> + <goals> + <goal>run</goal> + </goals> + <configuration> + <tasks> + <chmod perm="755"> + <fileset + dir="${project.build.directory}/assembly/bin"> + <include name="karaf" /> + <include name="instance" /> + <include name="start" /> + <include name="stop" /> + <include name="status" /> + <include name="client" /> + <include name="shell" /> + </fileset> + </chmod> + </tasks> + </configuration> + </execution> + </executions> + </plugin> + + <!-- DO NOT install or deploy the karaf artifact --> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-install-plugin</artifactId> + <configuration> + <skip>true</skip> + </configuration> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-deploy-plugin</artifactId> + <configuration> + <skip>true</skip> + </configuration> + </plugin> + </plugins> + </build> + + <scm> + <connection>scm:git:ssh://git.opendaylight.org:29418/aaa.git</connection> + <developerConnection>scm:git:ssh://git.opendaylight.org:29418/aaa.git</developerConnection> + <tag>HEAD</tag> + <url>https://git.opendaylight.org/gerrit/gitweb?p=aaa.git;a=summary</url> + </scm> +</project> diff --git a/odl-aaa-moon/aaa/features/api/pom.xml b/odl-aaa-moon/aaa/features/api/pom.xml new file mode 100644 index 00000000..80545866 --- /dev/null +++ b/odl-aaa-moon/aaa/features/api/pom.xml @@ -0,0 +1,91 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Copyright (c) 2014 Hewlett-Packard Development Company, L.P. and others. + 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 --> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + <parent> + <groupId>org.opendaylight.odlparent</groupId> + <artifactId>features-parent</artifactId> + <version>1.6.2-Beryllium-SR2</version> + <relativePath/> + </parent> + + <groupId>org.opendaylight.aaa</groupId> + <artifactId>features-aaa-api</artifactId> + <version>0.3.2-Beryllium-SR2</version> + <packaging>jar</packaging> + + <properties> + <yangtools.version>0.8.2-Beryllium-SR2</yangtools.version> + <mdsal.version>2.0.2-Beryllium-SR2</mdsal.version> + </properties> + + <dependencyManagement> + <dependencies> + <!-- This project --> + <dependency> + <groupId>org.opendaylight.aaa</groupId> + <artifactId>aaa-artifacts</artifactId> + <version>${project.version}</version> + <scope>import</scope> + <type>pom</type> + </dependency> + + <!-- YANG tools --> + <dependency> + <groupId>org.opendaylight.yangtools</groupId> + <artifactId>yangtools-artifacts</artifactId> + <version>${yangtools.version}</version> + <scope>import</scope> + <type>pom</type> + </dependency> + </dependencies> + </dependencyManagement> + + <dependencies> + <dependency> + <groupId>org.slf4j</groupId> + <artifactId>slf4j-api</artifactId> + </dependency> + <dependency> + <groupId>org.slf4j</groupId> + <artifactId>slf4j-simple</artifactId> + </dependency> + <dependency> + <groupId>com.sun.jersey</groupId> + <artifactId>jersey-server</artifactId> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>org.opendaylight.aaa</groupId> + <artifactId>aaa-authn-api</artifactId> + </dependency> + <dependency> + <groupId>org.opendaylight.aaa</groupId> + <artifactId>aaa-credential-store-api</artifactId> + </dependency> + <dependency> + <groupId>org.opendaylight.yangtools</groupId> + <artifactId>features-yangtools</artifactId> + <classifier>features</classifier> + <type>xml</type> + </dependency> + <dependency> + <groupId>org.opendaylight.mdsal</groupId> + <artifactId>features-mdsal</artifactId> + <version>2.0.2-Beryllium-SR2</version> + <classifier>features</classifier> + <type>xml</type> + </dependency> + </dependencies> + + <scm> + <connection>scm:git:ssh://git.opendaylight.org:29418/aaa.git</connection> + <developerConnection>scm:git:ssh://git.opendaylight.org:29418/aaa.git</developerConnection> + <tag>HEAD</tag> + <url>https://git.opendaylight.org/gerrit/gitweb?p=aaa.git;a=summary</url> + </scm> +</project> diff --git a/odl-aaa-moon/aaa/features/api/src/main/features/features.xml b/odl-aaa-moon/aaa/features/api/src/main/features/features.xml new file mode 100644 index 00000000..c526e174 --- /dev/null +++ b/odl-aaa-moon/aaa/features/api/src/main/features/features.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- vi: set et smarttab sw=4 tabstop=4: --> +<!-- Copyright (c) 2014 Hewlett-Packard Development Company, L.P. and others. + 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 --> +<features name="odl-aaa-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.2.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://karaf.apache.org/xmlns/features/v1.2.0 http://karaf.apache.org/xmlns/features/v1.2.0"> + <repository>mvn:org.opendaylight.yangtools/features-yangtools/{{VERSION}}/xml/features</repository> + <repository>mvn:org.opendaylight.mdsal/features-mdsal/{{VERSION}}/xml/features</repository> + <feature name='odl-aaa-api' description='OpenDaylight :: AAA :: APIs' + version='${project.version}'> + <bundle>mvn:com.sun.jersey/jersey-server/{{VERSION}}</bundle> + <bundle>mvn:com.sun.jersey/jersey-core/{{VERSION}}</bundle> + <bundle>mvn:org.opendaylight.aaa/aaa-authn-api/{{VERSION}}</bundle> + <bundle>mvn:org.opendaylight.aaa/aaa-credential-store-api/{{VERSION}}</bundle> + <feature version='${yangtools.version}'>odl-yangtools-common</feature> + <feature version='${mdsal.version}'>odl-mdsal-binding-base</feature> + </feature> +</features> diff --git a/odl-aaa-moon/aaa/features/authn/pom.xml b/odl-aaa-moon/aaa/features/authn/pom.xml new file mode 100644 index 00000000..0df53fbd --- /dev/null +++ b/odl-aaa-moon/aaa/features/authn/pom.xml @@ -0,0 +1,300 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Copyright (c) 2014-2015 Hewlett-Packard Development Company, L.P. and others. + 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 --> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + <parent> + <groupId>org.opendaylight.odlparent</groupId> + <artifactId>features-parent</artifactId> + <version>1.6.2-Beryllium-SR2</version> + <relativePath/> + </parent> + + <groupId>org.opendaylight.aaa</groupId> + <artifactId>features-aaa</artifactId> + <version>0.3.2-Beryllium-SR2</version> + <packaging>jar</packaging> + + <properties> + <config.version>0.4.2-Beryllium-SR2</config.version> + <mdsal.version>2.0.2-Beryllium-SR2</mdsal.version> + <controller.mdsal.version>1.3.2-Beryllium-SR2</controller.mdsal.version> + <yangtools.version>0.8.2-Beryllium-SR2</yangtools.version> + </properties> + + <dependencyManagement> + <dependencies> + <!-- This project --> + <dependency> + <groupId>org.opendaylight.aaa</groupId> + <artifactId>aaa-parent</artifactId> + <version>${project.version}</version> + <scope>import</scope> + <type>pom</type> + </dependency> + </dependencies> + </dependencyManagement> + + <dependencies> + <!-- odl-aaa-authn --> + <dependency> + <groupId>com.sun.jersey</groupId> + <artifactId>jersey-servlet</artifactId> + </dependency> + <dependency> + <groupId>com.sun.jersey</groupId> + <artifactId>jersey-core</artifactId> + </dependency> + <dependency> + <groupId>com.sun.jersey</groupId> + <artifactId>jersey-server</artifactId> + </dependency> + <!-- jersey client for moon APIs calls --> + <dependency> + <groupId>com.sun.jersey</groupId> + <artifactId>jersey-client</artifactId> + </dependency> + <dependency> + <groupId>com.sun.jersey</groupId> + <artifactId>jersey-json</artifactId> + </dependency> + <dependency> + <groupId>org.apache.commons</groupId> + <artifactId>commons-lang3</artifactId> + </dependency> + <dependency> + <groupId>org.apache.felix</groupId> + <artifactId>org.apache.felix.dependencymanager</artifactId> + </dependency> + <dependency> + <groupId>org.apache.felix</groupId> + <artifactId>org.apache.felix.metatype</artifactId> + </dependency> + <dependency> + <groupId>net.sf.ehcache</groupId> + <artifactId>ehcache</artifactId> + </dependency> + <dependency> + <groupId>org.apache.geronimo.specs</groupId> + <artifactId>geronimo-jta_1.1_spec</artifactId> + </dependency> + <dependency> + <groupId>org.apache.oltu.oauth2</groupId> + <artifactId>org.apache.oltu.oauth2.common</artifactId> + </dependency> + <dependency> + <groupId>org.apache.oltu.oauth2</groupId> + <artifactId>org.apache.oltu.oauth2.authzserver</artifactId> + </dependency> + <dependency> + <groupId>org.apache.oltu.oauth2</groupId> + <artifactId>org.apache.oltu.oauth2.resourceserver</artifactId> + </dependency> + <dependency> + <groupId>commons-codec</groupId> + <artifactId>commons-codec</artifactId> + </dependency> + <dependency> + <groupId>org.json</groupId> + <artifactId>json</artifactId> + </dependency> + <dependency> + <groupId>org.glassfish</groupId> + <artifactId>javax.json</artifactId> + </dependency> + <dependency> + <groupId>com.fasterxml.jackson.core</groupId> + <artifactId>jackson-core</artifactId> + </dependency> + <dependency> + <groupId>com.fasterxml.jackson.core</groupId> + <artifactId>jackson-annotations</artifactId> + </dependency> + <dependency> + <groupId>com.fasterxml.jackson.core</groupId> + <artifactId>jackson-databind</artifactId> + </dependency> + <dependency> + <groupId>com.fasterxml.jackson.datatype</groupId> + <artifactId>jackson-datatype-json-org</artifactId> + </dependency> + <dependency> + <groupId>com.fasterxml.jackson.jaxrs</groupId> + <artifactId>jackson-jaxrs-base</artifactId> + </dependency> + <dependency> + <groupId>com.fasterxml.jackson.jaxrs</groupId> + <artifactId>jackson-jaxrs-json-provider</artifactId> + </dependency> + <dependency> + <groupId>com.fasterxml.jackson.module</groupId> + <artifactId>jackson-module-jaxb-annotations</artifactId> + </dependency> + <dependency> + <groupId>com.google.guava</groupId> + <artifactId>guava</artifactId> + </dependency> + <dependency> + <groupId>com.h2database</groupId> + <artifactId>h2</artifactId> + </dependency> + <dependency> + <groupId>org.opendaylight.aaa</groupId> + <artifactId>features-aaa-api</artifactId> + <classifier>features</classifier> + <type>xml</type> + </dependency> + <dependency> + <groupId>org.opendaylight.aaa</groupId> + <artifactId>aaa-authn</artifactId> + </dependency> + <dependency> + <groupId>org.opendaylight.aaa</groupId> + <artifactId>aaa-shiro</artifactId> + </dependency> + <dependency> + <groupId>org.opendaylight.aaa</groupId> + <artifactId>aaa-shiro-act</artifactId> + </dependency> + <dependency> + <groupId>org.apache.shiro</groupId> + <artifactId>shiro-core</artifactId> + </dependency> + <dependency> + <groupId>org.apache.shiro</groupId> + <artifactId>shiro-web</artifactId> + </dependency> + <dependency> + <groupId>org.opendaylight.aaa</groupId> + <artifactId>aaa-authn-sts</artifactId> + </dependency> + <dependency> + <groupId>org.opendaylight.aaa</groupId> + <artifactId>aaa-authn-store</artifactId> + </dependency> + <dependency> + <groupId>org.opendaylight.aaa</groupId> + <artifactId>aaa-authn-basic</artifactId> + </dependency> + <dependency> + <groupId>org.opendaylight.aaa</groupId> + <artifactId>aaa-idmlight</artifactId> + </dependency> + <dependency> + <groupId>org.opendaylight.aaa</groupId> + <artifactId>aaa-idmlight</artifactId> + <type>xml</type> + <classifier>config</classifier> + </dependency> + <dependency> + <groupId>org.opendaylight.aaa</groupId> + <artifactId>aaa-idmlight</artifactId> + <version>${project.version}</version> + <type>py</type> + <classifier>config</classifier> + </dependency> + <dependency> + <groupId>org.opendaylight.aaa</groupId> + <artifactId>aaa-authn-federation</artifactId> + </dependency> + <dependency> + <groupId>org.opendaylight.aaa</groupId> + <artifactId>aaa-authn-mdsal-config</artifactId> + <type>xml</type> + <classifier>config</classifier> + </dependency> + <dependency> + <groupId>org.opendaylight.aaa</groupId> + <artifactId>aaa-authn</artifactId> + <type>cfg</type> + <classifier>config</classifier> + </dependency> + <dependency> + <groupId>org.opendaylight.aaa</groupId> + <artifactId>aaa-authn-store</artifactId> + <type>cfg</type> + <classifier>config</classifier> + </dependency> + <dependency> + <groupId>org.opendaylight.aaa</groupId> + <artifactId>aaa-authn-federation</artifactId> + <type>cfg</type> + <classifier>config</classifier> + </dependency> + <dependency> + <groupId>org.opendaylight.aaa</groupId> + <artifactId>aaa-h2-store</artifactId> + </dependency> + <dependency> + <groupId>org.opendaylight.aaa</groupId> + <artifactId>aaa-h2-store</artifactId> + <type>xml</type> + <classifier>config</classifier> + </dependency> + + + <dependency> + <groupId>org.osgi</groupId> + <artifactId>org.osgi.enterprise</artifactId> + <version>4.2.0</version> + </dependency> + + <!-- AuthN MD-SAL Cache dependencies --> + + <dependency> + <groupId>org.opendaylight.aaa</groupId> + <artifactId>aaa-authn-mdsal-store-impl</artifactId> + </dependency> + <dependency> + <groupId>org.opendaylight.aaa</groupId> + <artifactId>aaa-authn-mdsal-api</artifactId> + </dependency> + <dependency> + <groupId>org.opendaylight.yangtools</groupId> + <artifactId>features-yangtools</artifactId> + <classifier>features</classifier> + <type>xml</type> + </dependency> + <dependency> + <groupId>org.opendaylight.controller</groupId> + <artifactId>features-mdsal</artifactId> + <classifier>features</classifier> + <type>xml</type> + </dependency> + <dependency> + <groupId>org.opendaylight.controller</groupId> + <artifactId>features-config</artifactId> + <classifier>features</classifier> + <type>xml</type> + </dependency> + <dependency> + <groupId>org.opendaylight.controller</groupId> + <artifactId>sal-common-impl</artifactId> + </dependency> + + <!-- odl-aaa-sssd --> + <dependency> + <groupId>org.opendaylight.aaa</groupId> + <artifactId>aaa-authn-sssd</artifactId> + </dependency> + + <dependency> + <groupId>org.opendaylight.aaa</groupId> + <artifactId>aaa-authn-idpmapping</artifactId> + </dependency> + + <dependency> + <groupId>org.opendaylight.aaa</groupId> + <artifactId>aaa-authn-keystone</artifactId> + </dependency> + </dependencies> + <scm> + <connection>scm:git:ssh://git.opendaylight.org:29418/aaa.git</connection> + <developerConnection>scm:git:ssh://git.opendaylight.org:29418/aaa.git</developerConnection> + <tag>HEAD</tag> + <url>https://git.opendaylight.org/gerrit/gitweb?p=aaa.git;a=summary</url> + </scm> +</project> diff --git a/odl-aaa-moon/aaa/features/authn/src/main/features/features.xml b/odl-aaa-moon/aaa/features/authn/src/main/features/features.xml new file mode 100644 index 00000000..2796e467 --- /dev/null +++ b/odl-aaa-moon/aaa/features/authn/src/main/features/features.xml @@ -0,0 +1,249 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- vi: set et smarttab sw=4 tabstop=4: --> +<!-- Copyright (c) 2014-2015 Hewlett-Packard Development Company, L.P. and others. + 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 --> +<features name="odl-aaa-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.2.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://karaf.apache.org/xmlns/features/v1.2.0 http://karaf.apache.org/xmlns/features/v1.2.0"> + <repository>mvn:org.opendaylight.aaa/features-aaa-api/{{VERSION}}/xml/features</repository> + <repository>mvn:org.opendaylight.yangtools/features-yangtools/{{VERSION}}/xml/features</repository> + <repository>mvn:org.opendaylight.controller/features-config/{{VERSION}}/xml/features</repository> + <repository>mvn:org.opendaylight.mdsal/features-mdsal/{{VERSION}}/xml/features</repository> + <repository>mvn:org.opendaylight.controller/features-mdsal/{{VERSION}}/xml/features</repository> + + <feature name='odl-aaa-authn-no-cluster' description='OpenDaylight :: AAA :: Authentication - NO CLUSTER' + version='${project.version}'> + <feature version='${project.version}'>odl-aaa-api</feature> + + <!-- MD-SAL --> + <feature version='${yangtools.version}'>odl-yangtools-common</feature> + <feature version='${mdsal.version}'>odl-mdsal-binding-base</feature> + <feature version='${controller.mdsal.version}'>odl-mdsal-broker</feature> + <feature version='${config.version}'>odl-config-core</feature> + + <!-- REST --> + <feature>war</feature> + <bundle>mvn:com.sun.jersey/jersey-servlet/{{VERSION}}</bundle> + <bundle>mvn:com.sun.jersey/jersey-core/{{VERSION}}</bundle> + <bundle>mvn:com.sun.jersey/jersey-server/{{VERSION}}</bundle> + <bundle>mvn:com.sun.jersey/jersey-client/${jersey.version}</bundle> + + <!-- OSGi --> + <bundle>mvn:org.apache.felix/org.apache.felix.dependencymanager/{{VERSION}}</bundle> + <bundle>mvn:org.apache.felix/org.apache.felix.metatype/{{VERSION}}</bundle> + + <!-- EhCache --> + <bundle>mvn:net.sf.ehcache/ehcache/{{VERSION}}</bundle> + <bundle>mvn:org.apache.geronimo.specs/geronimo-jta_1.1_spec/{{VERSION}}</bundle> + + <!-- OAuth --> + <bundle>mvn:org.apache.oltu.oauth2/org.apache.oltu.oauth2.common/{{VERSION}}</bundle> + <bundle>mvn:org.apache.oltu.oauth2/org.apache.oltu.oauth2.authzserver/{{VERSION}}</bundle> + <bundle>mvn:org.apache.oltu.oauth2/org.apache.oltu.oauth2.resourceserver/{{VERSION}}</bundle> + <bundle>mvn:commons-codec/commons-codec/{{VERSION}}</bundle> + <bundle>wrap:mvn:org.json/json/{{VERSION}}</bundle> + + <!-- commons-lang --> + <bundle>wrap:mvn:org.apache.commons/commons-lang3/{{VERSION}}</bundle> + + <!-- AuthN --> + <bundle>mvn:org.opendaylight.aaa/aaa-shiro/{{VERSION}}</bundle> + <bundle>mvn:org.opendaylight.aaa/aaa-shiro-act/{{VERSION}}</bundle> + <bundle>mvn:org.apache.shiro/shiro-core/{{VERSION}}</bundle> + <bundle>mvn:org.apache.shiro/shiro-web/{{VERSION}}</bundle> + <bundle>mvn:org.opendaylight.aaa/aaa-authn/{{VERSION}}</bundle> + <bundle>mvn:org.opendaylight.aaa/aaa-authn-sts/{{VERSION}}</bundle> + <bundle>mvn:org.opendaylight.aaa/aaa-authn-store/{{VERSION}}</bundle> + <bundle>mvn:org.opendaylight.aaa/aaa-authn-basic/{{VERSION}}</bundle> + <bundle>mvn:com.google.guava/guava/{{VERSION}}</bundle> + + <!--H2 Store --> + <bundle>mvn:org.osgi/org.osgi.enterprise/4.2.0</bundle> + <bundle>wrap:mvn:com.h2database/h2/{{VERSION}}</bundle> + <bundle>mvn:org.opendaylight.aaa/aaa-h2-store/{{VERSION}}</bundle> + <configfile finalname="etc/opendaylight/karaf/08-aaa-h2-store-config.xml">mvn:org.opendaylight.aaa/aaa-h2-store/{{VERSION}}/xml/config</configfile> + + <!-- IDMLight --> + <bundle>mvn:org.opendaylight.aaa/aaa-idmlight/{{VERSION}}</bundle> + <configfile finalname="etc/opendaylight/karaf/08-aaa-idmlight-config.xml">mvn:org.opendaylight.aaa/aaa-idmlight/{{VERSION}}/xml/config</configfile> + <configfile finalname="etc/idmtool">mvn:org.opendaylight.aaa/aaa-idmlight/{{VERSION}}/py/config</configfile> + + <bundle>mvn:com.fasterxml.jackson.core/jackson-core/{{VERSION}}</bundle> + <bundle>mvn:com.fasterxml.jackson.core/jackson-annotations/{{VERSION}}</bundle> + <bundle>mvn:com.fasterxml.jackson.core/jackson-databind/{{VERSION}}</bundle> + <bundle>mvn:com.fasterxml.jackson.datatype/jackson-datatype-json-org/{{VERSION}}</bundle> + <bundle>mvn:com.fasterxml.jackson.jaxrs/jackson-jaxrs-base/{{VERSION}}</bundle> + <bundle>mvn:com.fasterxml.jackson.jaxrs/jackson-jaxrs-json-provider/{{VERSION}}</bundle> + <bundle>mvn:com.fasterxml.jackson.module/jackson-module-jaxb-annotations/{{VERSION}}</bundle> + + <!-- Federation --> + <bundle>mvn:org.opendaylight.aaa/aaa-authn-federation/{{VERSION}}</bundle> + <bundle>mvn:org.opendaylight.aaa/aaa-authn-idpmapping/{{VERSION}}</bundle> + <bundle>mvn:org.glassfish/javax.json/{{VERSION}}</bundle> + + <configfile finalname="/etc/org.opendaylight.aaa.authn.cfg">mvn:org.opendaylight.aaa/aaa-authn/{{VERSION}}/cfg/config</configfile> + <configfile finalname="/etc/org.opendaylight.aaa.tokens.cfg">mvn:org.opendaylight.aaa/aaa-authn-store/{{VERSION}}/cfg/config</configfile> + <configfile finalname="/etc/org.opendaylight.aaa.federation.cfg">mvn:org.opendaylight.aaa/aaa-authn-federation/{{VERSION}}/cfg/config</configfile> + </feature> + + <feature name='odl-aaa-authn' description='OpenDaylight :: AAA :: Authentication - NO CLUSTER' + version='${project.version}'> + <feature version='${project.version}'>odl-aaa-api</feature> + + <!-- MD-SAL --> + <feature version='${yangtools.version}'>odl-yangtools-common</feature> + <feature version='${mdsal.version}'>odl-mdsal-binding-base</feature> + <feature version='${controller.mdsal.version}'>odl-mdsal-broker</feature> + <feature version='${config.version}'>odl-config-core</feature> + + <!-- REST --> + <feature>war</feature> + <bundle>mvn:com.sun.jersey/jersey-servlet/{{VERSION}}</bundle> + <bundle>mvn:com.sun.jersey/jersey-core/{{VERSION}}</bundle> + <bundle>mvn:com.sun.jersey/jersey-server/{{VERSION}}</bundle> + <bundle>mvn:com.sun.jersey/jersey-client/${jersey.version}</bundle> + + <!-- OSGi --> + <bundle>mvn:org.apache.felix/org.apache.felix.dependencymanager/{{VERSION}}</bundle> + <bundle>mvn:org.apache.felix/org.apache.felix.metatype/{{VERSION}}</bundle> + + <!-- EhCache --> + <bundle>mvn:net.sf.ehcache/ehcache/{{VERSION}}</bundle> + <bundle>mvn:org.apache.geronimo.specs/geronimo-jta_1.1_spec/{{VERSION}}</bundle> + + <!-- OAuth --> + <bundle>mvn:org.apache.oltu.oauth2/org.apache.oltu.oauth2.common/{{VERSION}}</bundle> + <bundle>mvn:org.apache.oltu.oauth2/org.apache.oltu.oauth2.authzserver/{{VERSION}}</bundle> + <bundle>mvn:org.apache.oltu.oauth2/org.apache.oltu.oauth2.resourceserver/{{VERSION}}</bundle> + <bundle>mvn:commons-codec/commons-codec/{{VERSION}}</bundle> + <bundle>wrap:mvn:org.json/json/{{VERSION}}</bundle> + + <!-- commons-lang --> + <bundle>wrap:mvn:org.apache.commons/commons-lang3/{{VERSION}}</bundle> + + <!-- AuthN --> + <bundle>mvn:org.opendaylight.aaa/aaa-shiro/{{VERSION}}</bundle> + <bundle>mvn:org.opendaylight.aaa/aaa-shiro-act/{{VERSION}}</bundle> + <bundle>mvn:org.apache.shiro/shiro-core/{{VERSION}}</bundle> + <bundle>mvn:org.apache.shiro/shiro-web/{{VERSION}}</bundle> + <bundle>mvn:org.opendaylight.aaa/aaa-authn/{{VERSION}}</bundle> + <bundle>mvn:org.opendaylight.aaa/aaa-authn-sts/{{VERSION}}</bundle> + <bundle>mvn:org.opendaylight.aaa/aaa-authn-store/{{VERSION}}</bundle> + <bundle>mvn:org.opendaylight.aaa/aaa-authn-basic/{{VERSION}}</bundle> + <bundle>mvn:com.google.guava/guava/{{VERSION}}</bundle> + + <!--H2 Store --> + <bundle>mvn:org.osgi/org.osgi.enterprise/4.2.0</bundle> + <bundle>wrap:mvn:com.h2database/h2/{{VERSION}}</bundle> + <bundle>mvn:org.opendaylight.aaa/aaa-h2-store/{{VERSION}}</bundle> + <configfile finalname="etc/opendaylight/karaf/08-aaa-h2-store-config.xml">mvn:org.opendaylight.aaa/aaa-h2-store/{{VERSION}}/xml/config</configfile> + + <!-- IDMLight --> + <bundle>mvn:org.opendaylight.aaa/aaa-idmlight/{{VERSION}}</bundle> + <configfile finalname="etc/opendaylight/karaf/08-aaa-idmlight-config.xml">mvn:org.opendaylight.aaa/aaa-idmlight/{{VERSION}}/xml/config</configfile> + <configfile finalname="etc/idmtool">mvn:org.opendaylight.aaa/aaa-idmlight/{{VERSION}}/py/config</configfile> + + <bundle>mvn:com.fasterxml.jackson.core/jackson-core/{{VERSION}}</bundle> + <bundle>mvn:com.fasterxml.jackson.core/jackson-annotations/{{VERSION}}</bundle> + <bundle>mvn:com.fasterxml.jackson.core/jackson-databind/{{VERSION}}</bundle> + <bundle>mvn:com.fasterxml.jackson.datatype/jackson-datatype-json-org/{{VERSION}}</bundle> + <bundle>mvn:com.fasterxml.jackson.jaxrs/jackson-jaxrs-base/{{VERSION}}</bundle> + <bundle>mvn:com.fasterxml.jackson.jaxrs/jackson-jaxrs-json-provider/{{VERSION}}</bundle> + <bundle>mvn:com.fasterxml.jackson.module/jackson-module-jaxb-annotations/{{VERSION}}</bundle> + + <!-- Federation --> + <bundle>mvn:org.opendaylight.aaa/aaa-authn-federation/{{VERSION}}</bundle> + <bundle>mvn:org.opendaylight.aaa/aaa-authn-idpmapping/{{VERSION}}</bundle> + <bundle>mvn:org.glassfish/javax.json/{{VERSION}}</bundle> + + <configfile finalname="/etc/org.opendaylight.aaa.authn.cfg">mvn:org.opendaylight.aaa/aaa-authn/{{VERSION}}/cfg/config</configfile> + <configfile finalname="/etc/org.opendaylight.aaa.tokens.cfg">mvn:org.opendaylight.aaa/aaa-authn-store/{{VERSION}}/cfg/config</configfile> + <configfile finalname="/etc/org.opendaylight.aaa.federation.cfg">mvn:org.opendaylight.aaa/aaa-authn-federation/{{VERSION}}/cfg/config</configfile> + </feature> + + <feature name='odl-aaa-authn-mdsal-cluster' description='OpenDaylight :: AAA :: Authentication :: MD-SAL' + version='${project.version}'> + + <!-- MD-SAL --> + <feature version='${yangtools.version}'>odl-yangtools-common</feature> + <feature version='${mdsal.version}'>odl-mdsal-binding-base</feature> + <feature version='${controller.mdsal.version}'>odl-mdsal-broker</feature> + <feature version='${config.version}'>odl-config-core</feature> + + + <!-- OSGi --> + <bundle>mvn:org.apache.felix/org.apache.felix.dependencymanager/{{VERSION}}</bundle> + <bundle>mvn:org.apache.felix/org.apache.felix.metatype/{{VERSION}}</bundle> + + <!-- OAuth --> + <bundle>mvn:org.apache.oltu.oauth2/org.apache.oltu.oauth2.common/{{VERSION}}</bundle> + <bundle>mvn:org.apache.oltu.oauth2/org.apache.oltu.oauth2.authzserver/{{VERSION}}</bundle> + <bundle>mvn:org.apache.oltu.oauth2/org.apache.oltu.oauth2.resourceserver/{{VERSION}}</bundle> + <bundle>mvn:commons-codec/commons-codec/1.8</bundle> + <bundle>wrap:mvn:org.json/json/{{VERSION}}</bundle> + + <!-- AuthN --> + <bundle>mvn:org.opendaylight.aaa/aaa-shiro/{{VERSION}}</bundle> + <bundle>mvn:org.opendaylight.aaa/aaa-shiro-act/{{VERSION}}</bundle> + <bundle>mvn:org.apache.shiro/shiro-core/{{VERSION}}</bundle> + <bundle>mvn:org.apache.shiro/shiro-web/{{VERSION}}</bundle> + <bundle>mvn:org.opendaylight.aaa/aaa-authn-api/{{VERSION}}</bundle> + <bundle>mvn:org.opendaylight.aaa/aaa-authn/{{VERSION}}</bundle> + <bundle>mvn:org.opendaylight.aaa/aaa-authn-sts/{{VERSION}}</bundle> + <bundle>mvn:org.opendaylight.aaa/aaa-authn-mdsal-api/{{VERSION}}</bundle> + <bundle>mvn:org.opendaylight.aaa/aaa-authn-mdsal-store-impl/{{VERSION}}</bundle> + <bundle>mvn:org.opendaylight.aaa/aaa-authn-basic/{{VERSION}}</bundle> + <bundle>mvn:com.google.guava/guava/{{VERSION}}</bundle> + + <!-- IDMLight --> + <bundle>mvn:org.opendaylight.aaa/aaa-idmlight/{{VERSION}}</bundle> + <configfile finalname="etc/opendaylight/karaf/08-aaa-idmlight-config.xml">mvn:org.opendaylight.aaa/aaa-idmlight/{{VERSION}}/xml/config</configfile> + <bundle>mvn:com.fasterxml.jackson.core/jackson-core/{{VERSION}}</bundle> + <bundle>mvn:com.fasterxml.jackson.core/jackson-annotations/{{VERSION}}</bundle> + <bundle>mvn:com.fasterxml.jackson.core/jackson-databind/{{VERSION}}</bundle> + <bundle>mvn:com.fasterxml.jackson.datatype/jackson-datatype-json-org/{{VERSION}}</bundle> + <bundle>mvn:com.fasterxml.jackson.jaxrs/jackson-jaxrs-base/{{VERSION}}</bundle> + <bundle>mvn:com.fasterxml.jackson.jaxrs/jackson-jaxrs-json-provider/{{VERSION}}</bundle> + <bundle>mvn:com.fasterxml.jackson.module/jackson-module-jaxb-annotations/{{VERSION}}</bundle> + <bundle>wrap:mvn:com.h2database/h2/{{VERSION}}</bundle> + + <!-- Federation --> + <bundle>mvn:org.opendaylight.aaa/aaa-authn-federation/{{VERSION}}</bundle> + <bundle>mvn:org.opendaylight.aaa/aaa-authn-idpmapping/{{VERSION}}</bundle> + <bundle>mvn:org.glassfish/javax.json/1.0.4</bundle> + + <!-- REST --> + <feature>war</feature> + <bundle>mvn:com.sun.jersey/jersey-servlet/{{VERSION}}</bundle> + <bundle>mvn:com.sun.jersey/jersey-core/{{VERSION}}</bundle> + <bundle>mvn:com.sun.jersey/jersey-server/{{VERSION}}</bundle> + <bundle>mvn:com.sun.jersey/jersey-client/${jersey.version}</bundle> + + <configfile finalname="etc/opendaylight/karaf/08-authn-config.xml">mvn:org.opendaylight.aaa/aaa-authn-mdsal-config/{{VERSION}}/xml/config</configfile> + <configfile finalname="/etc/org.opendaylight.aaa.authn.cfg">mvn:org.opendaylight.aaa/aaa-authn/{{VERSION}}/cfg/config</configfile> + <configfile finalname="/etc/org.opendaylight.aaa.federation.cfg">mvn:org.opendaylight.aaa/aaa-authn-federation/{{VERSION}}/cfg/config</configfile> + + </feature> + + <feature name='odl-aaa-keystone-plugin' description='OpenDaylight :: AAA :: Keystone Plugin - NO CLUSTER' + version='${project.version}'> + <feature version='${project.version}'>odl-aaa-authn</feature> + <bundle>mvn:org.apache.httpcomponents/httpclient-osgi/{{VERSION}}</bundle> + <bundle>mvn:org.apache.httpcomponents/httpcore-osgi/{{VERSION}}</bundle> + <bundle>mvn:org.opendaylight.aaa/aaa-authn-keystone/{{VERSION}}</bundle> + </feature> + + <feature name='odl-aaa-sssd-plugin' description='OpenDaylight :: AAA :: SSSD Federation Plugin' + version='${project.version}'> + <feature version='${project.version}'>odl-aaa-authn</feature> + <bundle>mvn:org.opendaylight.aaa/aaa-authn-sssd/{{VERSION}}</bundle> + </feature> + + <feature name='odl-aaa-authn-sssd-no-cluster' description='OpenDaylight :: AAA :: SSSD Federation - NO CLUSTER' + version='${project.version}'> + <feature version='${project.version}'>odl-aaa-authn-no-cluster</feature> + <bundle>mvn:org.opendaylight.aaa/aaa-authn-sssd/{{VERSION}}</bundle> + </feature> +</features> diff --git a/odl-aaa-moon/aaa/features/authz/pom.xml b/odl-aaa-moon/aaa/features/authz/pom.xml new file mode 100644 index 00000000..2ff41307 --- /dev/null +++ b/odl-aaa-moon/aaa/features/authz/pom.xml @@ -0,0 +1,101 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Copyright (c) 2014 Hewlett-Packard Development Company, L.P. and others. + 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 --> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + <parent> + <groupId>org.opendaylight.odlparent</groupId> + <artifactId>features-parent</artifactId> + <version>1.6.2-Beryllium-SR2</version> + <relativePath/> + </parent> + + <groupId>org.opendaylight.aaa</groupId> + <artifactId>features-aaa-authz</artifactId> + <version>0.3.2-Beryllium-SR2</version> + <packaging>jar</packaging> + + <properties> + <config.version>0.4.2-Beryllium-SR2</config.version> + <mdsal.version>2.0.2-Beryllium-SR2</mdsal.version> + <controller.mdsal.version>1.3.2-Beryllium-SR2</controller.mdsal.version> + <yangtools.version>0.8.2-Beryllium-SR2</yangtools.version> + </properties> + + <dependencyManagement> + <dependencies> + <!-- This project --> + <dependency> + <groupId>org.opendaylight.aaa</groupId> + <artifactId>aaa-parent</artifactId> + <version>${project.version}</version> + <scope>import</scope> + <type>pom</type> + </dependency> + </dependencies> + </dependencyManagement> + + <dependencies> + <dependency> + <groupId>org.opendaylight.aaa</groupId> + <artifactId>features-aaa-api</artifactId> + <classifier>features</classifier> + <type>xml</type> + </dependency> + <!-- odl-aaa-authz --> + <dependency> + <groupId>org.opendaylight.yangtools</groupId> + <artifactId>features-yangtools</artifactId> + <classifier>features</classifier> + <type>xml</type> + </dependency> + <dependency> + <groupId>org.opendaylight.mdsal</groupId> + <artifactId>features-mdsal</artifactId> + <classifier>features</classifier> + <version>${mdsal.version}</version> + <type>xml</type> + </dependency> + <dependency> + <groupId>org.opendaylight.controller</groupId> + <artifactId>features-config</artifactId> + <classifier>features</classifier> + <type>xml</type> + </dependency> + <dependency> + <groupId>org.opendaylight.controller</groupId> + <artifactId>features-mdsal</artifactId> + <classifier>features</classifier> + <type>xml</type> + </dependency> + <dependency> + <groupId>org.opendaylight.aaa</groupId> + <artifactId>authz-restconf-config</artifactId> + <type>xml</type> + <classifier>config</classifier> + </dependency> + <dependency> + <groupId>org.opendaylight.aaa</groupId> + <artifactId>aaa-authz-model</artifactId> + </dependency> + <dependency> + <groupId>org.opendaylight.aaa</groupId> + <artifactId>aaa-authz-service</artifactId> + </dependency> + <dependency> + <groupId>org.opendaylight.aaa</groupId> + <artifactId>authz-service-config</artifactId> + <type>xml</type> + <classifier>config</classifier> + </dependency> + </dependencies> + <scm> + <connection>scm:git:ssh://git.opendaylight.org:29418/aaa.git</connection> + <developerConnection>scm:git:ssh://git.opendaylight.org:29418/aaa.git</developerConnection> + <tag>HEAD</tag> + <url>https://git.opendaylight.org/gerrit/gitweb?p=aaa.git;a=summary</url> + </scm> +</project> diff --git a/odl-aaa-moon/aaa/features/authz/src/main/features/features.xml b/odl-aaa-moon/aaa/features/authz/src/main/features/features.xml new file mode 100644 index 00000000..c5239045 --- /dev/null +++ b/odl-aaa-moon/aaa/features/authz/src/main/features/features.xml @@ -0,0 +1,31 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- vi: set et smarttab sw=4 tabstop=4: --> +<!-- Copyright (c) 2014 Hewlett-Packard Development Company, L.P. and others. + 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 --> +<features name="odl-aaa-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.2.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://karaf.apache.org/xmlns/features/v1.2.0 http://karaf.apache.org/xmlns/features/v1.2.0"> + <repository>mvn:org.opendaylight.yangtools/features-yangtools/{{VERSION}}/xml/features</repository> + <repository>mvn:org.opendaylight.controller/features-config/{{VERSION}}/xml/features</repository> + <repository>mvn:org.opendaylight.mdsal/features-mdsal/{{VERSION}}/xml/features</repository> + <repository>mvn:org.opendaylight.controller/features-mdsal/{{VERSION}}/xml/features</repository> + <repository>mvn:org.opendaylight.aaa/features-aaa-api/{{VERSION}}/xml/features</repository> + + <feature name='odl-aaa-authz' description='OpenDaylight :: AAA :: Authorization' + version='${project.version}'> + <feature version='${project.version}'>odl-aaa-api</feature> + <feature version='${yangtools.version}'>odl-yangtools-common</feature> + <feature version='${mdsal.version}'>odl-mdsal-binding-base</feature> + <feature version='${controller.mdsal.version}'>odl-mdsal-broker</feature> + <feature version='${config.version}'>odl-config-core</feature> + <bundle>mvn:org.opendaylight.aaa/aaa-authz-model/{{VERSION}}</bundle> + <bundle>mvn:org.opendaylight.aaa/aaa-authz-service/{{VERSION}}</bundle> + <configfile + finalname="/etc/opendaylight/karaf/08-authz-config.xml">mvn:org.opendaylight.aaa/authz-service-config/{{VERSION}}/xml/config</configfile> + <configfile + finalname="/etc/opendaylight/karaf/09-rest-connector.xml">mvn:org.opendaylight.aaa/authz-restconf-config/{{VERSION}}/xml/config</configfile> + </feature> + +</features> diff --git a/odl-aaa-moon/aaa/features/pom.xml b/odl-aaa-moon/aaa/features/pom.xml new file mode 100644 index 00000000..548a240b --- /dev/null +++ b/odl-aaa-moon/aaa/features/pom.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + <parent> + <groupId>org.opendaylight.aaa</groupId> + <artifactId>aaa-parent</artifactId> + <version>0.3.2-Beryllium-SR2</version> + <relativePath>../parent</relativePath> + </parent> + <groupId>org.opendaylight.aaa</groupId> + <artifactId>features-aggregator</artifactId> + <packaging>pom</packaging> + <modules> + <module>shiro</module> + <module>api</module> + <module>authn</module> + <module>authz</module> + </modules> +</project> diff --git a/odl-aaa-moon/aaa/features/shiro/pom.xml b/odl-aaa-moon/aaa/features/shiro/pom.xml new file mode 100644 index 00000000..04114355 --- /dev/null +++ b/odl-aaa-moon/aaa/features/shiro/pom.xml @@ -0,0 +1,179 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Copyright (c) 2015 Brocade Communications Systems and others. + 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 --> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + <parent> + <groupId>org.opendaylight.odlparent</groupId> + <artifactId>features-parent</artifactId> + <version>1.6.2-Beryllium-SR2</version> + <relativePath/> + </parent> + + <groupId>org.opendaylight.aaa</groupId> + <artifactId>features-aaa-shiro</artifactId> + <version>0.3.2-Beryllium-SR2</version> + <packaging>jar</packaging> + + <properties> + <javax.annotation.api.version>1.2</javax.annotation.api.version> + <servicemix.version>1.8.3_2</servicemix.version> + </properties> + <dependencyManagement> + <dependencies> + <!-- This project --> + <dependency> + <groupId>org.opendaylight.aaa</groupId> + <artifactId>aaa-parent</artifactId> + <version>${project.version}</version> + <scope>import</scope> + <type>pom</type> + </dependency> + </dependencies> + </dependencyManagement> + + <dependencies> + <dependency> + <groupId>com.google.code.findbugs</groupId> + <artifactId>jsr305</artifactId> + </dependency> + <dependency> + <groupId>org.opendaylight.aaa</groupId> + <artifactId>features-aaa</artifactId> + <version>0.3.2-Beryllium-SR2</version> + <classifier>features</classifier> + <type>xml</type> + </dependency> + <dependency> + <groupId>org.opendaylight.aaa</groupId> + <artifactId>aaa-shiro-act</artifactId> + <version>0.3.2-Beryllium-SR2</version> + </dependency> + <dependency> + <groupId>org.opendaylight.aaa</groupId> + <artifactId>aaa-shiro</artifactId> + <version>0.3.2-Beryllium-SR2</version> + <type>cfg</type> + <classifier>configuration</classifier> + </dependency> + <dependency> + <groupId>org.opendaylight.aaa</groupId> + <artifactId>aaa-shiro</artifactId> + </dependency> + <dependency> + <groupId>org.opendaylight.aaa</groupId> + <artifactId>aaa-authn-sts</artifactId> + <version>0.3.2-Beryllium-SR2</version> + </dependency> + <dependency> + <groupId>org.opendaylight.aaa</groupId> + <artifactId>aaa-authn-api</artifactId> + <version>0.3.2-Beryllium-SR2</version> + </dependency> + <dependency> + <groupId>com.sun.jersey</groupId> + <artifactId>jersey-servlet</artifactId> + </dependency> + <dependency> + <groupId>com.sun.jersey</groupId> + <artifactId>jersey-core</artifactId> + </dependency> + <dependency> + <groupId>com.sun.jersey</groupId> + <artifactId>jersey-server</artifactId> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>javax.servlet</groupId> + <artifactId>javax.servlet-api</artifactId> + </dependency> + <dependency> + <groupId>org.apache.felix</groupId> + <artifactId>org.apache.felix.dependencymanager</artifactId> + </dependency> + <dependency> + <groupId>org.apache.felix</groupId> + <artifactId>org.apache.felix.metatype</artifactId> + </dependency> + <dependency> + <groupId>com.google.guava</groupId> + <artifactId>guava</artifactId> + </dependency> + <dependency> + <groupId>org.opendaylight.aaa</groupId> + <artifactId>aaa-shiro</artifactId> + </dependency> + <dependency> + <groupId>org.opendaylight.aaa</groupId> + <artifactId>aaa-authn</artifactId> + </dependency> + <dependency> + <groupId>org.opendaylight.aaa</groupId> + <artifactId>aaa-authn-api</artifactId> + </dependency> + <dependency> + <groupId>org.opendaylight.aaa</groupId> + <artifactId>aaa-authn-sts</artifactId> + </dependency> + <dependency> + <groupId>javax.annotation</groupId> + <artifactId>javax.annotation-api</artifactId> + <version>${javax.annotation.api.version}</version> + </dependency> + <dependency> + <groupId>org.apache.felix</groupId> + <artifactId>org.apache.felix.dependencymanager</artifactId> + </dependency> + <dependency> + <groupId>org.apache.felix</groupId> + <artifactId>org.apache.felix.metatype</artifactId> + </dependency> + <dependency> + <groupId>org.apache.shiro</groupId> + <artifactId>shiro-web</artifactId> + </dependency> + <dependency> + <groupId>org.apache.shiro</groupId> + <artifactId>shiro-core</artifactId> + </dependency> + <dependency> + <groupId>org.apache.servicemix.bundles</groupId> + <artifactId>org.apache.servicemix.bundles.commons-beanutils</artifactId> + <version>${servicemix.version}</version> + </dependency> + <dependency> + <groupId>org.apache.oltu.oauth2</groupId> + <artifactId>org.apache.oltu.oauth2.resourceserver</artifactId> + </dependency> + <dependency> + <groupId>org.apache.oltu.oauth2</groupId> + <artifactId>org.apache.oltu.oauth2.authzserver</artifactId> + </dependency> + <dependency> + <groupId>org.apache.oltu.oauth2</groupId> + <artifactId>org.apache.oltu.oauth2.common</artifactId> + </dependency> + <dependency> + <groupId>javax.ws.rs</groupId> + <artifactId>javax.ws.rs-api</artifactId> + </dependency> + <dependency> + <groupId>org.json</groupId> + <artifactId>json</artifactId> + </dependency> + <dependency> + <groupId>commons-codec</groupId> + <artifactId>commons-codec</artifactId> + </dependency> + </dependencies> + + <scm> + <connection>scm:git:ssh://git.opendaylight.org:29418/aaa.git</connection> + <developerConnection>scm:git:ssh://git.opendaylight.org:29418/aaa.git</developerConnection> + <tag>HEAD</tag> + <url>https://git.opendaylight.org/gerrit/gitweb?p=aaa.git;a=summary</url> + </scm> +</project> diff --git a/odl-aaa-moon/aaa/features/shiro/src/main/features/features.xml b/odl-aaa-moon/aaa/features/shiro/src/main/features/features.xml new file mode 100644 index 00000000..c6073a2a --- /dev/null +++ b/odl-aaa-moon/aaa/features/shiro/src/main/features/features.xml @@ -0,0 +1,41 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Copyright (c) 2015 Brocade Communications Systems and others. + 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 --> +<features name="odl-aaa-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.2.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://karaf.apache.org/xmlns/features/v1.2.0 http://karaf.apache.org/xmlns/features/v1.2.0"> + + <repository>mvn:org.opendaylight.aaa/features-aaa/{{VERSION}}/xml/features</repository> + + <!-- odl-aaa-shiro feature which combines all aspects of AAA into one feature --> + <feature name='odl-aaa-shiro' description='OpenDaylight :: AAA :: Shiro' + version='${project.version}'> + + <!-- OSGI --> + <bundle>mvn:org.apache.felix/org.apache.felix.dependencymanager/{{VERSION}}</bundle> + <bundle>mvn:org.apache.felix/org.apache.felix.metatype/{{VERSION}}</bundle> + + <!-- Existing AAA infrastructure --> + <feature version='${project.version}'>odl-aaa-authn</feature> + + <bundle>mvn:org.apache.shiro/shiro-web/{{VERSION}}</bundle> + <bundle>mvn:org.apache.shiro/shiro-core/{{VERSION}}</bundle> + + <bundle>mvn:com.google.guava/guava/{{VERSION}}</bundle> + <bundle>wrap:mvn:javax.annotation/javax.annotation-api/{{VERSION}}</bundle> + <bundle>wrap:mvn:com.google.code.findbugs/jsr305/{{VERSION}}</bundle> + <bundle>wrap:mvn:commons-codec/commons-codec/{{VERSION}}</bundle> + <bundle>wrap:mvn:org.apache.oltu.oauth2/org.apache.oltu.oauth2.resourceserver/{{VERSION}}</bundle> + <bundle>wrap:mvn:org.apache.oltu.oauth2/org.apache.oltu.oauth2.authzserver/{{VERSION}}</bundle> + <bundle>wrap:mvn:org.apache.oltu.oauth2/org.apache.oltu.oauth2.common/{{VERSION}}</bundle> + <bundle>wrap:mvn:org.json/json/{{VERSION}}</bundle> + <bundle>mvn:org.apache.servicemix.bundles/org.apache.servicemix.bundles.commons-beanutils/{{VERSION}}</bundle> + <bundle>mvn:org.opendaylight.aaa/aaa-shiro/{{VERSION}}</bundle> + + <!-- AAA configuration file --> + <configfile finalname="/etc/shiro.ini">mvn:org.opendaylight.aaa/aaa-shiro/{{VERSION}}/cfg/configuration</configfile> + </feature> + +</features> diff --git a/odl-aaa-moon/aaa/parent/pom.xml b/odl-aaa-moon/aaa/parent/pom.xml new file mode 100644 index 00000000..42bf03b0 --- /dev/null +++ b/odl-aaa-moon/aaa/parent/pom.xml @@ -0,0 +1,278 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + <parent> + <groupId>org.opendaylight.odlparent</groupId> + <artifactId>odlparent</artifactId> + <version>1.6.2-Beryllium-SR2</version> + <relativePath/> + </parent> + + <groupId>org.opendaylight.aaa</groupId> + <artifactId>aaa-parent</artifactId> + <version>0.3.2-Beryllium-SR2</version> + <packaging>pom</packaging> + <prerequisites> + <maven>3.0.4</maven> + </prerequisites> + + <properties> + <!-- Karaf --> + <karaf.branding.version>1.2.2-Beryllium-SR2</karaf.branding.version> + <karaf.resources.version>1.6.2-Beryllium-SR2</karaf.resources.version> + + <!-- OSGi --> + <osgi.metatype.version>1.0.10</osgi.metatype.version> + + <!-- Local project version, needed for import --> + <aaa.version>${project.version}</aaa.version> + <parent-path>${basedir}</parent-path> + + <!-- AuthZ --> + <yangtools.version>0.8.2-Beryllium-SR2</yangtools.version> + <jmxGeneratorPath>src/main/yang-gen-config</jmxGeneratorPath> + <salGeneratorPath>src/main/yang-gen-sal</salGeneratorPath> + <mdsal.version>2.0.2-Beryllium-SR2</mdsal.version> + <mdsal.model.version>0.8.2-Beryllium-SR2</mdsal.model.version> + <controller.mdsal.version>1.3.2-Beryllium-SR2</controller.mdsal.version> + <restconf.version>1.3.2-Beryllium-SR2</restconf.version> + <config.version>0.4.2-Beryllium-SR2</config.version> + <config.authz.service.configfile>08-authz-config.xml</config.authz.service.configfile> + <config.restconf.configfile>09-rest-connector.xml</config.restconf.configfile> + <config.configfile.directory>etc/opendaylight/karaf</config.configfile.directory> + + <!-- AuthN --> + <glassfish.json.version>1.0.4</glassfish.json.version> + <ehcache.version>2.8.3</ehcache.version> + <jta.version>1.1.1</jta.version> + <oltu.version>1.0.0</oltu.version> + + <config.authn.store.configfile>08-authn-config.xml</config.authn.store.configfile> + + <!-- IdmLight --> + <h2.version>1.4.185</h2.version> + + <!-- Keystone plugin --> + <httpclient.version>4.4</httpclient.version> + + <!-- Test --> + <javax.inject.version>1</javax.inject.version> + <servlet.tester.version>7.0.0.M2</servlet.tester.version> + <features.test.version>1.6.2-Beryllium-SR2</features.test.version> + </properties> + + <dependencyManagement> + <dependencies> + <!-- ODL --> + <dependency> + <groupId>org.opendaylight.aaa</groupId> + <artifactId>aaa-artifacts</artifactId> + <version>${aaa.version}</version> + <type>pom</type> + <scope>import</scope> + </dependency> + <dependency> + <groupId>org.opendaylight.yangtools</groupId> + <artifactId>yangtools-artifacts</artifactId> + <version>${yangtools.version}</version> + <type>pom</type> + <scope>import</scope> + </dependency> + <dependency> + <groupId>org.opendaylight.mdsal</groupId> + <artifactId>mdsal-artifacts</artifactId> + <version>${mdsal.version}</version> + <scope>import</scope> + <type>pom</type> + </dependency> + <dependency> + <groupId>org.opendaylight.mdsal.model</groupId> + <artifactId>mdsal-model-artifacts</artifactId> + <version>${mdsal.model.version}</version> + <scope>import</scope> + <type>pom</type> + </dependency> + <dependency> + <groupId>org.opendaylight.controller</groupId> + <artifactId>mdsal-artifacts</artifactId> + <version>${controller.mdsal.version}</version> + <scope>import</scope> + <type>pom</type> + </dependency> + <dependency> + <groupId>org.opendaylight.controller</groupId> + <artifactId>config-artifacts</artifactId> + <version>${config.version}</version> + <type>pom</type> + <scope>import</scope> + </dependency> + + <!-- Third-party --> + <dependency> + <groupId>org.glassfish</groupId> + <artifactId>javax.json</artifactId> + <version>${glassfish.json.version}</version> + </dependency> + <dependency> + <groupId>org.apache.felix</groupId> + <artifactId>org.apache.felix.metatype</artifactId> + <version>${osgi.metatype.version}</version> + </dependency> + <dependency> + <groupId>net.sf.ehcache</groupId> + <artifactId>ehcache</artifactId> + <version>${ehcache.version}</version> + </dependency> + <dependency> + <groupId>org.apache.geronimo.specs</groupId> + <artifactId>geronimo-jta_1.1_spec</artifactId> + <version>${jta.version}</version> + </dependency> + <dependency> + <groupId>org.apache.oltu.oauth2</groupId> + <artifactId>org.apache.oltu.oauth2.common</artifactId> + <version>${oltu.version}</version> + </dependency> + <dependency> + <groupId>org.apache.oltu.oauth2</groupId> + <artifactId>org.apache.oltu.oauth2.authzserver</artifactId> + <version>${oltu.version}</version> + </dependency> + <dependency> + <groupId>org.apache.oltu.oauth2</groupId> + <artifactId>org.apache.oltu.oauth2.resourceserver</artifactId> + <version>${oltu.version}</version> + </dependency> + <dependency> + <groupId>com.h2database</groupId> + <artifactId>h2</artifactId> + <version>${h2.version}</version> + </dependency> + + <!-- Test stuff --> + <dependency> + <groupId>org.opendaylight.odlparent</groupId> + <artifactId>features-test</artifactId> + <version>${features.test.version}</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>javax.inject</groupId> + <artifactId>javax.inject</artifactId> + <version>${javax.inject.version}</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.eclipse.jetty</groupId> + <artifactId>jetty-servlet-tester</artifactId> + <version>${servlet.tester.version}</version> + <scope>test</scope> + </dependency> + </dependencies> + </dependencyManagement> + + <build> + <plugins> + <plugin> + <groupId>org.jacoco</groupId> + <artifactId>jacoco-maven-plugin</artifactId> + <configuration> + <includes> + <include>org.opendaylight.aaa.*</include> + </includes> + </configuration> + <executions> + <execution> + <id>pre-test</id> + <goals> + <goal>prepare-agent</goal> + </goals> + </execution> + <execution> + <id>post-test</id> + <goals> + <goal>report</goal> + </goals> + <phase>test</phase> + </execution> + </executions> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-checkstyle-plugin</artifactId> + <configuration> + <!-- checkstyle is evil --> + <skip>false</skip> + <failOnViolation>true</failOnViolation> + <configLocation>checkstyle-logging.xml</configLocation> + <consoleOutput>true</consoleOutput> + <includeTestSourceDirectory>true</includeTestSourceDirectory> + <sourceDirectory>${project.basedir}</sourceDirectory> + <includes>**\/*.java,**\/*.xml,**\/*.ini,**\/*.sh,**\/*.bat,**\/*.yang</includes> + <excludes>**\/target\/,**\/bin\/,**\/target-ide\/,**\/src/main/yang-gen-config\/,**\/src/main/yang-gen-sal\/</excludes> + </configuration> + <executions> + <execution> + <goals> + <goal>check</goal> + </goals> + <phase>process-sources</phase> + </execution> + </executions> + <dependencies> + <dependency> + <groupId>org.opendaylight.yangtools</groupId> + <artifactId>checkstyle-logging</artifactId> + <version>${yangtools.version}</version> + </dependency> + </dependencies> + </plugin> + <plugin> + <groupId>org.codehaus.mojo</groupId> + <artifactId>build-helper-maven-plugin</artifactId> + <executions> + <execution> + <phase>generate-sources</phase> + <goals> + <goal>add-source</goal> + </goals> + <configuration> + <sources> + <source>${jmxGeneratorPath}</source> + <source>${salGeneratorPath}</source> + </sources> + </configuration> + </execution> + </executions> + </plugin> + </plugins> + </build> + + <url>https://wiki.opendaylight.org/view/AAA:Main</url> + <scm> + <connection>scm:git:ssh://git.opendaylight.org:29418/aaa.git</connection> + <developerConnection>scm:git:ssh://git.opendaylight.org:29418/aaa.git</developerConnection> + <tag>HEAD</tag> + </scm> + + <reporting> + <plugins> + <plugin> + <groupId>org.codehaus.mojo</groupId> + <artifactId>findbugs-maven-plugin</artifactId> + <version>${findbugs.maven.plugin.version}</version> + <configuration> + <effort>Max</effort> + <threshold>Low</threshold> + <goal>site</goal> + </configuration> + </plugin> + <plugin> + <groupId>org.codehaus.mojo</groupId> + <artifactId>jdepend-maven-plugin</artifactId> + <version>${jdepend.maven.plugin.version}</version> + </plugin> + </plugins> + </reporting> +</project> diff --git a/odl-aaa-moon/aaa/pom.xml b/odl-aaa-moon/aaa/pom.xml new file mode 100644 index 00000000..bafd03a2 --- /dev/null +++ b/odl-aaa-moon/aaa/pom.xml @@ -0,0 +1,50 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + <parent> + <groupId>org.opendaylight.aaa</groupId> + <artifactId>aaa-parent</artifactId> + <version>0.3.2-Beryllium-SR2</version> + <relativePath>parent</relativePath> + </parent> + + <groupId>org.opendaylight.aaa</groupId> + <artifactId>aaa.project</artifactId> + <version>0.3.2-Beryllium-SR2</version> + <packaging>pom</packaging> + <name>aaa</name> <!-- Used by Sonar to set project name --> + <prerequisites> + <maven>3.0</maven> + </prerequisites> + + <modules> + <module>aaa-authn-api</module> + <module>aaa-authn</module> + <module>aaa-idp-mapping</module> + <module>aaa-authn-sts</module> + <module>aaa-authn-store</module> + <module>aaa-authn-federation</module> + <module>aaa-authn-sssd</module> + <module>aaa-authn-keystone</module> + <module>aaa-authn-basic</module> + <module>aaa-idmlight</module> + <module>aaa-authn-mdsal-store</module> + <module>aaa-authz</module> + <module>aaa-credential-store-api</module> + <module>artifacts</module> + <module>features</module> + <module>distribution-karaf</module> + <module>parent</module> + <module>aaa-shiro</module> + <module>aaa-shiro-act</module> + <module>aaa-h2-store</module> + </modules> + + <scm> + <connection>scm:git:ssh://git.opendaylight.org:29418/aaa.git</connection> + <developerConnection>scm:git:ssh://git.opendaylight.org:29418/aaa.git</developerConnection> + <tag>HEAD</tag> + <url>https://wiki.opendaylight.org/view/AAA:Main</url> + </scm> + +</project> |