From e63b03f3d7e4851e008e4bb4d184982c2c0bd229 Mon Sep 17 00:00:00 2001 From: WuKong Date: Tue, 24 May 2016 17:13:17 +0200 Subject: [PATCH] odl/aaa clone Change-Id: I2b72c16aa3245e02d985a2c6189aacee7caad36e Signed-off-by: WuKong --- odl-aaa-moon/README.md | 64 + odl-aaa-moon/aaa-authn-api/pom.xml | 38 + odl-aaa-moon/aaa-authn-api/src/main/docs/Makefile | 29 + .../aaa-authn-api/src/main/docs/class_diagram.png | Bin 0 -> 30016 bytes .../aaa-authn-api/src/main/docs/class_diagram.ucls | 127 ++ .../src/main/docs/credential_auth_sequence.png | Bin 0 -> 29197 bytes .../src/main/docs/credential_auth_sequence.wsd | 18 + .../src/main/docs/federated_auth_sequence.png | Bin 0 -> 40566 bytes .../src/main/docs/federated_auth_sequence.wsd | 24 + .../aaa-authn-api/src/main/docs/mapping.rst | 1609 +++++++++++++++++++ .../src/main/docs/resource_access_sequence.png | Bin 0 -> 38693 bytes .../src/main/docs/resource_access_sequence.wsd | 25 + .../aaa-authn-api/src/main/docs/sssd_01.diag | 6 + .../aaa-authn-api/src/main/docs/sssd_01.svg | 32 + .../aaa-authn-api/src/main/docs/sssd_02.diag | 18 + .../aaa-authn-api/src/main/docs/sssd_02.svg | 79 + .../aaa-authn-api/src/main/docs/sssd_03.diag | 31 + .../aaa-authn-api/src/main/docs/sssd_03.svg | 143 ++ .../aaa-authn-api/src/main/docs/sssd_04.diag | 25 + .../aaa-authn-api/src/main/docs/sssd_04.svg | 100 ++ .../aaa-authn-api/src/main/docs/sssd_05.svg | 613 +++++++ .../src/main/docs/sssd_auth_sequence.png | Bin 0 -> 39322 bytes .../src/main/docs/sssd_auth_sequence.wsd | 23 + .../src/main/docs/sssd_configuration.rst | 1687 ++++++++++++++++++++ .../org/opendaylight/aaa/api/Authentication.java | 26 + .../aaa/api/AuthenticationException.java | 31 + .../aaa/api/AuthenticationService.java | 42 + .../main/java/org/opendaylight/aaa/api/Claim.java | 56 + .../java/org/opendaylight/aaa/api/ClaimAuth.java | 37 + .../org/opendaylight/aaa/api/ClientService.java | 20 + .../org/opendaylight/aaa/api/CredentialAuth.java | 28 + .../java/org/opendaylight/aaa/api/Credentials.java | 15 + .../opendaylight/aaa/api/IDMStoreException.java | 24 + .../org/opendaylight/aaa/api/IDMStoreUtil.java | 40 + .../java/org/opendaylight/aaa/api/IIDMStore.java | 72 + .../java/org/opendaylight/aaa/api/IdMService.java | 39 + .../opendaylight/aaa/api/PasswordCredentials.java | 20 + .../org/opendaylight/aaa/api/SHA256Calculator.java | 83 + .../java/org/opendaylight/aaa/api/TokenAuth.java | 37 + .../java/org/opendaylight/aaa/api/TokenStore.java | 25 + .../java/org/opendaylight/aaa/api/model/Claim.java | 60 + .../org/opendaylight/aaa/api/model/Domain.java | 86 + .../org/opendaylight/aaa/api/model/Domains.java | 34 + .../java/org/opendaylight/aaa/api/model/Grant.java | 86 + .../org/opendaylight/aaa/api/model/Grants.java | 35 + .../org/opendaylight/aaa/api/model/IDMError.java | 61 + .../java/org/opendaylight/aaa/api/model/Role.java | 86 + .../java/org/opendaylight/aaa/api/model/Roles.java | 34 + .../java/org/opendaylight/aaa/api/model/User.java | 126 ++ .../org/opendaylight/aaa/api/model/UserPwd.java | 40 + .../java/org/opendaylight/aaa/api/model/Users.java | 34 + .../org/opendaylight/aaa/api/model/Version.java | 49 + odl-aaa-moon/aaa-authn-basic/pom.xml | 76 + .../java/org/opendaylight/aaa/basic/Activator.java | 31 + .../org/opendaylight/aaa/basic/HttpBasicAuth.java | 129 ++ .../opendaylight/aaa/basic/HttpBasicAuthTest.java | 102 ++ odl-aaa-moon/aaa-authn-federation/pom.xml | 132 ++ .../org/opendaylight/aaa/federation/Activator.java | 51 + .../aaa/federation/ClaimAuthFilter.java | 249 +++ .../aaa/federation/FederationConfiguration.java | 95 ++ .../aaa/federation/FederationEndpoint.java | 149 ++ .../aaa/federation/ServiceLocator.java | 83 + .../opendaylight/aaa/federation/SssdFilter.java | 151 ++ .../OSGI-INF/metatype/metatype.properties | 11 + .../main/resources/OSGI-INF/metatype/metatype.xml | 19 + .../src/main/resources/WEB-INF/web.xml | 34 + .../src/main/resources/federation.cfg | 3 + .../aaa/federation/FederationEndpointTest.java | 121 ++ odl-aaa-moon/aaa-authn-keystone/pom.xml | 106 ++ .../org/opendaylight/aaa/keystone/Activator.java | 34 + .../aaa/keystone/KeystoneTokenAuth.java | 39 + .../aaa-authn-mdsal-api/pom.xml | 99 ++ .../src/main/yang/aaa-authn-model.yang | 154 ++ .../aaa-authn-mdsal-config/pom.xml | 40 + .../src/main/resources/initial/08-authn-config.xml | 43 + .../aaa-authn-mdsal-store-impl/pom.xml | 169 ++ .../aaa/authn/mdsal/store/AuthNStore.java | 263 +++ .../aaa/authn/mdsal/store/DataEncrypter.java | 101 ++ .../aaa/authn/mdsal/store/IDMMDSALStore.java | 483 ++++++ .../aaa/authn/mdsal/store/IDMObject2MDSAL.java | 224 +++ .../aaa/authn/mdsal/store/IDMStore.java | 182 +++ .../aaa/authn/mdsal/store/util/AuthNStoreUtil.java | 140 ++ .../mdsal/store/rev141031/AuthNStoreModule.java | 90 ++ .../store/rev141031/AuthNStoreModuleFactory.java | 46 + .../src/main/yang/aaa-authn-mdsal-store-cfg.yang | 77 + .../authn/mdsal/store/DataBrokerReadMocker.java | 112 ++ .../aaa/authn/mdsal/store/DataEncrypterTest.java | 38 + .../aaa/authn/mdsal/store/IDMStoreTest.java | 175 ++ .../aaa/authn/mdsal/store/IDMStoreTestUtil.java | 181 +++ .../aaa/authn/mdsal/store/MDSALConvertTest.java | 78 + .../authn/mdsal/store/util/AuthNStoreUtilTest.java | 88 + odl-aaa-moon/aaa-authn-mdsal-store/pom.xml | 22 + odl-aaa-moon/aaa-authn-sssd/pom.xml | 88 + .../java/org/opendaylight/aaa/sssd/Activator.java | 28 + .../org/opendaylight/aaa/sssd/SssdClaimAuth.java | 220 +++ odl-aaa-moon/aaa-authn-store/pom.xml | 100 ++ .../java/org/opendaylight/aaa/store/Activator.java | 45 + .../opendaylight/aaa/store/DefaultTokenStore.java | 154 ++ .../OSGI-INF/metatype/metatype.properties | 14 + .../main/resources/OSGI-INF/metatype/metatype.xml | 22 + .../aaa-authn-store/src/main/resources/tokens.cfg | 4 + .../aaa/store/DefaultTokenStoreTest.java | 66 + odl-aaa-moon/aaa-authn-sts/pom.xml | 112 ++ .../java/org/opendaylight/aaa/sts/Activator.java | 207 +++ .../aaa/sts/AnonymousPasswordValidator.java | 30 + .../aaa/sts/AnonymousRefreshTokenValidator.java | 29 + .../org/opendaylight/aaa/sts/OAuthRequest.java | 42 + .../org/opendaylight/aaa/sts/ServiceLocator.java | 141 ++ .../org/opendaylight/aaa/sts/TokenAuthFilter.java | 148 ++ .../org/opendaylight/aaa/sts/TokenEndpoint.java | 242 +++ .../src/main/resources/WEB-INF/web.xml | 23 + .../java/org/opendaylight/aaa/sts/RestFixture.java | 34 + .../org/opendaylight/aaa/sts/TokenAuthTest.java | 94 ++ .../opendaylight/aaa/sts/TokenEndpointTest.java | 164 ++ odl-aaa-moon/aaa-authn/pom.xml | 103 ++ .../main/java/org/opendaylight/aaa/Activator.java | 51 + .../opendaylight/aaa/AuthenticationBuilder.java | 122 ++ .../opendaylight/aaa/AuthenticationManager.java | 77 + .../java/org/opendaylight/aaa/ClaimBuilder.java | 160 ++ .../java/org/opendaylight/aaa/ClientManager.java | 88 + .../main/java/org/opendaylight/aaa/EqualUtil.java | 42 + .../java/org/opendaylight/aaa/HashCodeUtil.java | 104 ++ .../aaa/PasswordCredentialBuilder.java | 87 + .../org/opendaylight/aaa/SecureBlockingQueue.java | 258 +++ .../OSGI-INF/metatype/metatype.properties | 12 + .../main/resources/OSGI-INF/metatype/metatype.xml | 16 + .../aaa-authn/src/main/resources/authn.cfg | 2 + .../aaa/AuthenticationBuilderTest.java | 129 ++ .../aaa/AuthenticationManagerTest.java | 133 ++ .../org/opendaylight/aaa/ClaimBuilderTest.java | 208 +++ .../org/opendaylight/aaa/ClientManagerTest.java | 70 + .../opendaylight/aaa/PasswordCredentialTest.java | 39 + .../opendaylight/aaa/SecureBlockingQueueTest.java | 191 +++ odl-aaa-moon/aaa-authz/aaa-authz-config/pom.xml | 43 + .../src/main/resources/initial/08-authz-config.xml | 60 + odl-aaa-moon/aaa-authz/aaa-authz-model/pom.xml | 95 ++ .../src/main/yang/authorization-schema.yang | 190 +++ .../aaa-authz/aaa-authz-restconf-config/pom.xml | 43 + .../main/resources/initial/09-rest-connector.xml | 42 + odl-aaa-moon/aaa-authz/aaa-authz-service/pom.xml | 152 ++ .../aaa/authz/srv/AuthzBrokerImpl.java | 150 ++ .../aaa/authz/srv/AuthzConsumerContextImpl.java | 46 + .../authz/srv/AuthzDataReadWriteTransaction.java | 129 ++ .../aaa/authz/srv/AuthzDomDataBroker.java | 100 ++ .../aaa/authz/srv/AuthzProviderContextImpl.java | 47 + .../aaa/authz/srv/AuthzReadOnlyTransaction.java | 69 + .../aaa/authz/srv/AuthzServiceImpl.java | 121 ++ .../aaa/authz/srv/AuthzWriteOnlyTransaction.java | 103 ++ .../yang/config/aaa_authz/srv/AuthzSrvModule.java | 76 + .../aaa_authz/srv/AuthzSrvModuleFactory.java | 53 + .../src/main/yang/aaa-authz-service-impl.yang | 115 ++ .../authz/srv/AuthzConsumerContextImplTest.java | 46 + odl-aaa-moon/aaa-authz/pom.xml | 23 + odl-aaa-moon/aaa-credential-store-api/pom.xml | 22 + .../src/main/yang/credential-model.yang | 47 + odl-aaa-moon/aaa-h2-store/.gitignore | 2 + odl-aaa-moon/aaa-h2-store/pom.xml | 160 ++ .../opendaylight/aaa/h2/config/IdmLightConfig.java | 133 ++ .../aaa/h2/persistence/AbstractStore.java | 187 +++ .../aaa/h2/persistence/DomainStore.java | 166 ++ .../aaa/h2/persistence/GrantStore.java | 158 ++ .../opendaylight/aaa/h2/persistence/H2Store.java | 316 ++++ .../opendaylight/aaa/h2/persistence/RoleStore.java | 151 ++ .../aaa/h2/persistence/StoreException.java | 29 + .../opendaylight/aaa/h2/persistence/UserStore.java | 202 +++ .../authn/h2/store/rev151128/AAAH2StoreModule.java | 49 + .../store/rev151128/AAAH2StoreModuleFactory.java | 29 + .../resources/initial/08-aaa-h2-store-config.xml | 26 + .../aaa-h2-store/src/main/yang/aaa-h2-store.yang | 28 + .../aaa/h2/persistence/DomainStoreTest.java | 76 + .../aaa/h2/persistence/GrantStoreTest.java | 76 + .../aaa/h2/persistence/H2StoreTest.java | 187 +++ .../aaa/h2/persistence/RoleStoreTest.java | 76 + .../aaa/h2/persistence/UserStoreTest.java | 79 + odl-aaa-moon/aaa-idmlight/pom.xml | 229 +++ .../opendaylight/aaa/idm/IdmLightApplication.java | 57 + .../org/opendaylight/aaa/idm/IdmLightProxy.java | 208 +++ .../org/opendaylight/aaa/idm/StoreBuilder.java | 118 ++ .../opendaylight/aaa/idm/rest/DomainHandler.java | 591 +++++++ .../org/opendaylight/aaa/idm/rest/RoleHandler.java | 228 +++ .../org/opendaylight/aaa/idm/rest/UserHandler.java | 419 +++++ .../opendaylight/aaa/idm/rest/VersionHandler.java | 46 + .../idmlight/rev151204/AAAIDMLightModule.java | 90 ++ .../rev151204/AAAIDMLightModuleFactory.java | 29 + .../src/main/resources/WEB-INF/web.xml | 79 + .../aaa-idmlight/src/main/resources/idmtool.py | 247 +++ .../resources/initial/08-aaa-idmlight-config.xml | 26 + .../aaa-idmlight/src/main/yang/aaa-idmlight.yang | 28 + .../aaa/idm/persistence/PasswordHashTest.java | 93 ++ odl-aaa-moon/aaa-idmlight/tests/cleardb.sh | 5 + odl-aaa-moon/aaa-idmlight/tests/domain.json | 5 + odl-aaa-moon/aaa-idmlight/tests/domain2.json | 5 + odl-aaa-moon/aaa-idmlight/tests/grant.json | 4 + odl-aaa-moon/aaa-idmlight/tests/grant2.json | 4 + odl-aaa-moon/aaa-idmlight/tests/result.json | 1 + odl-aaa-moon/aaa-idmlight/tests/role-admin.json | 4 + odl-aaa-moon/aaa-idmlight/tests/role-user.json | 4 + odl-aaa-moon/aaa-idmlight/tests/test.sh | 308 ++++ odl-aaa-moon/aaa-idmlight/tests/user.json | 7 + odl-aaa-moon/aaa-idmlight/tests/user2.json | 7 + odl-aaa-moon/aaa-idmlight/tests/userpwd.json | 4 + odl-aaa-moon/aaa-idp-mapping/pom.xml | 84 + .../org/opendaylight/aaa/idpmapping/Activator.java | 25 + .../org/opendaylight/aaa/idpmapping/IdpJson.java | 248 +++ .../aaa/idpmapping/InvalidRuleException.java | 35 + .../aaa/idpmapping/InvalidTypeException.java | 35 + .../aaa/idpmapping/InvalidValueException.java | 35 + .../opendaylight/aaa/idpmapping/RuleProcessor.java | 1368 ++++++++++++++++ .../aaa/idpmapping/StatementErrorException.java | 35 + .../org/opendaylight/aaa/idpmapping/Token.java | 401 +++++ .../aaa/idpmapping/UndefinedValueException.java | 34 + .../aaa/idpmapping/RuleProcessorTest.java | 130 ++ .../org/opendaylight/aaa/idpmapping/TokenTest.java | 66 + odl-aaa-moon/aaa-shiro-act/pom.xml | 84 + .../org/opendaylight/aaa/shiroact/Activator.java | 51 + .../opendaylight/aaa/shiroact/ActivatorTest.java | 25 + odl-aaa-moon/aaa-shiro/pom.xml | 156 ++ .../java/org/opendaylight/aaa/shiro/Activator.java | 45 + .../org/opendaylight/aaa/shiro/ServiceProxy.java | 94 ++ .../aaa/shiro/accounting/Accounter.java | 38 + .../aaa/shiro/authorization/DefaultRBACRules.java | 78 + .../aaa/shiro/authorization/RBACRule.java | 170 ++ .../opendaylight/aaa/shiro/filters/AAAFilter.java | 72 + .../aaa/shiro/filters/MoonOAuthFilter.java | 187 +++ .../shiro/filters/ODLHttpAuthenticationFilter.java | 78 + .../opendaylight/aaa/shiro/moon/MoonPrincipal.java | 155 ++ .../aaa/shiro/moon/MoonTokenEndpoint.java | 30 + .../aaa-shiro/src/main/resources/WEB-INF/web.xml | 48 + .../aaa-shiro/src/main/resources/shiro.ini | 95 ++ .../opendaylight/aaa/shiro/ServiceProxyTest.java | 45 + .../shiro/authorization/DefaultRBACRulesTest.java | 43 + .../aaa/shiro/authorization/RBACRuleTest.java | 106 ++ .../aaa/shiro/realm/ODLJndiLdapRealmTest.java | 246 +++ .../aaa/shiro/realm/TokenAuthRealmTest.java | 139 ++ .../shiro/web/env/KarafIniWebEnvironmentTest.java | 76 + odl-aaa-moon/artifacts/pom.xml | 231 +++ odl-aaa-moon/commons/docs/AuthNusecases.vsd | Bin 0 -> 206336 bytes odl-aaa-moon/commons/docs/direct_authn.png | Bin 0 -> 22058 bytes odl-aaa-moon/commons/docs/federated_authn1.png | Bin 0 -> 36542 bytes odl-aaa-moon/commons/docs/federated_authn2.png | Bin 0 -> 35203 bytes odl-aaa-moon/commons/federation/README | 271 ++++ .../federation/idp_mapping_rules.json.example | 30 + odl-aaa-moon/commons/federation/jetty.xml.example | 85 + .../commons/federation/my_app.conf.example | 31 + .../AAA_AuthZ_MDSAL.json.postman_collection | 77 + odl-aaa-moon/distribution-karaf/pom.xml | 274 ++++ odl-aaa-moon/features/api/pom.xml | 91 ++ .../features/api/src/main/features/features.xml | 21 + odl-aaa-moon/features/authn/pom.xml | 304 ++++ .../features/authn/src/main/features/features.xml | 247 +++ odl-aaa-moon/features/authz/pom.xml | 101 ++ .../features/authz/src/main/features/features.xml | 31 + odl-aaa-moon/features/pom.xml | 19 + odl-aaa-moon/features/shiro/pom.xml | 179 +++ .../features/shiro/src/main/features/features.xml | 41 + odl-aaa-moon/parent/pom.xml | 278 ++++ odl-aaa-moon/pom.xml | 50 + 257 files changed, 28002 insertions(+) create mode 100644 odl-aaa-moon/README.md create mode 100644 odl-aaa-moon/aaa-authn-api/pom.xml create mode 100644 odl-aaa-moon/aaa-authn-api/src/main/docs/Makefile create mode 100644 odl-aaa-moon/aaa-authn-api/src/main/docs/class_diagram.png create mode 100644 odl-aaa-moon/aaa-authn-api/src/main/docs/class_diagram.ucls create mode 100644 odl-aaa-moon/aaa-authn-api/src/main/docs/credential_auth_sequence.png create mode 100644 odl-aaa-moon/aaa-authn-api/src/main/docs/credential_auth_sequence.wsd create mode 100644 odl-aaa-moon/aaa-authn-api/src/main/docs/federated_auth_sequence.png create mode 100644 odl-aaa-moon/aaa-authn-api/src/main/docs/federated_auth_sequence.wsd create mode 100644 odl-aaa-moon/aaa-authn-api/src/main/docs/mapping.rst create mode 100644 odl-aaa-moon/aaa-authn-api/src/main/docs/resource_access_sequence.png create mode 100644 odl-aaa-moon/aaa-authn-api/src/main/docs/resource_access_sequence.wsd create mode 100644 odl-aaa-moon/aaa-authn-api/src/main/docs/sssd_01.diag create mode 100644 odl-aaa-moon/aaa-authn-api/src/main/docs/sssd_01.svg create mode 100644 odl-aaa-moon/aaa-authn-api/src/main/docs/sssd_02.diag create mode 100644 odl-aaa-moon/aaa-authn-api/src/main/docs/sssd_02.svg create mode 100644 odl-aaa-moon/aaa-authn-api/src/main/docs/sssd_03.diag create mode 100644 odl-aaa-moon/aaa-authn-api/src/main/docs/sssd_03.svg create mode 100644 odl-aaa-moon/aaa-authn-api/src/main/docs/sssd_04.diag create mode 100644 odl-aaa-moon/aaa-authn-api/src/main/docs/sssd_04.svg create mode 100644 odl-aaa-moon/aaa-authn-api/src/main/docs/sssd_05.svg create mode 100644 odl-aaa-moon/aaa-authn-api/src/main/docs/sssd_auth_sequence.png create mode 100644 odl-aaa-moon/aaa-authn-api/src/main/docs/sssd_auth_sequence.wsd create mode 100644 odl-aaa-moon/aaa-authn-api/src/main/docs/sssd_configuration.rst create mode 100644 odl-aaa-moon/aaa-authn-api/src/main/java/org/opendaylight/aaa/api/Authentication.java create mode 100644 odl-aaa-moon/aaa-authn-api/src/main/java/org/opendaylight/aaa/api/AuthenticationException.java create mode 100644 odl-aaa-moon/aaa-authn-api/src/main/java/org/opendaylight/aaa/api/AuthenticationService.java create mode 100644 odl-aaa-moon/aaa-authn-api/src/main/java/org/opendaylight/aaa/api/Claim.java create mode 100644 odl-aaa-moon/aaa-authn-api/src/main/java/org/opendaylight/aaa/api/ClaimAuth.java create mode 100644 odl-aaa-moon/aaa-authn-api/src/main/java/org/opendaylight/aaa/api/ClientService.java create mode 100644 odl-aaa-moon/aaa-authn-api/src/main/java/org/opendaylight/aaa/api/CredentialAuth.java create mode 100644 odl-aaa-moon/aaa-authn-api/src/main/java/org/opendaylight/aaa/api/Credentials.java create mode 100644 odl-aaa-moon/aaa-authn-api/src/main/java/org/opendaylight/aaa/api/IDMStoreException.java create mode 100644 odl-aaa-moon/aaa-authn-api/src/main/java/org/opendaylight/aaa/api/IDMStoreUtil.java create mode 100644 odl-aaa-moon/aaa-authn-api/src/main/java/org/opendaylight/aaa/api/IIDMStore.java create mode 100644 odl-aaa-moon/aaa-authn-api/src/main/java/org/opendaylight/aaa/api/IdMService.java create mode 100644 odl-aaa-moon/aaa-authn-api/src/main/java/org/opendaylight/aaa/api/PasswordCredentials.java create mode 100644 odl-aaa-moon/aaa-authn-api/src/main/java/org/opendaylight/aaa/api/SHA256Calculator.java create mode 100644 odl-aaa-moon/aaa-authn-api/src/main/java/org/opendaylight/aaa/api/TokenAuth.java create mode 100644 odl-aaa-moon/aaa-authn-api/src/main/java/org/opendaylight/aaa/api/TokenStore.java create mode 100644 odl-aaa-moon/aaa-authn-api/src/main/java/org/opendaylight/aaa/api/model/Claim.java create mode 100644 odl-aaa-moon/aaa-authn-api/src/main/java/org/opendaylight/aaa/api/model/Domain.java create mode 100644 odl-aaa-moon/aaa-authn-api/src/main/java/org/opendaylight/aaa/api/model/Domains.java create mode 100644 odl-aaa-moon/aaa-authn-api/src/main/java/org/opendaylight/aaa/api/model/Grant.java create mode 100644 odl-aaa-moon/aaa-authn-api/src/main/java/org/opendaylight/aaa/api/model/Grants.java create mode 100644 odl-aaa-moon/aaa-authn-api/src/main/java/org/opendaylight/aaa/api/model/IDMError.java create mode 100644 odl-aaa-moon/aaa-authn-api/src/main/java/org/opendaylight/aaa/api/model/Role.java create mode 100644 odl-aaa-moon/aaa-authn-api/src/main/java/org/opendaylight/aaa/api/model/Roles.java create mode 100644 odl-aaa-moon/aaa-authn-api/src/main/java/org/opendaylight/aaa/api/model/User.java create mode 100644 odl-aaa-moon/aaa-authn-api/src/main/java/org/opendaylight/aaa/api/model/UserPwd.java create mode 100644 odl-aaa-moon/aaa-authn-api/src/main/java/org/opendaylight/aaa/api/model/Users.java create mode 100644 odl-aaa-moon/aaa-authn-api/src/main/java/org/opendaylight/aaa/api/model/Version.java create mode 100644 odl-aaa-moon/aaa-authn-basic/pom.xml create mode 100644 odl-aaa-moon/aaa-authn-basic/src/main/java/org/opendaylight/aaa/basic/Activator.java create mode 100644 odl-aaa-moon/aaa-authn-basic/src/main/java/org/opendaylight/aaa/basic/HttpBasicAuth.java create mode 100644 odl-aaa-moon/aaa-authn-basic/src/test/java/org/opendaylight/aaa/basic/HttpBasicAuthTest.java create mode 100644 odl-aaa-moon/aaa-authn-federation/pom.xml create mode 100644 odl-aaa-moon/aaa-authn-federation/src/main/java/org/opendaylight/aaa/federation/Activator.java create mode 100644 odl-aaa-moon/aaa-authn-federation/src/main/java/org/opendaylight/aaa/federation/ClaimAuthFilter.java create mode 100644 odl-aaa-moon/aaa-authn-federation/src/main/java/org/opendaylight/aaa/federation/FederationConfiguration.java create mode 100644 odl-aaa-moon/aaa-authn-federation/src/main/java/org/opendaylight/aaa/federation/FederationEndpoint.java create mode 100644 odl-aaa-moon/aaa-authn-federation/src/main/java/org/opendaylight/aaa/federation/ServiceLocator.java create mode 100644 odl-aaa-moon/aaa-authn-federation/src/main/java/org/opendaylight/aaa/federation/SssdFilter.java create mode 100644 odl-aaa-moon/aaa-authn-federation/src/main/resources/OSGI-INF/metatype/metatype.properties create mode 100644 odl-aaa-moon/aaa-authn-federation/src/main/resources/OSGI-INF/metatype/metatype.xml create mode 100644 odl-aaa-moon/aaa-authn-federation/src/main/resources/WEB-INF/web.xml create mode 100644 odl-aaa-moon/aaa-authn-federation/src/main/resources/federation.cfg create mode 100644 odl-aaa-moon/aaa-authn-federation/src/test/java/org/opendaylight/aaa/federation/FederationEndpointTest.java create mode 100644 odl-aaa-moon/aaa-authn-keystone/pom.xml create mode 100644 odl-aaa-moon/aaa-authn-keystone/src/main/java/org/opendaylight/aaa/keystone/Activator.java create mode 100644 odl-aaa-moon/aaa-authn-keystone/src/main/java/org/opendaylight/aaa/keystone/KeystoneTokenAuth.java create mode 100644 odl-aaa-moon/aaa-authn-mdsal-store/aaa-authn-mdsal-api/pom.xml create mode 100644 odl-aaa-moon/aaa-authn-mdsal-store/aaa-authn-mdsal-api/src/main/yang/aaa-authn-model.yang create mode 100644 odl-aaa-moon/aaa-authn-mdsal-store/aaa-authn-mdsal-config/pom.xml create mode 100644 odl-aaa-moon/aaa-authn-mdsal-store/aaa-authn-mdsal-config/src/main/resources/initial/08-authn-config.xml create mode 100644 odl-aaa-moon/aaa-authn-mdsal-store/aaa-authn-mdsal-store-impl/pom.xml create mode 100644 odl-aaa-moon/aaa-authn-mdsal-store/aaa-authn-mdsal-store-impl/src/main/java/org/opendaylight/aaa/authn/mdsal/store/AuthNStore.java create mode 100644 odl-aaa-moon/aaa-authn-mdsal-store/aaa-authn-mdsal-store-impl/src/main/java/org/opendaylight/aaa/authn/mdsal/store/DataEncrypter.java create mode 100644 odl-aaa-moon/aaa-authn-mdsal-store/aaa-authn-mdsal-store-impl/src/main/java/org/opendaylight/aaa/authn/mdsal/store/IDMMDSALStore.java create mode 100644 odl-aaa-moon/aaa-authn-mdsal-store/aaa-authn-mdsal-store-impl/src/main/java/org/opendaylight/aaa/authn/mdsal/store/IDMObject2MDSAL.java create mode 100644 odl-aaa-moon/aaa-authn-mdsal-store/aaa-authn-mdsal-store-impl/src/main/java/org/opendaylight/aaa/authn/mdsal/store/IDMStore.java create mode 100644 odl-aaa-moon/aaa-authn-mdsal-store/aaa-authn-mdsal-store-impl/src/main/java/org/opendaylight/aaa/authn/mdsal/store/util/AuthNStoreUtil.java create mode 100644 odl-aaa-moon/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 create mode 100644 odl-aaa-moon/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 create mode 100644 odl-aaa-moon/aaa-authn-mdsal-store/aaa-authn-mdsal-store-impl/src/main/yang/aaa-authn-mdsal-store-cfg.yang create mode 100644 odl-aaa-moon/aaa-authn-mdsal-store/aaa-authn-mdsal-store-impl/src/test/java/org/opendaylight/aaa/authn/mdsal/store/DataBrokerReadMocker.java create mode 100644 odl-aaa-moon/aaa-authn-mdsal-store/aaa-authn-mdsal-store-impl/src/test/java/org/opendaylight/aaa/authn/mdsal/store/DataEncrypterTest.java create mode 100644 odl-aaa-moon/aaa-authn-mdsal-store/aaa-authn-mdsal-store-impl/src/test/java/org/opendaylight/aaa/authn/mdsal/store/IDMStoreTest.java create mode 100644 odl-aaa-moon/aaa-authn-mdsal-store/aaa-authn-mdsal-store-impl/src/test/java/org/opendaylight/aaa/authn/mdsal/store/IDMStoreTestUtil.java create mode 100644 odl-aaa-moon/aaa-authn-mdsal-store/aaa-authn-mdsal-store-impl/src/test/java/org/opendaylight/aaa/authn/mdsal/store/MDSALConvertTest.java create mode 100644 odl-aaa-moon/aaa-authn-mdsal-store/aaa-authn-mdsal-store-impl/src/test/java/org/opendaylight/aaa/authn/mdsal/store/util/AuthNStoreUtilTest.java create mode 100644 odl-aaa-moon/aaa-authn-mdsal-store/pom.xml create mode 100644 odl-aaa-moon/aaa-authn-sssd/pom.xml create mode 100644 odl-aaa-moon/aaa-authn-sssd/src/main/java/org/opendaylight/aaa/sssd/Activator.java create mode 100644 odl-aaa-moon/aaa-authn-sssd/src/main/java/org/opendaylight/aaa/sssd/SssdClaimAuth.java create mode 100644 odl-aaa-moon/aaa-authn-store/pom.xml create mode 100644 odl-aaa-moon/aaa-authn-store/src/main/java/org/opendaylight/aaa/store/Activator.java create mode 100644 odl-aaa-moon/aaa-authn-store/src/main/java/org/opendaylight/aaa/store/DefaultTokenStore.java create mode 100644 odl-aaa-moon/aaa-authn-store/src/main/resources/OSGI-INF/metatype/metatype.properties create mode 100644 odl-aaa-moon/aaa-authn-store/src/main/resources/OSGI-INF/metatype/metatype.xml create mode 100644 odl-aaa-moon/aaa-authn-store/src/main/resources/tokens.cfg create mode 100644 odl-aaa-moon/aaa-authn-store/src/test/java/org/opendaylight/aaa/store/DefaultTokenStoreTest.java create mode 100644 odl-aaa-moon/aaa-authn-sts/pom.xml create mode 100644 odl-aaa-moon/aaa-authn-sts/src/main/java/org/opendaylight/aaa/sts/Activator.java create mode 100644 odl-aaa-moon/aaa-authn-sts/src/main/java/org/opendaylight/aaa/sts/AnonymousPasswordValidator.java create mode 100644 odl-aaa-moon/aaa-authn-sts/src/main/java/org/opendaylight/aaa/sts/AnonymousRefreshTokenValidator.java create mode 100644 odl-aaa-moon/aaa-authn-sts/src/main/java/org/opendaylight/aaa/sts/OAuthRequest.java create mode 100644 odl-aaa-moon/aaa-authn-sts/src/main/java/org/opendaylight/aaa/sts/ServiceLocator.java create mode 100644 odl-aaa-moon/aaa-authn-sts/src/main/java/org/opendaylight/aaa/sts/TokenAuthFilter.java create mode 100644 odl-aaa-moon/aaa-authn-sts/src/main/java/org/opendaylight/aaa/sts/TokenEndpoint.java create mode 100644 odl-aaa-moon/aaa-authn-sts/src/main/resources/WEB-INF/web.xml create mode 100644 odl-aaa-moon/aaa-authn-sts/src/test/java/org/opendaylight/aaa/sts/RestFixture.java create mode 100644 odl-aaa-moon/aaa-authn-sts/src/test/java/org/opendaylight/aaa/sts/TokenAuthTest.java create mode 100644 odl-aaa-moon/aaa-authn-sts/src/test/java/org/opendaylight/aaa/sts/TokenEndpointTest.java create mode 100644 odl-aaa-moon/aaa-authn/pom.xml create mode 100644 odl-aaa-moon/aaa-authn/src/main/java/org/opendaylight/aaa/Activator.java create mode 100644 odl-aaa-moon/aaa-authn/src/main/java/org/opendaylight/aaa/AuthenticationBuilder.java create mode 100644 odl-aaa-moon/aaa-authn/src/main/java/org/opendaylight/aaa/AuthenticationManager.java create mode 100644 odl-aaa-moon/aaa-authn/src/main/java/org/opendaylight/aaa/ClaimBuilder.java create mode 100644 odl-aaa-moon/aaa-authn/src/main/java/org/opendaylight/aaa/ClientManager.java create mode 100644 odl-aaa-moon/aaa-authn/src/main/java/org/opendaylight/aaa/EqualUtil.java create mode 100644 odl-aaa-moon/aaa-authn/src/main/java/org/opendaylight/aaa/HashCodeUtil.java create mode 100644 odl-aaa-moon/aaa-authn/src/main/java/org/opendaylight/aaa/PasswordCredentialBuilder.java create mode 100644 odl-aaa-moon/aaa-authn/src/main/java/org/opendaylight/aaa/SecureBlockingQueue.java create mode 100644 odl-aaa-moon/aaa-authn/src/main/resources/OSGI-INF/metatype/metatype.properties create mode 100644 odl-aaa-moon/aaa-authn/src/main/resources/OSGI-INF/metatype/metatype.xml create mode 100644 odl-aaa-moon/aaa-authn/src/main/resources/authn.cfg create mode 100644 odl-aaa-moon/aaa-authn/src/test/java/org/opendaylight/aaa/AuthenticationBuilderTest.java create mode 100644 odl-aaa-moon/aaa-authn/src/test/java/org/opendaylight/aaa/AuthenticationManagerTest.java create mode 100644 odl-aaa-moon/aaa-authn/src/test/java/org/opendaylight/aaa/ClaimBuilderTest.java create mode 100644 odl-aaa-moon/aaa-authn/src/test/java/org/opendaylight/aaa/ClientManagerTest.java create mode 100644 odl-aaa-moon/aaa-authn/src/test/java/org/opendaylight/aaa/PasswordCredentialTest.java create mode 100644 odl-aaa-moon/aaa-authn/src/test/java/org/opendaylight/aaa/SecureBlockingQueueTest.java create mode 100644 odl-aaa-moon/aaa-authz/aaa-authz-config/pom.xml create mode 100644 odl-aaa-moon/aaa-authz/aaa-authz-config/src/main/resources/initial/08-authz-config.xml create mode 100644 odl-aaa-moon/aaa-authz/aaa-authz-model/pom.xml create mode 100644 odl-aaa-moon/aaa-authz/aaa-authz-model/src/main/yang/authorization-schema.yang create mode 100644 odl-aaa-moon/aaa-authz/aaa-authz-restconf-config/pom.xml create mode 100644 odl-aaa-moon/aaa-authz/aaa-authz-restconf-config/src/main/resources/initial/09-rest-connector.xml create mode 100644 odl-aaa-moon/aaa-authz/aaa-authz-service/pom.xml create mode 100644 odl-aaa-moon/aaa-authz/aaa-authz-service/src/main/java/org/opendaylight/aaa/authz/srv/AuthzBrokerImpl.java create mode 100644 odl-aaa-moon/aaa-authz/aaa-authz-service/src/main/java/org/opendaylight/aaa/authz/srv/AuthzConsumerContextImpl.java create mode 100644 odl-aaa-moon/aaa-authz/aaa-authz-service/src/main/java/org/opendaylight/aaa/authz/srv/AuthzDataReadWriteTransaction.java create mode 100644 odl-aaa-moon/aaa-authz/aaa-authz-service/src/main/java/org/opendaylight/aaa/authz/srv/AuthzDomDataBroker.java create mode 100644 odl-aaa-moon/aaa-authz/aaa-authz-service/src/main/java/org/opendaylight/aaa/authz/srv/AuthzProviderContextImpl.java create mode 100644 odl-aaa-moon/aaa-authz/aaa-authz-service/src/main/java/org/opendaylight/aaa/authz/srv/AuthzReadOnlyTransaction.java create mode 100644 odl-aaa-moon/aaa-authz/aaa-authz-service/src/main/java/org/opendaylight/aaa/authz/srv/AuthzServiceImpl.java create mode 100644 odl-aaa-moon/aaa-authz/aaa-authz-service/src/main/java/org/opendaylight/aaa/authz/srv/AuthzWriteOnlyTransaction.java create mode 100644 odl-aaa-moon/aaa-authz/aaa-authz-service/src/main/java/org/opendaylight/controller/config/yang/config/aaa_authz/srv/AuthzSrvModule.java create mode 100644 odl-aaa-moon/aaa-authz/aaa-authz-service/src/main/java/org/opendaylight/controller/config/yang/config/aaa_authz/srv/AuthzSrvModuleFactory.java create mode 100644 odl-aaa-moon/aaa-authz/aaa-authz-service/src/main/yang/aaa-authz-service-impl.yang create mode 100644 odl-aaa-moon/aaa-authz/aaa-authz-service/src/test/java/org/opendaylight/aaa/authz/srv/AuthzConsumerContextImplTest.java create mode 100644 odl-aaa-moon/aaa-authz/pom.xml create mode 100644 odl-aaa-moon/aaa-credential-store-api/pom.xml create mode 100644 odl-aaa-moon/aaa-credential-store-api/src/main/yang/credential-model.yang create mode 100644 odl-aaa-moon/aaa-h2-store/.gitignore create mode 100644 odl-aaa-moon/aaa-h2-store/pom.xml create mode 100644 odl-aaa-moon/aaa-h2-store/src/main/java/org/opendaylight/aaa/h2/config/IdmLightConfig.java create mode 100644 odl-aaa-moon/aaa-h2-store/src/main/java/org/opendaylight/aaa/h2/persistence/AbstractStore.java create mode 100644 odl-aaa-moon/aaa-h2-store/src/main/java/org/opendaylight/aaa/h2/persistence/DomainStore.java create mode 100644 odl-aaa-moon/aaa-h2-store/src/main/java/org/opendaylight/aaa/h2/persistence/GrantStore.java create mode 100644 odl-aaa-moon/aaa-h2-store/src/main/java/org/opendaylight/aaa/h2/persistence/H2Store.java create mode 100644 odl-aaa-moon/aaa-h2-store/src/main/java/org/opendaylight/aaa/h2/persistence/RoleStore.java create mode 100644 odl-aaa-moon/aaa-h2-store/src/main/java/org/opendaylight/aaa/h2/persistence/StoreException.java create mode 100644 odl-aaa-moon/aaa-h2-store/src/main/java/org/opendaylight/aaa/h2/persistence/UserStore.java create mode 100644 odl-aaa-moon/aaa-h2-store/src/main/java/org/opendaylight/yang/gen/v1/config/aaa/authn/h2/store/rev151128/AAAH2StoreModule.java create mode 100644 odl-aaa-moon/aaa-h2-store/src/main/java/org/opendaylight/yang/gen/v1/config/aaa/authn/h2/store/rev151128/AAAH2StoreModuleFactory.java create mode 100644 odl-aaa-moon/aaa-h2-store/src/main/resources/initial/08-aaa-h2-store-config.xml create mode 100644 odl-aaa-moon/aaa-h2-store/src/main/yang/aaa-h2-store.yang create mode 100644 odl-aaa-moon/aaa-h2-store/src/test/java/org/opendaylight/aaa/h2/persistence/DomainStoreTest.java create mode 100644 odl-aaa-moon/aaa-h2-store/src/test/java/org/opendaylight/aaa/h2/persistence/GrantStoreTest.java create mode 100644 odl-aaa-moon/aaa-h2-store/src/test/java/org/opendaylight/aaa/h2/persistence/H2StoreTest.java create mode 100644 odl-aaa-moon/aaa-h2-store/src/test/java/org/opendaylight/aaa/h2/persistence/RoleStoreTest.java create mode 100644 odl-aaa-moon/aaa-h2-store/src/test/java/org/opendaylight/aaa/h2/persistence/UserStoreTest.java create mode 100644 odl-aaa-moon/aaa-idmlight/pom.xml create mode 100644 odl-aaa-moon/aaa-idmlight/src/main/java/org/opendaylight/aaa/idm/IdmLightApplication.java create mode 100644 odl-aaa-moon/aaa-idmlight/src/main/java/org/opendaylight/aaa/idm/IdmLightProxy.java create mode 100644 odl-aaa-moon/aaa-idmlight/src/main/java/org/opendaylight/aaa/idm/StoreBuilder.java create mode 100644 odl-aaa-moon/aaa-idmlight/src/main/java/org/opendaylight/aaa/idm/rest/DomainHandler.java create mode 100644 odl-aaa-moon/aaa-idmlight/src/main/java/org/opendaylight/aaa/idm/rest/RoleHandler.java create mode 100644 odl-aaa-moon/aaa-idmlight/src/main/java/org/opendaylight/aaa/idm/rest/UserHandler.java create mode 100644 odl-aaa-moon/aaa-idmlight/src/main/java/org/opendaylight/aaa/idm/rest/VersionHandler.java create mode 100644 odl-aaa-moon/aaa-idmlight/src/main/java/org/opendaylight/yang/gen/v1/config/aaa/authn/idmlight/rev151204/AAAIDMLightModule.java create mode 100644 odl-aaa-moon/aaa-idmlight/src/main/java/org/opendaylight/yang/gen/v1/config/aaa/authn/idmlight/rev151204/AAAIDMLightModuleFactory.java create mode 100644 odl-aaa-moon/aaa-idmlight/src/main/resources/WEB-INF/web.xml create mode 100644 odl-aaa-moon/aaa-idmlight/src/main/resources/idmtool.py create mode 100644 odl-aaa-moon/aaa-idmlight/src/main/resources/initial/08-aaa-idmlight-config.xml create mode 100644 odl-aaa-moon/aaa-idmlight/src/main/yang/aaa-idmlight.yang create mode 100644 odl-aaa-moon/aaa-idmlight/src/test/java/org/opendaylight/aaa/idm/persistence/PasswordHashTest.java create mode 100644 odl-aaa-moon/aaa-idmlight/tests/cleardb.sh create mode 100644 odl-aaa-moon/aaa-idmlight/tests/domain.json create mode 100644 odl-aaa-moon/aaa-idmlight/tests/domain2.json create mode 100644 odl-aaa-moon/aaa-idmlight/tests/grant.json create mode 100644 odl-aaa-moon/aaa-idmlight/tests/grant2.json create mode 100644 odl-aaa-moon/aaa-idmlight/tests/result.json create mode 100644 odl-aaa-moon/aaa-idmlight/tests/role-admin.json create mode 100644 odl-aaa-moon/aaa-idmlight/tests/role-user.json create mode 100644 odl-aaa-moon/aaa-idmlight/tests/test.sh create mode 100644 odl-aaa-moon/aaa-idmlight/tests/user.json create mode 100644 odl-aaa-moon/aaa-idmlight/tests/user2.json create mode 100644 odl-aaa-moon/aaa-idmlight/tests/userpwd.json create mode 100644 odl-aaa-moon/aaa-idp-mapping/pom.xml create mode 100644 odl-aaa-moon/aaa-idp-mapping/src/main/java/org/opendaylight/aaa/idpmapping/Activator.java create mode 100644 odl-aaa-moon/aaa-idp-mapping/src/main/java/org/opendaylight/aaa/idpmapping/IdpJson.java create mode 100644 odl-aaa-moon/aaa-idp-mapping/src/main/java/org/opendaylight/aaa/idpmapping/InvalidRuleException.java create mode 100644 odl-aaa-moon/aaa-idp-mapping/src/main/java/org/opendaylight/aaa/idpmapping/InvalidTypeException.java create mode 100644 odl-aaa-moon/aaa-idp-mapping/src/main/java/org/opendaylight/aaa/idpmapping/InvalidValueException.java create mode 100644 odl-aaa-moon/aaa-idp-mapping/src/main/java/org/opendaylight/aaa/idpmapping/RuleProcessor.java create mode 100644 odl-aaa-moon/aaa-idp-mapping/src/main/java/org/opendaylight/aaa/idpmapping/StatementErrorException.java create mode 100644 odl-aaa-moon/aaa-idp-mapping/src/main/java/org/opendaylight/aaa/idpmapping/Token.java create mode 100644 odl-aaa-moon/aaa-idp-mapping/src/main/java/org/opendaylight/aaa/idpmapping/UndefinedValueException.java create mode 100644 odl-aaa-moon/aaa-idp-mapping/src/test/java/org/opendaylight/aaa/idpmapping/RuleProcessorTest.java create mode 100644 odl-aaa-moon/aaa-idp-mapping/src/test/java/org/opendaylight/aaa/idpmapping/TokenTest.java create mode 100644 odl-aaa-moon/aaa-shiro-act/pom.xml create mode 100644 odl-aaa-moon/aaa-shiro-act/src/main/java/org/opendaylight/aaa/shiroact/Activator.java create mode 100644 odl-aaa-moon/aaa-shiro-act/src/test/java/org/opendaylight/aaa/shiroact/ActivatorTest.java create mode 100644 odl-aaa-moon/aaa-shiro/pom.xml create mode 100644 odl-aaa-moon/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/Activator.java create mode 100644 odl-aaa-moon/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/ServiceProxy.java create mode 100644 odl-aaa-moon/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/accounting/Accounter.java create mode 100644 odl-aaa-moon/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/authorization/DefaultRBACRules.java create mode 100644 odl-aaa-moon/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/authorization/RBACRule.java create mode 100644 odl-aaa-moon/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/filters/AAAFilter.java create mode 100644 odl-aaa-moon/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/filters/MoonOAuthFilter.java create mode 100644 odl-aaa-moon/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/filters/ODLHttpAuthenticationFilter.java create mode 100644 odl-aaa-moon/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/moon/MoonPrincipal.java create mode 100644 odl-aaa-moon/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/moon/MoonTokenEndpoint.java create mode 100644 odl-aaa-moon/aaa-shiro/src/main/resources/WEB-INF/web.xml create mode 100644 odl-aaa-moon/aaa-shiro/src/main/resources/shiro.ini create mode 100644 odl-aaa-moon/aaa-shiro/src/test/java/org/opendaylight/aaa/shiro/ServiceProxyTest.java create mode 100644 odl-aaa-moon/aaa-shiro/src/test/java/org/opendaylight/aaa/shiro/authorization/DefaultRBACRulesTest.java create mode 100644 odl-aaa-moon/aaa-shiro/src/test/java/org/opendaylight/aaa/shiro/authorization/RBACRuleTest.java create mode 100644 odl-aaa-moon/aaa-shiro/src/test/java/org/opendaylight/aaa/shiro/realm/ODLJndiLdapRealmTest.java create mode 100644 odl-aaa-moon/aaa-shiro/src/test/java/org/opendaylight/aaa/shiro/realm/TokenAuthRealmTest.java create mode 100644 odl-aaa-moon/aaa-shiro/src/test/java/org/opendaylight/aaa/shiro/web/env/KarafIniWebEnvironmentTest.java create mode 100644 odl-aaa-moon/artifacts/pom.xml create mode 100644 odl-aaa-moon/commons/docs/AuthNusecases.vsd create mode 100644 odl-aaa-moon/commons/docs/direct_authn.png create mode 100644 odl-aaa-moon/commons/docs/federated_authn1.png create mode 100644 odl-aaa-moon/commons/docs/federated_authn2.png create mode 100644 odl-aaa-moon/commons/federation/README create mode 100644 odl-aaa-moon/commons/federation/idp_mapping_rules.json.example create mode 100644 odl-aaa-moon/commons/federation/jetty.xml.example create mode 100644 odl-aaa-moon/commons/federation/my_app.conf.example create mode 100644 odl-aaa-moon/commons/postman_examples/AAA_AuthZ_MDSAL.json.postman_collection create mode 100644 odl-aaa-moon/distribution-karaf/pom.xml create mode 100644 odl-aaa-moon/features/api/pom.xml create mode 100644 odl-aaa-moon/features/api/src/main/features/features.xml create mode 100644 odl-aaa-moon/features/authn/pom.xml create mode 100644 odl-aaa-moon/features/authn/src/main/features/features.xml create mode 100644 odl-aaa-moon/features/authz/pom.xml create mode 100644 odl-aaa-moon/features/authz/src/main/features/features.xml create mode 100644 odl-aaa-moon/features/pom.xml create mode 100644 odl-aaa-moon/features/shiro/pom.xml create mode 100644 odl-aaa-moon/features/shiro/src/main/features/features.xml create mode 100644 odl-aaa-moon/parent/pom.xml create mode 100644 odl-aaa-moon/pom.xml diff --git a/odl-aaa-moon/README.md b/odl-aaa-moon/README.md new file mode 100644 index 00000000..71f52a63 --- /dev/null +++ b/odl-aaa-moon/README.md @@ -0,0 +1,64 @@ +## 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.105.135 +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&scope=sdn' http://:/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://:/restconf/operational/opendaylight-inventory:nodes + +The operational state of access tokens cached in the MD-SAL can also be obtained after enabling the restconf feature: + + feature:install odl-aaa-all + +At the following URL + + http://controller:8181/restconf/operational/aaa-authn-model:tokencache/ diff --git a/odl-aaa-moon/aaa-authn-api/pom.xml b/odl-aaa-moon/aaa-authn-api/pom.xml new file mode 100644 index 00000000..31a17236 --- /dev/null +++ b/odl-aaa-moon/aaa-authn-api/pom.xml @@ -0,0 +1,38 @@ + + + 4.0.0 + + org.opendaylight.aaa + aaa-parent + 0.3.1-Beryllium-SR1 + ../parent + + + aaa-authn-api + bundle + + + + org.slf4j + slf4j-api + + + org.slf4j + slf4j-simple + + + com.sun.jersey + jersey-server + provided + + + + + + org.apache.felix + maven-bundle-plugin + + + + + diff --git a/odl-aaa-moon/aaa-authn-api/src/main/docs/Makefile b/odl-aaa-moon/aaa-authn-api/src/main/docs/Makefile new file mode 100644 index 00000000..446795b4 --- /dev/null +++ b/odl-aaa-moon/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-authn-api/src/main/docs/class_diagram.png b/odl-aaa-moon/aaa-authn-api/src/main/docs/class_diagram.png new file mode 100644 index 0000000000000000000000000000000000000000..999a41f9b68d60c9f980ce6b0364d434153a573c GIT binary patch literal 30016 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 ZN*F}@^S%<$yNAhr$xAC;&yh0r{x9OgH|GEV literal 0 HcmV?d00001 diff --git a/odl-aaa-moon/aaa-authn-api/src/main/docs/class_diagram.ucls b/odl-aaa-moon/aaa-authn-api/src/main/docs/class_diagram.ucls new file mode 100644 index 00000000..68345256 --- /dev/null +++ b/odl-aaa-moon/aaa-authn-api/src/main/docs/class_diagram.ucls @@ -0,0 +1,127 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/odl-aaa-moon/aaa-authn-api/src/main/docs/credential_auth_sequence.png b/odl-aaa-moon/aaa-authn-api/src/main/docs/credential_auth_sequence.png new file mode 100644 index 0000000000000000000000000000000000000000..52d636503141c50d6a2544a5674dcd2cc5e27edf GIT binary patch literal 29197 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 WctVnGFYspqK}S>XP`ZY>_rC$pz5QeW literal 0 HcmV?d00001 diff --git a/odl-aaa-moon/aaa-authn-api/src/main/docs/credential_auth_sequence.wsd b/odl-aaa-moon/aaa-authn-api/src/main/docs/credential_auth_sequence.wsd new file mode 100644 index 00000000..383d4031 --- /dev/null +++ b/odl-aaa-moon/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-authn-api/src/main/docs/federated_auth_sequence.png b/odl-aaa-moon/aaa-authn-api/src/main/docs/federated_auth_sequence.png new file mode 100644 index 0000000000000000000000000000000000000000..799cc9095681dea6dc3f5709eca7cb710994bb4a GIT binary patch literal 40566 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

8KT65mlp`)D#;)%iTz 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 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 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-authn-api/src/main/docs/mapping.rst b/odl-aaa-moon/aaa-authn-api/src/main/docs/mapping.rst new file mode 100644 index 00000000..33635502 --- /dev/null +++ b/odl-aaa-moon/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 | ++-----------+------------+--------------------+---------------------+ +| array | ARRAY | list | List | ++-----------+------------+--------------------+---------------------+ +| string | STRING | unicode (Python 2) | String | +| | +--------------------+ | +| | | str (Python 3) | | ++-----------+------------+--------------------+---------------------+ +| | INTEGER | int | Long | +| number +------------+--------------------+---------------------+ +| | REAL | float | Double | ++-----------+------------+--------------------+---------------------+ +| true | | | | ++-----------+ BOOLEAN | bool | Boolean | +| false | | | | ++-----------+------------+--------------------+---------------------+ +| null | NULL | None | null | ++-----------+------------+--------------------+---------------------+ + + +Rule Debugging and Documentation +-------------------------------- + +If the rule processor reports an error or if you're debugging your +rules by enabling DEBUG log tracing then you must be able to correlate +the reported statement to where it appears in your rule JSON source. A +message will always identify a statement by the rule number, block +number within that rule and the statement number within that +block. However once your rules become moderately complex it will +become increasingly difficult to identify a statement by counting +rules, blocks and statements. + +A better approach is to tag rules and blocks with a name or other +identifying string. You can set the `Reserved Variables`_ +``rule_name`` and ``block_name`` to a string of your choice. These +strings will be reported in all messages along with the rule, block +and statement numbers. + +JSON does not permit comments, as such you cannot include explanatory +comments next to your rules, blocks and statements in the JSON +source. The ``rule_name`` and ``block_name`` can serve a similar +purpose. By putting assignments to these variables as the first +statement in a block you'll both document your rules and be able to +identify specific statements in log messages. + +During rule execution the ``rule_name`` and ``block_name`` are +initialized to the empty string at the beginning of each rule and +block respectively. + +The above example is augmented to include this information. The rule +name is set in the first statement in the first block. + +:: + + [ + [ + ["set", "$rule_name", "Must have UserName or subject"], + ["set", "block_name", "Initialization"], + ["set", $user, ""], + ["set", $roles, []] + ], + [ + ["set", "block_name", "Test for UserName, set $user"], + ["in", "UserName", "$assertion"], + ["continue", "if_not_success"], + ["set", "$user", "$assertion[UserName"], + ], + [ + ["set", "block_name", "Test for subject, set $user"], + ["in", "subject", "$assertion"], + ["continue", "if_not_success"], + ["set", "$user", "$assertion[subject]"], + ], + [ + ["set", "block_name", "If not $user fail, else append unprivileged to roles"], + ["length", "$temp", "$user"], + ["compare", "$temp", ">", 0], + ["exit", "rule_fails", "if_not_success"] + ["append" "$roles", "unprivileged"] + ] + ] + + + + +Variables +--------- + + +Variables always begin with a dollar sign ($) and are followed by an +identifier which is any alpha character followed by zero or more +alphanumeric or underscore characters. The variable may optionally be +delimited with braces ({}) to separate the variable from surrounding +text. Three types of variables are supported: + +* scalar +* array (indexed by zero based integer) +* associative array (indexed by string) + +Both arrays and associative arrays use square brackets ([]) to specify +a member of the array. Examples of variable usage: + +:: + + $name + ${name} + $groups[0] + ${groups[0]} + $properties[key] + ${properties[key]} + +An array or an associative array may be referenced by it's base name +(omitting the indexing brackets). For example the associative array +array named "properties" is referenced using it's base name +``$properties`` but if you want to access a member of the "properties" +associative array named "duration" you would do this ``$properties[duration]`` + +This is not a general purpose language with full expression +syntax. Only one level of variable lookup is supported. Therefore +compound references like this + +:: + + $properties[$groups[2]] + +will not work. + + +Escaping +^^^^^^^^ + +If you need to include a dollar sign in a string (where it is +immediately followed by either an identifier or a brace and identifier) +and do not want to have it be interpreted as representing a variable +you must escape the dollar sign with a backslash, for example +"$amount" is interpreted as the variable ``amount`` but "\\$amount" +is interpreted as the string "$amount" . + + +Reserved Variables +------------------ + +A rule has the following reserved variables: + +assertion + The current assertion values from the federated IdP. It is a + dictionary of key/value pairs. + +regexp_array + The regular expression groups from the last successful regexp match + indexed by number. Group 0 is the entire match. Groups 1..n are + the corresponding parenthesized group counting from the left. For + example regexp_array[1] is the first group. + +regexp_map + The regular expression groups from the last successful regexp match + indexed by group name. + +rule_number + The zero based index of the currently executing rule. + +rule_name + The name of the currently executing rule. If the rule name has not + been set it will be the empty string. + +block_number + The zero based index of the currently executing block within the + currently executing rule. + +block_name + The name of the currently executing block. If the block name has not + been set it will be the empty string. + + +statement_number + The zero based index of the currently executing statement within the + currently executing block. + + +Examples +======== + +Split a fully qualified username into user and realm components +--------------------------------------------------------------- + +It's common for some IdP's to return a fully qualified username +(e.g. principal or subject). The fully qualified username is the +concatenation of the user name, separator and realm name. A common +separator is the @ character. In this example lets say the fully +qualified username is ``bob@example.com`` and you want to return the +user and realm as independent values in your mapped result. The +username appears in the assertion as the value ``Principal``. + +Our strategy will be to use a regular expression identify the user and +realm components and then assign them to local variables which will +then populate the mapped result. + +The mapping in JSON is: + +:: + + { + "user": "$username", + "realm": "$domain" + } + +The assertion in JSON is: + +:: + + { + "Principal": "bob@example.com" + } + +Our rule is: + +:: + + [ + [ + ["in", "Principal", "assertion"], + ["exit", "rule_fails", "if_not_success"], + ["regexp", "$assertion[Principal]", (?P\\w+)@(?P.+)"], + ["set", "$username", "$regexp_map[username]"], + ["set", "$domain", "$regexp_map[domain]"], + ["exit, "rule_succeeds", "always"] + ] + ] + +Rule explanation: + +Block 0: + +0. Test if the assertion contains a Principal value. +1. Abort the rule if the assertion does not contain a Principal + value. +2. Apply a regular expression the the Principal value. Use named + groupings for the username and domain components for clarity. +3. Assign the regexp group username to the $username local variable. +4. Assign the regexp group domain to the $domain local variable. +5. Exit the rule, apply the mapping, return the mapped values. Note, an + explicit `exit`_ is not required if there are no further statements + in the rule, as is the case here. + +The mapped result in JSON is: + +:: + + { + "user": "bob", + "realm": "example.com" + } + +Build a set of roles based on group membership +---------------------------------------------- + +Often one wants to grant roles to a user based on their membership in +certain groups. In this example let's say the assertion contains a +``Groups`` value which is a colon separated list of group names. Our +strategy is to split the ``Groups`` assertion value into an array of +group names. Then we'll test if a specific group is in the groups +array, if it is we'll add a role. Finally if no roles have been mapped +we fail. Users in the group "student" will get the role "unprivileged" +and users in the group "helpdesk" will get the role "admin". + +The mapping in JSON is: + +:: + + { + "roles": "$roles", + } + +The assertion in JSON is: + +:: + + { + "Groups": "student:helpdesk" + } + +Our rule is: + +:: + + [ + [ + ["in", "Groups", "assertion"], + ["exit", "rule_fails", "if_not_success"], + ["set", "$roles", []], + ["split", "$groups", "$assertion[Groups]", ":"], + ], + [ + ["in", "student", "$groups"], + ["continue", "if_not_success"], + ["append", "$roles", "unprivileged"] + ], + [ + ["in", "helpdesk", "$groups"], + ["continue", "if_not_success"], + ["append", "$roles", "admin"] + ], + [ + ["unique", "$roles", "$roles"], + ["length", "$temp", "roles"], + ["compare", $temp", ">", 0], + ["exit", "rule_fails", "if_not_success"] + ] + + ] + +Rule explanation: + +Block 0 + +0. Test if the assertion contains a Groups value. +1. Abort the rule if the assertion does not contain a Groups + value. +2. Initialize the $roles variable to an empty array. +3. Split the colon separated list of group names into an array of + individual group names + +Block 1 + +0. Test if "student" is in the $groups array +1. Exit the block if it's not. +2. Append "unprivileged" to the $roles array + +Block 2 + +0. Test if "helpdesk" is in the $groups array +1. Exit the block if it's not. +2. Append "admin" to the $roles array + +Block 3 + +0. Strip any duplicate roles that might have been appended to the + $roles array to assure each role is unique. +1. Count how many members are in the $roles array, assign the + length to the $temp variable. +2. Test to see if the $roles array had any members. +3. Fail if no roles had been assigned. + +The mapped result in JSON is: + +:: + + { + "roles": ["unprivileged", "admin"] + } + +However, suppose whatever is receiving your mapped results is not +expecting an array of roles. Instead it expects a comma separated list +in a string. To accomplish this add the following statement as the +last one in the final block: + +:: + + ["join", "$roles", "$roles", ","] + +Then the mapped result will be: + +:: + + { + "roles": "unprivileged,admin"] + } + + + + +White list certain users and grant them specific roles +------------------------------------------------------ + +Suppose you have certain users you always want to unconditionally +accept and authorize with specific roles. For example if the user is +"head_of_IT" then assign her the "user" and "admin" roles. Otherwise +keep processing. The list of white listed users is hard-coded into the +rule. + +The mapping in JSON is: + +:: + + { + "user": $user, + "roles": "$roles", + } + +The assertion in JSON is: + +:: + + { + "UserName": "head_of_IT" + } + +Our rule in JSON is: + +:: + + [ + [ + ["in", "UserName", "assertion"], + ["exit", "rule_fails", "if_not_success"], + ["in", "$assertion[UserName]", ["head_of_IT", "head_of_Engineering"]], + ["continue", "if_not_success"], + ["set", "$user", "$assertion[UserName"] + ["set", "$roles", ["user", "admin"]], + ["exit", "rule_succeeds", "always"] + ], + [ + ... + ] + ] + +Rule explanation: + +Block 0 + +0. Test if the assertion contains a UserName value. +1. Abort the rule if the assertion does not contain a UserName + value. +2. Test if the user is in the hardcoded list of white listed users. +3. If the user isn't in the white listed array then exit the block and + continue execution at the next block. +4. Set the $user local variable to $assertion[UserName] +5. Set the $roles local variable to the hardcoded array containing + "user" and "admin" +6. We're done, unconditionally exit and return the mapped result. + +Block 1 + +0. Further processing + +The mapped result in JSON is: + +:: + + { + "user": "head_of_IT", + "roles": ["users", "admin"] + } + + +Black list certain users +------------------------ + +Suppose you have certain users you always want to unconditionally +deny access to by placing them in a black list. In this example the +user "BlackHat" will try to gain access. The black list includes the +users "BlackHat" and "Spook". + +The mapping in JSON is: + +:: + + { + "user": $user, + "roles": "$roles", + } + +The assertion in JSON is: + +:: + + { + "UserName": "BlackHat" + } + +Our rule in JSON is: + +:: + + [ + [ + ["in", "UserName", "assertion"], + ["exit", "rule_fails", "if_not_success"], + ["in", "$assertion[UserName]", ["BlackHat", "Spook"]], + ["exit", "rule_fails", "if_success"] + ], + [ + ... + ] + ] + +Rule explanation: + +Block 0 + +0. Test if the assertion contains a UserName value. +1. Abort the rule if the assertion does not contain a UserName + value. +2. Test if the user is in the hard-coded list of black listed users. +3. If the test succeeds then immediately abort and return failure. + +Block 1 + +0. Further processing + +The mapped result in JSON is: + +:: + + Null + +Format Strings and/or Concatenate Strings +----------------------------------------- + +You can replace variables in a format string using the `interpolate`_ +verb. String concatenation is trivially placing two variables adjacent +to one another in a format string. Suppose you want to form an email +address from the username and domain in an assertion. + +The mapping in JSON is: + +:: + + { + "email": $email, + } + +The assertion in JSON is: + +:: + + { + "UserName": "Bob", + "Domain": "example.com" + } + +Our rule in JSON is: + +:: + + [ + [ + ["interpolate", "$email", "$assertion[UserName]@$assertion[Domain]"], + ] + ] + +Rule explanation: + +Block 0 + +0. Replace the variable $assertion[UserName] with it's value and + replace the variable $assertion[Domain] with it's value. + +The mapped result in JSON is: + +:: + + { + "email": "Bob@example.com", + } + + +Note, sometimes it's necessary to utilize braces to separate variables +from surrounding text by using the brace notation. This can also make +the format string more readable. Using braces to delimit variables the +above would be: + +:: + + [ + [ + ["interpolate", "$email", "${assertion[UserName]}@${assertion[Domain]}"], + ] + ] + + + +Make associative array lookups case insensitive +----------------------------------------------- + +Many systems treat field names as case insensitive. By default +associative array indexing is case sensitive. The solution is to lower +case all the keys in an associative array and then only use lower case +indices. Suppose you want the assertion associative array to be case +insensitive. + +The mapping in JSON is: + +:: + + { + "user": $user, + } + +The assertion in JSON is: + +:: + + { + "UserName": "Bob" + } + +Our rule in JSON is: + +:: + + [ + [ + ["lower", "$assertion", "$assertion"], + ["in", "username", "assertion"], + ["exit", "rule_fails", "if_not_success"], + ["set", "$user", "$assertion[username"] + ] + ] + +Rule explanation: + +Block 0 + +0. Lower case all the keys in the assertion associative array. +1. Test if the assertion contains a username value. +2. Abort the rule if the assertion does not contain a username + value. +3. Assign the username value in the assertion to $user + +The mapped result in JSON is: + +:: + + { + "user": "Bob", + } + + +Verbs +===== + +The following verbs are supported: + +* `set`_ +* `length`_ +* `interpolate`_ +* `append`_ +* `unique`_ +* `regexp`_ +* `regexp_replace`_ +* `split`_ +* `join`_ +* `lower`_ +* `upper`_ +* `compare`_ +* `in`_ +* `not_in`_ +* `exit`_ +* `continue`_ + +Some verbs have a side effects. A verb may set a boolean success/fail +result which may then be tested with a subsequent verb. For example +the ``fail`` verb can be used to indicate the rule fails if a prior +result is either ``success`` or ``not_success``. The ``regexp`` verb +which performs a regular expression search on a string stores the +regular expression sub-matches as a side effect in the variables +``$regexp_array`` and ``$regexp_map``. + + +Verb Definitions +================ + +set +--- + +``set $variable value`` + +$variable + The variable being assigned (i.e. lhs) + +value + The value to assign to the variable (i.e. rhs). The value may be + another variable or a constant. + +**set** assigns a value to a variable, in other words it's an +assignment statement. + +Examples: +^^^^^^^^^ + +Initialize a variable to an empty array. + +:: + + ["set", "$groups", []] + +Initialize a variable to an empty associative array. + +:: + + ["set", "$groups", {}] + +Assign a string. + +:: + + ["set", "$version", "1.2.3"] + +Copy the ``UserName`` value from the assertion to a temporary variable. + +:: + + ["set", "$temp", "$assertion[UserName]"], + + +Get the 2nd item in an array (array indexing is zero based) + +:: + + ["set", "$group", "$groups[1]"] + + +Set the associative array entry "IdP" to "kdc.example.com". + +:: + + ["set", "$metadata[IdP]", "kdc.example.com""] + +-------------------------------------------------------------------------------- + +length +------ + +``length $variable value`` + +$variable + The variable which receives the length value + +value + The value whose length is to be determined. May be one of array, + associative array, or string. + +**length** computes the number of items in the value. How this is done +depends upon the type of value: + +array + The length is the number of items in the array. + +associative array + The length is the number of key/value pairs in the associative + array. + +string + The length is the number of *characters* (not octets) in the + string. + +Examples: +^^^^^^^^^ + +Count how many items are in the ``$groups`` array and assign that +value to the ``$groups_length`` variable. + +:: + + ["length", "$groups_length", "$groups"] + +Count how many key/value pairs are in the ``$assertion`` associative +array and assign that value to the ``$num_assertion_values`` variable. + +:: + + ["length", "$num_assertion_values", "$assertion"] + +Count how many characters are in the assertion's UserName and assign +the value to ``$username_length``. + +:: + + ["length", "$user_name_length", "$assertion[UserName]"] + + +-------------------------------------------------------------------------------- + +interpolate +----------- + +``interpolate $variable string`` + +$variable + This variable is assigned the result of the interpolation. + +string + A string containing references to variables which will be replaced + in the string. + +**interpolate** replaces each occurrence of a variable in a string with +it's value. The result is assigned to $variable. + +Examples: +^^^^^^^^^ + +Form an email address given the username and domain. If the username +is "jane" and the domain is "example.com" then $email will be +"jane@example.com" + +:: + + ["interpolate", "$email", "${username}@${domain}"] + + +-------------------------------------------------------------------------------- + + +append +------ + +``append $variable value`` + +$variable + This variable **must** be an array. It is modified in place by + appending ``value`` to the end of the array. + +value + The value to append to the end of the array. + +**append** adds a value to end of an array. + +Examples: +^^^^^^^^^ + +Append the role "qa_test" to the roles list. + +:: + + ["append", "$roles", "qa_test"] + + +-------------------------------------------------------------------------------- + + +unique +------ + +``unique $variable value`` + +$variable + This variable is assigned the unique values in the ``value`` + array. + +value + An array of values. **must** be an array. + +**unique** builds an array of unique values in ``value`` by stripping +out duplicates and assigns the array of unique values to +``$variable``. The order of items in the ``value`` array are +preserved. + +Examples: +^^^^^^^^^ + +$one_of_a_kind will be assigned ["a", "b"] + +:: + + ["unique", "$one_of_a_kind", ["a", "b", "a"]] + + +-------------------------------------------------------------------------------- + +regexp +------ + +``regexp string pattern`` + +string + The string the regular expression pattern is applied to. + +pattern + The regular expression pattern. + +**regexp** performs a regular expression match against ``string``. The +regular expression pattern syntax is defined by the regular expression +implementation of the language this API is written in. + +Pattern groups are a convenient way to select sub-matches. Pattern +groups may accessed by either group number or group name. After a +successful regular expression match the groups are stored in the +special variables ``$regexp_array`` and +``$regexp_map``. + +``$regexp_array`` is used to access the groups by +numerical index. Groups are numbered by counting the left parenthesis +group delimiter starting at 1. Group 0 is the entire +match. ``$regexp_array`` is valid irregardless of whether you used +named groups or not. + +``$regexp_map`` is used to access the groups by +name. ``$regexp_map`` is only valid if you used named groups in the +pattern. + +Examples: +^^^^^^^^^ + +Many user names are of the form "user@domain", to split the username +from the domain and to be able to work with those values independently +use a regular expression and then assign the results to a variable. In +this example there are two regular expression groups, the first group +is the username and the second group is the domain. In the first +example we use named groups and then access the match information in +the special variable ``$regexp_map`` via the name of the group. + +:: + + ["regexp", "$assertion[UserName]", "(?P\\w+)@(?P.+)"], + ["continue", "if_not_success"], + ["set", "$username", "$regexp_map[username]"], + ["set", "$domain", "$regexp_map[domain]"], + + +This is exactly equivalent but uses numbered groups instead of named +groups. In this instance the group matches are stored in the special +variable ``$regexp_array`` and accessed by numerical index. + +:: + + ["regexp", "$assertion[UserName]", "(\\w+)@(.+)"], + ["continue", "if_not_success"], + ["set", "$username", "$regexp_array[1]"], + ["set", "$domain", "$regexp_array[2]"], + + + +-------------------------------------------------------------------------------- + +regexp_replace +-------------- + +``regexp_replace $variable string pattern replacement`` + +$variable + The variable which receives result of the replacement. + +string + The string to perform the replacement on. + +pattern + The regular expression pattern. + +replacement + The replacement specification. + +**regexp_replace** replaces each occurrence of ``pattern`` in +``$string`` with ``replacement``. See `regexp`_ for details of using +regular expressions. + +Examples: +^^^^^^^^^ + +Convert hyphens in a name to underscores. + +:: + + ["regexp_replace", "$name", "$name", "-", "_"] + + +-------------------------------------------------------------------------------- + +split +----- + +``split $variable string pattern`` + +$variable + This variable is assigned an array containing the split items. + +string + The string to split into separate items. + +pattern + The regular expression pattern used to split the string. + +**split** splits ``string`` into separate pieces and assigns the +result to ``$variable`` as an array of pieces. The split occurs +wherever the regular expression ``pattern`` occurs in ``string``. See +`regexp`_ for details of using regular expressions. + +Examples: +^^^^^^^^^ + +Split a list of groups separated by a colon (:) into an array of +individual group names. If $assertion[Groups] contained the string +"user:admin" then $group_list will set to ["user", "admin"]. + +:: + + ["split", "$group_list", "$assertion[Groups]", ":"] + + + +-------------------------------------------------------------------------------- + +join +---- + +``join $variable array join_string`` + +$variable + This variable is assigned the string result of the join operation. + +array + An array of string items to be joined together with + ``$join_string``. + +join_string + The string inserted between each element in ``array``. + +**join** accepts an array of strings and produces a single string +where each element in the array is separated by ``join_string``. + +Examples: +^^^^^^^^^ + +Convert a list of group names into a single string where each group +name is separated by a colon (:). If the array ``$group_list`` is +["user", "admin"] and the ``join_string`` is ":" then the +``$group_string`` variable will be set to "user:admin". + +:: + + ["join", "$group_string", "$groups", ":"] + + +-------------------------------------------------------------------------------- + +lower +----- + +``lower $variable value`` + +$variable + This variable is assigned the result of the lower operation. + +value + The value to lower case, may be either a string, array, or + associative array. + +**lower** lower cases the input value. The input value may be one of +the following types: + +string + The string is lower cased. + +array + Each member of the array must be a string, the result is an array + with the items replaced by their lower case value. + +associative array + Each key in the associative array is lower cased. The values + associated with the key are **not** modified. + +Examples: +^^^^^^^^^ + +Lookup ``UserName`` in the assertion and set the variable +``$username`` to it's lower case value. + +:: + + ["lower", "$username", "$assertion[UserName]"], + +Set each member of the ``$groups`` array to it's lower case value. If +``$groups`` was ["User", "Admin"] then ``$groups`` will become +["user", "admin"]. + +:: + + ["lower", "$groups", "$groups"], + +To enable case insensitive lookup's in an associative array lower case +each key in the associative array. If ``$assertion`` was {"UserName": +"JoeUser"} then ``$assertion`` will become {"username": "JoeUser"} + +:: + + ["lower", "$assertion", $assertion"] + +-------------------------------------------------------------------------------- + +upper +----- + +``upper $variable value`` + +$variable + This variable is assigned the result of the upper operation. + +value + The value to upper case, may be either a string, array, or + associative array. + +**upper** is exactly analogous to `lower`_ except the values are upper +cased, see `lower`_ for details. + + +-------------------------------------------------------------------------------- + +in +-- + +``in member collection`` + +member + The value whose membership is being tested. + +collection + A collection of members. May be string, array or associative array. + +**in** tests to see if ``member`` is a member of ``collection``. The +membership test depends on the type of collection, the following are +supported: + +array + If any item in the array is equal to ``member`` then the result is + success. + +associative array + If the associative array contains a key equal to ``member`` then + the result is success. + +string + If the string contains a sub-string equal to ``member`` then the + result is success. + +Examples: +^^^^^^^^^ + +Test to see if the assertion contains a UserName value. + +:: + + ["in", "UserName", "$assertion"] + ["continue", "if_not_success"] + +Test to see if a group is one of "user" or "admin". + +:: + + ["in", "$group", ["user", "admin"]] + ["continue", "if_not_success"] + +Test to see if the sub-string "BigCorp" is in +the assertion's ``Provider`` value. + +:: + + ["in", "BigCorp", "$assertion[Provider]"] + ["continue", "if_not_success"] + + +-------------------------------------------------------------------------------- + +not_in +------ + +``in member collection`` + +member + The value whose membership is being tested. + +collection + A collection of members. May be string, array or associative array. + +**not_in** is exactly analogous to `in`_ except the sense of the test +is reversed. See `in`_ for details. + +-------------------------------------------------------------------------------- + +compare +------- + +``compare left operator right`` + +left + The left hand value of the binary operator. + +operator + The binary operator used for comparing left to right. + +right + The right hand value of the binary operator. + + +**compare** compares the left value to the right value according the +operator and sets success if the comparison evaluates to True. The +following relational operators are supported. + ++----------+-----------------------+ +| Operator | Description | ++==========+=======================+ +| == | equal | ++----------+-----------------------+ +| != | not equal | ++----------+-----------------------+ +| < | less than | ++----------+-----------------------+ +| <= | less than or equal | ++----------+-----------------------+ +| > | greater than | ++----------+-----------------------+ +| >= | greater than or equal | ++----------+-----------------------+ + + +The left and right hand sides of the comparison operator *must* be +the same type, no type conversions are performed. Not all combinations +of operator and type are supported. The table below illustrates the +supported combinations. Essentially you can test for equality or +inequality on any type. But only strings and numbers support the +magnitude relational operators. + + ++----------+--------+---------+------+---------+-----+------+------+ +| Operator | STRING | INTEGER | REAL | BOOLEAN | MAP | LIST | NULL | ++==========+========+=========+======+=========+=====+======+======+ +| == | X | X | X | X | X | X | X | ++----------+--------+---------+------+---------+-----+------+------+ +| != | X | X | X | X | X | X | X | ++----------+--------+---------+------+---------+-----+------+------+ +| < | X | X | X | | | | | ++----------+--------+---------+------+---------+-----+------+------+ +| <= | X | X | X | | | | | ++----------+--------+---------+------+---------+-----+------+------+ +| > | X | X | X | | | | | ++----------+--------+---------+------+---------+-----+------+------+ +| >= | X | X | X | | | | | ++----------+--------+---------+------+---------+-----+------+------+ + + +Examples: +^^^^^^^^^ + +Test to see if the ``$groups`` array has at least 2 members + +:: + + ["length", "$group_length", "$groups"], + ["compare", "$group_length", ">=", 2] + + +-------------------------------------------------------------------------------- + +exit +---- + +``exit status criteria`` + +status + The result for the rule. + +criteria + The criteria upon which will cause the rule will be immediately + exited with a failed status. + +**exit** causes the rule being executed to immediately exit and a rule +result if the specified criteria is met. Statement verbs such as `in`_ +or `compare`_ set the result status which may be tested with the +``success`` and ``not_success`` criteria. + +The exit ``status`` may be one of: + +rule_fails + The rule has failed and no mapping will occur. + +rule_succeeds + The rule succeeded and the mapping will be applied. + +The ``criteria`` may be one of: + +if_success + If current result status is success then exit with ``status``. + +if_not_success + If current result status is not success then exit with ``status``. + +always + Unconditionally exit with ``status``. + +never + Effectively a no-op. Useful for debugging. + +Examples: +^^^^^^^^^ + +The rule requires ``UserName`` to be in the assertion. + +:: + + ["in", "UserName", "$assertion"] + ["exit", "rule_fails", "if_not_success"] + +-------------------------------------------------------------------------------- + + +continue +-------- + +``continue criteria`` + +criteria + The criteria which causes the remainder of the *block* to be + skipped. + +**continue** is used to control execution for statement blocks. It +mirrors in a crude way the `if` expression in a procedural +language. ``continue`` does *not* affect the success or failure of a +rule, rather it controls whether subsequent statements in a block are +executed or not. Control continues at the next statement block. + +Statement verbs such as `in`_ or `compare`_ set the result status +which may be tested with the ``success`` and ``not_success`` criteria. + +The criteria may be one of: + +if_success + If current result status is success then exit the statement + block and continue execution at the next statement block. + +if_not_success + If current result status is not success then exit the statement + block and continue execution at the next statement block. + +always + Immediately exit the statement block and continue execution at the + next statement block. + +never + Effectively a no-op. Useful for debugging. Execution continues at + the next statement. + +Examples: +^^^^^^^^^ + +The following pseudo code: + +:: + + roles = []; + if ("Groups" in assertion) { + groups = assertion["Groups"].split(":"); + if ("qa_test" in groups) { + roles.append("tester"); + } + } + +could be implemented this way: + +:: + + [ + ["set", "$roles", []], + ["in", "Groups", "$assertion"], + ["continue", "if_not_success"], + ["split" "$groups", $assertion[Groups]", ":"], + ["in", "qa_test", "$groups"], + ["continue", "if_not_success"], + ["append", "$roles", "tester"] + ] diff --git a/odl-aaa-moon/aaa-authn-api/src/main/docs/resource_access_sequence.png b/odl-aaa-moon/aaa-authn-api/src/main/docs/resource_access_sequence.png new file mode 100644 index 0000000000000000000000000000000000000000..728b86cebae2353c42ab382e9e95ab417479cd80 GIT binary patch literal 38693 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 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-authn-api/src/main/docs/sssd_01.diag b/odl-aaa-moon/aaa-authn-api/src/main/docs/sssd_01.diag new file mode 100644 index 00000000..28317393 --- /dev/null +++ b/odl-aaa-moon/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-authn-api/src/main/docs/sssd_01.svg b/odl-aaa-moon/aaa-authn-api/src/main/docs/sssd_01.svg new file mode 100644 index 00000000..4056b10a --- /dev/null +++ b/odl-aaa-moon/aaa-authn-api/src/main/docs/sssd_01.svg @@ -0,0 +1,32 @@ + + + + + + + + + blockdiag + blockdiag { + User <-> AAA; + User [numbered = 1, shape = actor] + AAA [numbered = 2, label = "App Server\nAAA"] +} + + + + + + + + + 1 + + App Server + AAA + + 2 + + + + diff --git a/odl-aaa-moon/aaa-authn-api/src/main/docs/sssd_02.diag b/odl-aaa-moon/aaa-authn-api/src/main/docs/sssd_02.diag new file mode 100644 index 00000000..2076dd16 --- /dev/null +++ b/odl-aaa-moon/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-authn-api/src/main/docs/sssd_02.svg b/odl-aaa-moon/aaa-authn-api/src/main/docs/sssd_02.svg new file mode 100644 index 00000000..42196b60 --- /dev/null +++ b/odl-aaa-moon/aaa-authn-api/src/main/docs/sssd_02.svg @@ -0,0 +1,79 @@ + + + + + + + + + blockdiag + 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"] +} + + + + + + + + + + + + + + 1 + + Apache + Authenticates user + + 2 + + SSSD + Provides user info + + 3 + + Proxy Transport + Request + Metadata + + 4 + + App Server + AAA + + 5 + + + + + + + + + + + + + + + + + + + diff --git a/odl-aaa-moon/aaa-authn-api/src/main/docs/sssd_03.diag b/odl-aaa-moon/aaa-authn-api/src/main/docs/sssd_03.diag new file mode 100644 index 00000000..6ece3760 --- /dev/null +++ b/odl-aaa-moon/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-authn-api/src/main/docs/sssd_03.svg b/odl-aaa-moon/aaa-authn-api/src/main/docs/sssd_03.svg new file mode 100644 index 00000000..91e8b1be --- /dev/null +++ b/odl-aaa-moon/aaa-authn-api/src/main/docs/sssd_03.svg @@ -0,0 +1,143 @@ + + + + + + + + + blockdiag + 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"]; + +} + + + + + + + + + + + + + + + + + + + + + + + + + + Client + + Apache + + SSSD + + IdP + + Container + + + + + + + + + + + + + + + + + + Only if + not cached + by SSSD + + + + + + + + + + + + + + + Request + 401 Unauthorized + Authorization: Credentials + Set + User Name + Auth Type + Get User Info + Get User Info + Return User Info + Return User Info + Set User specific + environment + variables + Proxy With User's Metadata + Response + Response + + + + + + Apache mod_auth_kerb + + + + + + Apache mod_lookup_identity + + + + + + Apache mod_proxy + diff --git a/odl-aaa-moon/aaa-authn-api/src/main/docs/sssd_04.diag b/odl-aaa-moon/aaa-authn-api/src/main/docs/sssd_04.diag new file mode 100644 index 00000000..8f69a0b8 --- /dev/null +++ b/odl-aaa-moon/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-authn-api/src/main/docs/sssd_04.svg b/odl-aaa-moon/aaa-authn-api/src/main/docs/sssd_04.svg new file mode 100644 index 00000000..74850a85 --- /dev/null +++ b/odl-aaa-moon/aaa-authn-api/src/main/docs/sssd_04.svg @@ -0,0 +1,100 @@ + + + + + + + + + blockdiag + 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] +} + + + + + + + + + + + + + Connector + + 1 + + SssdFilter + + 2 + + ClaimAuthFilter + + 3 + + SssdClaimAuth + + 4 + + Assertion + + 4.1 + + JsonAssertion + + 4.2 + + IdPMapper + + 4.3 + + JsonMapped + + 4.4 + + Claim + + 5 + + + + + + + + + + + + + + + + + + + diff --git a/odl-aaa-moon/aaa-authn-api/src/main/docs/sssd_05.svg b/odl-aaa-moon/aaa-authn-api/src/main/docs/sssd_05.svg new file mode 100644 index 00000000..f4657f06 --- /dev/null +++ b/odl-aaa-moon/aaa-authn-api/src/main/docs/sssd_05.svg @@ -0,0 +1,613 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + Apache mod_proxy:forward port 8383 + + + + Connector:port = 80(web) + + + + Connector:port = 8383(auth proxy) + + + + + + + + + + + + AAA Servletexecuteswith roles + + + Non-AAAServlet + + + + ClaimAuthFilter:localPort insecureProxyPorts? + + + No + + + + Yes + + + + + + + + + + + + Java EE Container + + + + + + + + + diff --git a/odl-aaa-moon/aaa-authn-api/src/main/docs/sssd_auth_sequence.png b/odl-aaa-moon/aaa-authn-api/src/main/docs/sssd_auth_sequence.png new file mode 100644 index 0000000000000000000000000000000000000000..9f9a0b496c7a40f51e7b14efe7903f96dba015a2 GIT binary patch literal 39322 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 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-authn-api/src/main/docs/sssd_configuration.rst b/odl-aaa-moon/aaa-authn-api/src/main/docs/sssd_configuration.rst new file mode 100644 index 00000000..7f912d94 --- /dev/null +++ b/odl-aaa-moon/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: + +:: + + + AuthType Kerberos + AuthName "Kerberos Login" + KrbMethodNegotiate On + KrbMethodK5Passwd Off + KrbAuthRealms EXAMPLE.COM + Krb5KeyTab /etc/http.keytab + require valid-user + + +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. + + :: + + + LookupUserAttr mail REMOTE_USER_EMAIL + + +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: + + :: + + + LookupUserAttr mail AJP_REMOTE_USER_EMAIL + + +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. + + :: + + + LookupUserAttr mail REMOTE_USER_EMAIL + RequestHeader set X-SSSD-REMOTE_USER_EMAIL %{REMOTE_USER_EMAIL}e + + +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: + +:: + + + + 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 ":" + + + +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: + +:: + + + + 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 + + + +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. + +:: + + + + +: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. + +:: + + + + + + 127.0.0.1 + 8383 + 300000 + 2 + false + 8445 + federationConn + 20000 + 5000 + + + + +: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: + +:: + + + + + + + + + +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]", "(?\\w+)@(?.+)"], + 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: + +:: + + + + + +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-authn-api/src/main/java/org/opendaylight/aaa/api/Authentication.java b/odl-aaa-moon/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-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-authn-api/src/main/java/org/opendaylight/aaa/api/AuthenticationException.java b/odl-aaa-moon/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-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-authn-api/src/main/java/org/opendaylight/aaa/api/AuthenticationService.java b/odl-aaa-moon/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-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-authn-api/src/main/java/org/opendaylight/aaa/api/Claim.java b/odl-aaa-moon/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-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 roles(); +} \ No newline at end of file diff --git a/odl-aaa-moon/aaa-authn-api/src/main/java/org/opendaylight/aaa/api/ClaimAuth.java b/odl-aaa-moon/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-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 + * "USER_NAME" -> "joe". + *

+ * If there is no applicable claim information for the current + * implementation, this method should return a null. + *

+ * 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 claim); +} diff --git a/odl-aaa-moon/aaa-authn-api/src/main/java/org/opendaylight/aaa/api/ClientService.java b/odl-aaa-moon/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-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-authn-api/src/main/java/org/opendaylight/aaa/api/CredentialAuth.java b/odl-aaa-moon/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-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 { + + /** + * 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-authn-api/src/main/java/org/opendaylight/aaa/api/Credentials.java b/odl-aaa-moon/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-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-authn-api/src/main/java/org/opendaylight/aaa/api/IDMStoreException.java b/odl-aaa-moon/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-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-authn-api/src/main/java/org/opendaylight/aaa/api/IDMStoreUtil.java b/odl-aaa-moon/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-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-authn-api/src/main/java/org/opendaylight/aaa/api/IIDMStore.java b/odl-aaa-moon/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-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-authn-api/src/main/java/org/opendaylight/aaa/api/IdMService.java b/odl-aaa-moon/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-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 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 listRoles(String userId, String domain); +} diff --git a/odl-aaa-moon/aaa-authn-api/src/main/java/org/opendaylight/aaa/api/PasswordCredentials.java b/odl-aaa-moon/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-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-authn-api/src/main/java/org/opendaylight/aaa/api/SHA256Calculator.java b/odl-aaa-moon/aaa-authn-api/src/main/java/org/opendaylight/aaa/api/SHA256Calculator.java new file mode 100644 index 00000000..81f4b899 --- /dev/null +++ b/odl-aaa-moon/aaa-authn-api/src/main/java/org/opendaylight/aaa/api/SHA256Calculator.java @@ -0,0 +1,83 @@ +/* + * 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 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(); + } + return removeSpecialCharacters(new String(by)); + } + + public static String getSHA256(String password, String salt) { + return getSHA256(password.getBytes(), salt); + } + + public static String removeSpecialCharacters(String str) { + StringBuilder buff = new StringBuilder(); + for (int i = 0; i < str.length(); i++) { + if (str.charAt(i) != '\'' && str.charAt(i) != 0) { + buff.append(str.charAt(i)); + } + } + return buff.toString(); + } +} diff --git a/odl-aaa-moon/aaa-authn-api/src/main/java/org/opendaylight/aaa/api/TokenAuth.java b/odl-aaa-moon/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-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. + *

+ * 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> headers) throws AuthenticationException; + +} diff --git a/odl-aaa-moon/aaa-authn-api/src/main/java/org/opendaylight/aaa/api/TokenStore.java b/odl-aaa-moon/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-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-authn-api/src/main/java/org/opendaylight/aaa/api/model/Claim.java b/odl-aaa-moon/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-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 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 getRoles() { + return roles; + } + + public void setRoles(List roles) { + this.roles = roles; + } + +} \ No newline at end of file diff --git a/odl-aaa-moon/aaa-authn-api/src/main/java/org/opendaylight/aaa/api/model/Domain.java b/odl-aaa-moon/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-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-authn-api/src/main/java/org/opendaylight/aaa/api/model/Domains.java b/odl-aaa-moon/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-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 domains = new ArrayList(); + + public void setDomains(List domains) { + this.domains = domains; + } + + public List getDomains() { + return domains; + } + +} diff --git a/odl-aaa-moon/aaa-authn-api/src/main/java/org/opendaylight/aaa/api/model/Grant.java b/odl-aaa-moon/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-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-authn-api/src/main/java/org/opendaylight/aaa/api/model/Grants.java b/odl-aaa-moon/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-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 grants = new ArrayList(); + + public void setGrants(List grants) { + this.grants = grants; + } + + public List getGrants() { + return grants; + } + +} diff --git a/odl-aaa-moon/aaa-authn-api/src/main/java/org/opendaylight/aaa/api/model/IDMError.java b/odl-aaa-moon/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-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-authn-api/src/main/java/org/opendaylight/aaa/api/model/Role.java b/odl-aaa-moon/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-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-authn-api/src/main/java/org/opendaylight/aaa/api/model/Roles.java b/odl-aaa-moon/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-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 roles = new ArrayList(); + + public void setRoles(List roles) { + this.roles = roles; + } + + public List getRoles() { + return roles; + } + +} diff --git a/odl-aaa-moon/aaa-authn-api/src/main/java/org/opendaylight/aaa/api/model/User.java b/odl-aaa-moon/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-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-authn-api/src/main/java/org/opendaylight/aaa/api/model/UserPwd.java b/odl-aaa-moon/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-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-authn-api/src/main/java/org/opendaylight/aaa/api/model/Users.java b/odl-aaa-moon/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-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 users = new ArrayList(); + + public void setUsers(List users) { + this.users = users; + } + + public List getUsers() { + return users; + } + +} diff --git a/odl-aaa-moon/aaa-authn-api/src/main/java/org/opendaylight/aaa/api/model/Version.java b/odl-aaa-moon/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-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-authn-basic/pom.xml b/odl-aaa-moon/aaa-authn-basic/pom.xml new file mode 100644 index 00000000..f98e6294 --- /dev/null +++ b/odl-aaa-moon/aaa-authn-basic/pom.xml @@ -0,0 +1,76 @@ + + + 4.0.0 + + org.opendaylight.aaa + aaa-parent + 0.3.1-Beryllium-SR1 + ../parent + + + aaa-authn-basic + bundle + + + + org.opendaylight.aaa + aaa-authn + + + org.opendaylight.aaa + aaa-authn-api + + + org.slf4j + slf4j-api + + + com.sun.jersey + jersey-server + provided + + + org.osgi + org.osgi.core + provided + + + org.apache.felix + org.apache.felix.dependencymanager + provided + + + + junit + junit + test + + + org.slf4j + slf4j-simple + test + + + org.mockito + mockito-all + test + + + + + + + org.apache.felix + maven-bundle-plugin + true + + + org.opendaylight.aaa.basic.Activator + + ${project.basedir}/META-INF + + + + + diff --git a/odl-aaa-moon/aaa-authn-basic/src/main/java/org/opendaylight/aaa/basic/Activator.java b/odl-aaa-moon/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-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-authn-basic/src/main/java/org/opendaylight/aaa/basic/HttpBasicAuth.java b/odl-aaa-moon/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-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: + * Authorization: Basic BASE_64_ENCODED_CREDENTIALS + * + * Where BASE_64_ENCODED_CREDENTIALS is the base 64 encoded value + * of the user's credentials in the following form: user:password + * + * For example, assuming the user is "admin" and the password is "admin": + * Authorization: Basic YWRtaW46YWRtaW4= + * + * @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 credentialAuth; + + private static boolean checkAuthHeaderFormat(final String authHeader) { + return (authHeader != null && authHeader.startsWith(BASIC_PREFIX)); + } + + private static String extractAuthHeader(final Map> 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 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> 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-authn-basic/src/test/java/org/opendaylight/aaa/basic/HttpBasicAuthTest.java b/odl-aaa-moon/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-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> 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> 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> 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> 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> 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-authn-federation/pom.xml b/odl-aaa-moon/aaa-authn-federation/pom.xml new file mode 100644 index 00000000..0e84e185 --- /dev/null +++ b/odl-aaa-moon/aaa-authn-federation/pom.xml @@ -0,0 +1,132 @@ + + + 4.0.0 + + org.opendaylight.aaa + aaa-parent + 0.3.1-Beryllium-SR1 + ../parent + + + aaa-authn-federation + bundle + + + + org.opendaylight.aaa + aaa-authn-api + + + org.opendaylight.aaa + aaa-authn + + + org.slf4j + slf4j-api + + + com.sun.jersey + jersey-server + provided + + + javax.servlet + javax.servlet-api + provided + + + org.apache.oltu.oauth2 + org.apache.oltu.oauth2.authzserver + provided + + + org.apache.oltu.oauth2 + org.apache.oltu.oauth2.common + provided + + + org.apache.oltu.oauth2 + org.apache.oltu.oauth2.resourceserver + provided + + + org.osgi + org.osgi.core + provided + + + org.apache.felix + org.apache.felix.dependencymanager + provided + + + + com.sun.jersey.jersey-test-framework + jersey-test-framework-grizzly2 + test + + + org.eclipse.jetty + jetty-servlet-tester + test + + + junit + junit + test + + + org.mockito + mockito-all + test + + + org.slf4j + slf4j-simple + test + + + + + + + org.apache.felix + maven-bundle-plugin + true + + + *,com.sun.jersey.spi.container.servlet + /oauth2/federation + federationConn + org.opendaylight.aaa.federation.Activator + ${project.basedir}/META-INF + + + + + org.codehaus.mojo + build-helper-maven-plugin + + + attach-artifacts + package + + attach-artifact + + + + + ${project.build.directory}/classes/federation.cfg + cfg + config + + + + + + + + + + diff --git a/odl-aaa-moon/aaa-authn-federation/src/main/java/org/opendaylight/aaa/federation/Activator.java b/odl-aaa-moon/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-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 + * CredentialAuth 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 addPid(Dictionary dict) { + dict.put(Constants.SERVICE_PID, FEDERATION_PID); + return dict; + } +} diff --git a/odl-aaa-moon/aaa-authn-federation/src/main/java/org/opendaylight/aaa/federation/ClaimAuthFilter.java b/odl-aaa-moon/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-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. + *

+ * 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 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 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 claims(HttpServletRequest req) { + String name; + Object objectValue; + String stringValue; + Map 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 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-authn-federation/src/main/java/org/opendaylight/aaa/federation/FederationConfiguration.java b/odl-aaa-moon/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-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 defaults = new Hashtable<>(); + static { + defaults.put(HTTP_HEADERS, ""); + defaults.put(HTTP_ATTRIBUTES, ""); + } + private static Map configs = new ConcurrentHashMap<>(); + + // singleton + private FederationConfiguration() { + } + + public static FederationConfiguration instance() { + return instance; + } + + @Override + public void updated(Dictionary props) throws ConfigurationException { + if (props == null) { + configs.clear(); + configs.putAll(defaults); + } else { + try { + Enumeration 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 httpHeaders() { + String headers = configs.get(HTTP_HEADERS); + return (headers == null) ? new ArrayList() : Arrays.asList(headers.split(" ")); + } + + public List httpAttributes() { + String attributes = configs.get(HTTP_ATTRIBUTES); + return (attributes == null) ? new ArrayList() : Arrays + .asList(attributes.split(" ")); + } + + public Set secureProxyPorts() { + String ports = configs.get(SECURE_PROXY_PORTS); + Set secureProxyPorts = new TreeSet(); + + if (ports != null && !ports.isEmpty()) { + for (String port : ports.split(" ")) { + secureProxyPorts.add(Integer.parseInt(port)); + } + } + return secureProxyPorts; + } + +} diff --git a/odl-aaa-moon/aaa-authn-federation/src/main/java/org/opendaylight/aaa/federation/FederationEndpoint.java b/odl-aaa-moon/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-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 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-authn-federation/src/main/java/org/opendaylight/aaa/federation/ServiceLocator.java b/odl-aaa-moon/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-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 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 getClaimAuthCollection() { + return claimAuthCollection; + } + + public void setClaimAuthCollection(List 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-authn-federation/src/main/java/org/opendaylight/aaa/federation/SssdFilter.java b/odl-aaa-moon/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-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-authn-federation/src/main/resources/OSGI-INF/metatype/metatype.properties b/odl-aaa-moon/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-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-authn-federation/src/main/resources/OSGI-INF/metatype/metatype.xml b/odl-aaa-moon/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-authn-federation/src/main/resources/OSGI-INF/metatype/metatype.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + diff --git a/odl-aaa-moon/aaa-authn-federation/src/main/resources/WEB-INF/web.xml b/odl-aaa-moon/aaa-authn-federation/src/main/resources/WEB-INF/web.xml new file mode 100644 index 00000000..9fd9751f --- /dev/null +++ b/odl-aaa-moon/aaa-authn-federation/src/main/resources/WEB-INF/web.xml @@ -0,0 +1,34 @@ + + + + + federation + org.opendaylight.aaa.federation.FederationEndpoint + 1 + + + federation + /* + + + + + SssdFilter + org.opendaylight.aaa.federation.SssdFilter + + + SssdFilter + /* + + + ClaimAuthFilter + org.opendaylight.aaa.federation.ClaimAuthFilter + + + ClaimAuthFilter + /* + + + diff --git a/odl-aaa-moon/aaa-authn-federation/src/main/resources/federation.cfg b/odl-aaa-moon/aaa-authn-federation/src/main/resources/federation.cfg new file mode 100644 index 00000000..60ef1c46 --- /dev/null +++ b/odl-aaa-moon/aaa-authn-federation/src/main/resources/federation.cfg @@ -0,0 +1,3 @@ +httpHeaders= +httpAttributes= +secureProxyPorts= diff --git a/odl-aaa-moon/aaa-authn-federation/src/test/java/org/opendaylight/aaa/federation/FederationEndpointTest.java b/odl-aaa-moon/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-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(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-authn-keystone/pom.xml b/odl-aaa-moon/aaa-authn-keystone/pom.xml new file mode 100644 index 00000000..ee1d3278 --- /dev/null +++ b/odl-aaa-moon/aaa-authn-keystone/pom.xml @@ -0,0 +1,106 @@ + + + 4.0.0 + + org.opendaylight.aaa + aaa-parent + 0.3.1-Beryllium-SR1 + ../parent + + + aaa-authn-keystone + bundle + + + + org.opendaylight.aaa + aaa-authn + + + org.opendaylight.aaa + aaa-authn-api + + + org.slf4j + slf4j-api + + + com.sun.jersey + jersey-server + provided + + + javax.servlet + javax.servlet-api + provided + + + org.osgi + org.osgi.core + provided + + + org.apache.felix + org.apache.felix.dependencymanager + provided + + + com.fasterxml.jackson.core + jackson-annotations + provided + + + com.fasterxml.jackson.core + jackson-core + provided + + + com.fasterxml.jackson.core + jackson-databind + provided + + + org.apache.httpcomponents + httpcore-osgi + ${httpclient.version} + + + org.apache.httpcomponents + httpclient-osgi + ${httpclient.version} + + + + com.sun.jersey.jersey-test-framework + jersey-test-framework-grizzly2 + test + + + junit + junit + test + + + org.slf4j + slf4j-simple + test + + + + + + + org.apache.felix + maven-bundle-plugin + true + + + org.opendaylight.aaa.keystone.Activator + + ${project.basedir}/META-INF + + + + + diff --git a/odl-aaa-moon/aaa-authn-keystone/src/main/java/org/opendaylight/aaa/keystone/Activator.java b/odl-aaa-moon/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-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-authn-keystone/src/main/java/org/opendaylight/aaa/keystone/KeystoneTokenAuth.java b/odl-aaa-moon/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-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> 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-authn-mdsal-store/aaa-authn-mdsal-api/pom.xml b/odl-aaa-moon/aaa-authn-mdsal-store/aaa-authn-mdsal-api/pom.xml new file mode 100644 index 00000000..fede7e5e --- /dev/null +++ b/odl-aaa-moon/aaa-authn-mdsal-store/aaa-authn-mdsal-api/pom.xml @@ -0,0 +1,99 @@ + + + 4.0.0 + + + org.opendaylight.aaa + aaa-parent + 0.3.1-Beryllium-SR1 + ../../parent + + + aaa-authn-mdsal-api + + + + org.opendaylight.aaa + aaa-authn-api + + + org.opendaylight.mdsal + yang-binding + + + org.opendaylight.mdsal.model + ietf-inet-types + + + org.opendaylight.mdsal.model + ietf-yang-types + + + org.opendaylight.mdsal.model + yang-ext + + + + + + + org.apache.felix + maven-bundle-plugin + ${bundle.plugin.version} + true + + + org.apache.maven.plugins + maven-javadoc-plugin + + maven + + + + + aggregate + + site + + + + + org.opendaylight.yangtools + yang-maven-plugin + ${yangtools.version} + + + + generate-sources + + + src/main/yang + + + + org.opendaylight.yangtools.maven.sal.api.gen.plugin.CodeGeneratorImpl + + ${salGeneratorPath} + + + true + + + + + + + org.opendaylight.mdsal + maven-sal-api-gen-plugin + ${yangtools.version} + jar + + + + + + bundle + + diff --git a/odl-aaa-moon/aaa-authn-mdsal-store/aaa-authn-mdsal-api/src/main/yang/aaa-authn-model.yang b/odl-aaa-moon/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-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-authn-mdsal-store/aaa-authn-mdsal-config/pom.xml b/odl-aaa-moon/aaa-authn-mdsal-store/aaa-authn-mdsal-config/pom.xml new file mode 100644 index 00000000..f01969a4 --- /dev/null +++ b/odl-aaa-moon/aaa-authn-mdsal-store/aaa-authn-mdsal-config/pom.xml @@ -0,0 +1,40 @@ + + 4.0.0 + + org.opendaylight.aaa + aaa-parent + 0.3.1-Beryllium-SR1 + ../../parent + + + aaa-authn-mdsal-config + AuthN Token Store Service Configuration file + jar + + + + + org.codehaus.mojo + build-helper-maven-plugin + + + attach-artifacts + + attach-artifact + + package + + + + ${project.build.directory}/classes/initial/${config.authn.store.configfile} + xml + config + + + + + + + + + diff --git a/odl-aaa-moon/aaa-authn-mdsal-store/aaa-authn-mdsal-config/src/main/resources/initial/08-authn-config.xml b/odl-aaa-moon/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-authn-mdsal-store/aaa-authn-mdsal-config/src/main/resources/initial/08-authn-config.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + authn:aaa-authn-mdsal-store + aaa-authn-mdsal-store + + + dom:dom-broker-osgi-registry + + dom-broker + + + + binding:binding-async-data-broker + + binding-data-broker + + 3600000 + 15 + CHANGE_ME + + + + + + + config:aaa:authn:mdsal:store?module=aaa-authn-mdsal-store-cfg&revision=2014-10-31 + + + diff --git a/odl-aaa-moon/aaa-authn-mdsal-store/aaa-authn-mdsal-store-impl/pom.xml b/odl-aaa-moon/aaa-authn-mdsal-store/aaa-authn-mdsal-store-impl/pom.xml new file mode 100644 index 00000000..c36febee --- /dev/null +++ b/odl-aaa-moon/aaa-authn-mdsal-store/aaa-authn-mdsal-store-impl/pom.xml @@ -0,0 +1,169 @@ + + + + + 4.0.0 + + org.opendaylight.aaa + aaa-parent + 0.3.1-Beryllium-SR1 + ../../parent + + + aaa-authn-mdsal-store-impl + bundle + + + 1.5.2 + + + + + org.opendaylight.controller + sal-binding-util + + + org.opendaylight.controller + sal-common-util + + + org.opendaylight.yangtools + yang-data-api + + + commons-codec + commons-codec + + + org.opendaylight.controller + sal-binding-api + + + org.opendaylight.controller + config-api + + + org.opendaylight.controller + sal-binding-config + + + org.opendaylight.aaa + aaa-authn-api + + + org.opendaylight.aaa + aaa-authn + + + org.opendaylight.controller + sal-core-api + + + org.opendaylight.aaa + aaa-authn-mdsal-api + + + + + junit + junit + test + + + org.mockito + mockito-all + test + + + org.slf4j + slf4j-simple + test + + + org.powermock + powermock-api-mockito + ${powermock.version} + test + + + org.powermock + powermock-module-junit4 + ${powermock.version} + test + + + + + + + org.apache.felix + maven-bundle-plugin + true + + + + org.opendaylight.yang.gen.v1.config.aaa.authn.mdsal.store.* + + + + + + + org.opendaylight.yangtools + yang-maven-plugin + ${yangtools.version} + + + config + + generate-sources + + + + + + org.opendaylight.controller.config.yangjmxgenerator.plugin.JMXGenerator + + ${jmxGeneratorPath} + + + urn:opendaylight:params:xml:ns:yang:controller==org.opendaylight.controller.config.yang + + + + + org.opendaylight.yangtools.maven.sal.api.gen.plugin.CodeGeneratorImpl + ${salGeneratorPath} + + + true + + + + + + org.opendaylight.controller + yang-jmx-generator-plugin + ${config.version} + + + org.opendaylight.mdsal + maven-sal-api-gen-plugin + ${yangtools.version} + + + + + + + + diff --git a/odl-aaa-moon/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-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-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_iid = AuthNStoreUtil.createInstIdentifierForTokencache(claims.getToken()); + WriteTransaction tx = broker.newWriteOnlyTransaction(); + tx.put(LogicalDatastoreType.OPERATIONAL, claims_iid, claims, true); + + final InstanceIdentifier userTokens_iid = AuthNStoreUtil.createInstIdentifierUserTokens( + tokenlist.getUserId(), usertokens.getTokenid()); + tx.put(LogicalDatastoreType.OPERATIONAL, userTokens_iid, usertokens, true); + + CheckedFuture commitFuture = tx.submit(); + Futures.addCallback(commitFuture, new FutureCallback() { + + @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_iid = AuthNStoreUtil.createInstIdentifierForTokencache(token); + Claims claims = null; + ReadTransaction rt = broker.newReadOnlyTransaction(); + CheckedFuture, ReadFailedException> claimsFuture = rt.read( + LogicalDatastoreType.OPERATIONAL, claims_iid); + try { + Optional 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_iid = InstanceIdentifier.builder( + TokenCacheTimes.class).child(TokenList.class, new TokenListKey(userId)).build(); + TokenList tokenList = null; + ReadTransaction rt = broker.newReadOnlyTransaction(); + CheckedFuture, ReadFailedException> userTokenListFuture = rt.read( + LogicalDatastoreType.OPERATIONAL, tokenList_iid); + try { + Optional 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_iid = AuthNStoreUtil.createInstIdentifierUserTokens( + userId, token); + UserTokens userTokens = null; + + ReadTransaction rt = broker.newReadOnlyTransaction(); + CheckedFuture, ReadFailedException> userTokensFuture = rt.read( + LogicalDatastoreType.OPERATIONAL, userTokens_iid); + + try { + Optional 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_iid = AuthNStoreUtil.createInstIdentifierForTokencache(token); + boolean result = false; + WriteTransaction tx = broker.newWriteOnlyTransaction(); + tx.delete(LogicalDatastoreType.OPERATIONAL, claims_iid); + CheckedFuture 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_iid = AuthNStoreUtil.createInstIdentifierUserTokens( + userId, token); + + WriteTransaction tx = broker.newWriteOnlyTransaction(); + tx.delete(LogicalDatastoreType.OPERATIONAL, userTokens_iid); + CheckedFuture 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-authn-mdsal-store/aaa-authn-mdsal-store-impl/src/main/java/org/opendaylight/aaa/authn/mdsal/store/DataEncrypter.java b/odl-aaa-moon/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-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-authn-mdsal-store/aaa-authn-mdsal-store-impl/src/main/java/org/opendaylight/aaa/authn/mdsal/store/IDMMDSALStore.java b/odl-aaa-moon/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-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 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 ID = InstanceIdentifier.create(Authentication.class).child( + Domain.class, new DomainKey(domain.getDomainid())); + WriteTransaction wrt = dataBroker.newWriteOnlyTransaction(); + wrt.put(LogicalDatastoreType.CONFIGURATION, ID, domain, true); + CheckedFuture submit = wrt.submit(); + if (!waitForSubmit(submit)) { + return domain; + } else { + return null; + } + } + + public Domain readDomain(String domainid) { + Preconditions.checkNotNull(domainid); + InstanceIdentifier ID = InstanceIdentifier.create(Authentication.class).child( + Domain.class, new DomainKey(domainid)); + ReadOnlyTransaction rot = dataBroker.newReadOnlyTransaction(); + CheckedFuture, ReadFailedException> read = rot.read( + LogicalDatastoreType.CONFIGURATION, ID); + if (read == null) { + LOG.error("Failed to read domain from data store"); + return null; + } + Optional 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 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 getAllDomains() { + InstanceIdentifier id = InstanceIdentifier.create(Authentication.class); + ReadOnlyTransaction rot = dataBroker.newReadOnlyTransaction(); + CheckedFuture, 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 getAllRoles() { + InstanceIdentifier id = InstanceIdentifier.create(Authentication.class); + ReadOnlyTransaction rot = dataBroker.newReadOnlyTransaction(); + CheckedFuture, 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 getAllUsers() { + InstanceIdentifier id = InstanceIdentifier.create(Authentication.class); + ReadOnlyTransaction rot = dataBroker.newReadOnlyTransaction(); + CheckedFuture, 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 getAllGrants() { + InstanceIdentifier id = InstanceIdentifier.create(Authentication.class); + ReadOnlyTransaction rot = dataBroker.newReadOnlyTransaction(); + CheckedFuture, 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 ID = InstanceIdentifier.create(Authentication.class).child( + Role.class, new RoleKey(role.getRoleid())); + WriteTransaction wrt = dataBroker.newWriteOnlyTransaction(); + wrt.put(LogicalDatastoreType.CONFIGURATION, ID, role, true); + CheckedFuture submit = wrt.submit(); + if (!waitForSubmit(submit)) { + return role; + } else { + return null; + } + } + + public Role readRole(String roleid) { + Preconditions.checkNotNull(roleid); + InstanceIdentifier ID = InstanceIdentifier.create(Authentication.class).child( + Role.class, new RoleKey(roleid)); + ReadOnlyTransaction rot = dataBroker.newReadOnlyTransaction(); + CheckedFuture, ReadFailedException> read = rot.read( + LogicalDatastoreType.CONFIGURATION, ID); + if (read == null) { + LOG.error("Failed to read role from data store"); + return null; + } + Optional 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 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 ID = InstanceIdentifier.create(Authentication.class).child( + User.class, new UserKey(user.getUserid())); + WriteTransaction wrt = dataBroker.newWriteOnlyTransaction(); + wrt.put(LogicalDatastoreType.CONFIGURATION, ID, user, true); + CheckedFuture submit = wrt.submit(); + if (!waitForSubmit(submit)) { + return user; + } else { + return null; + } + } + + public User readUser(String userid) { + Preconditions.checkNotNull(userid); + InstanceIdentifier ID = InstanceIdentifier.create(Authentication.class).child( + User.class, new UserKey(userid)); + ReadOnlyTransaction rot = dataBroker.newReadOnlyTransaction(); + CheckedFuture, ReadFailedException> read = rot.read( + LogicalDatastoreType.CONFIGURATION, ID); + if (read == null) { + LOG.error("Failed to read user from data store"); + return null; + } + Optional 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 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 ID = InstanceIdentifier.create(Authentication.class).child( + Grant.class, new GrantKey(grant.getGrantid())); + WriteTransaction wrt = dataBroker.newWriteOnlyTransaction(); + wrt.put(LogicalDatastoreType.CONFIGURATION, ID, grant, true); + CheckedFuture submit = wrt.submit(); + if (!waitForSubmit(submit)) { + return grant; + } else { + return null; + } + } + + public Grant readGrant(String grantid) { + Preconditions.checkNotNull(grantid); + InstanceIdentifier ID = InstanceIdentifier.create(Authentication.class).child( + Grant.class, new GrantKey(grantid)); + ReadOnlyTransaction rot = dataBroker.newReadOnlyTransaction(); + CheckedFuture, ReadFailedException> read = rot.read( + LogicalDatastoreType.CONFIGURATION, ID); + if (read == null) { + LOG.error("Failed to read grant from data store"); + return null; + } + Optional 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 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-authn-mdsal-store/aaa-authn-mdsal-store-impl/src/main/java/org/opendaylight/aaa/authn/mdsal/store/IDMObject2MDSAL.java b/odl-aaa-moon/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-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, ConvertionMethods> typesMethods = new HashMap, 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 setMethods = new ArrayList(); + private Map getMethods = new HashMap(); + 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-authn-mdsal-store/aaa-authn-mdsal-store-impl/src/main/java/org/opendaylight/aaa/authn/mdsal/store/IDMStore.java b/odl-aaa-moon/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-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 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 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 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 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 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 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-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-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-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 createInstIdentifierForTokencache(String token) { + if (token == null || token.length() == 0) + return null; + + InstanceIdentifier claims_iid = InstanceIdentifier.builder(Tokencache.class) + .child(Claims.class, + new ClaimsKey(token)) + .build(); + return claims_iid; + } + + public static InstanceIdentifier createInstIdentifierUserTokens(String userId, + String token) { + if (userId == null || userId.length() == 0 || token == null || token.length() == 0) + return null; + + InstanceIdentifier 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 roles = new ArrayList(); + 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 = new ArrayList(); + 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 roles() { + return new HashSet<>(claims.getRoles()); + } + }; + AuthenticationBuilder authBuilder = new AuthenticationBuilder(claim); + authBuilder.setExpiration(expiration); + return authBuilder.build(); + } +} diff --git a/odl-aaa-moon/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-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-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-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-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-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-authn-mdsal-store/aaa-authn-mdsal-store-impl/src/main/yang/aaa-authn-mdsal-store-cfg.yang b/odl-aaa-moon/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-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-authn-mdsal-store/aaa-authn-mdsal-store-impl/src/test/java/org/opendaylight/aaa/authn/mdsal/store/DataBrokerReadMocker.java b/odl-aaa-moon/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-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> stubs = new HashMap>(); + private Class mokingClass = null; + + @Override + public Object invoke(Object arg0, Method arg1, Object[] arg2) throws Throwable { + List 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 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-authn-mdsal-store/aaa-authn-mdsal-store-impl/src/test/java/org/opendaylight/aaa/authn/mdsal/store/DataEncrypterTest.java b/odl-aaa-moon/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-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-authn-mdsal-store/aaa-authn-mdsal-store-impl/src/test/java/org/opendaylight/aaa/authn/mdsal/store/IDMStoreTest.java b/odl-aaa-moon/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-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-authn-mdsal-store/aaa-authn-mdsal-store-impl/src/test/java/org/opendaylight/aaa/authn/mdsal/store/IDMStoreTestUtil.java b/odl-aaa-moon/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-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 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, ReadFailedException> read = mock(CheckedFuture.class); + DataBrokerReadMocker.getMocker(rot).addWhen("read", + new Object[] { LogicalDatastoreType.CONFIGURATION, grantID }, read); + Optional 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 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, ReadFailedException> read = mock(CheckedFuture.class); + DataBrokerReadMocker.getMocker(rot).addWhen("read", + new Object[] { LogicalDatastoreType.CONFIGURATION, domainID }, read); + Optional 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 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, ReadFailedException> read = mock(CheckedFuture.class); + DataBrokerReadMocker.getMocker(rot).addWhen("read", + new Object[] { LogicalDatastoreType.CONFIGURATION, roleID }, read); + Optional 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 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, ReadFailedException> read = mock(CheckedFuture.class); + DataBrokerReadMocker.getMocker(rot).addWhen("read", + new Object[] { LogicalDatastoreType.CONFIGURATION, userID }, read); + Optional 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-authn-mdsal-store/aaa-authn-mdsal-store-impl/src/test/java/org/opendaylight/aaa/authn/mdsal/store/MDSALConvertTest.java b/odl-aaa-moon/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-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-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-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-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 roles = new ArrayList(); + 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-authn-mdsal-store/pom.xml b/odl-aaa-moon/aaa-authn-mdsal-store/pom.xml new file mode 100644 index 00000000..38d29147 --- /dev/null +++ b/odl-aaa-moon/aaa-authn-mdsal-store/pom.xml @@ -0,0 +1,22 @@ + + + + org.opendaylight.aaa + aaa-parent + 0.3.1-Beryllium-SR1 + ../parent + + 4.0.0 + + aaa-authn-mdsal-store + ${project.artifactId} + pom + + + aaa-authn-mdsal-api + aaa-authn-mdsal-config + aaa-authn-mdsal-store-impl + + diff --git a/odl-aaa-moon/aaa-authn-sssd/pom.xml b/odl-aaa-moon/aaa-authn-sssd/pom.xml new file mode 100644 index 00000000..b70c2466 --- /dev/null +++ b/odl-aaa-moon/aaa-authn-sssd/pom.xml @@ -0,0 +1,88 @@ + + + 4.0.0 + + org.opendaylight.aaa + aaa-parent + 0.3.1-Beryllium-SR1 + ../parent + + + aaa-authn-sssd + bundle + + + + org.opendaylight.aaa + aaa-authn + + + org.opendaylight.aaa + aaa-authn-api + + + org.glassfish + javax.json + + + org.opendaylight.aaa + aaa-authn-idpmapping + + + org.slf4j + slf4j-api + + + com.sun.jersey + jersey-server + provided + + + javax.servlet + javax.servlet-api + provided + + + org.osgi + org.osgi.core + provided + + + org.apache.felix + org.apache.felix.dependencymanager + provided + + + + com.sun.jersey.jersey-test-framework + jersey-test-framework-grizzly2 + test + + + junit + junit + test + + + org.slf4j + slf4j-simple + test + + + + + + + org.apache.felix + maven-bundle-plugin + true + + + org.opendaylight.aaa.sssd.Activator + + ${project.basedir}/META-INF + + + + + diff --git a/odl-aaa-moon/aaa-authn-sssd/src/main/java/org/opendaylight/aaa/sssd/Activator.java b/odl-aaa-moon/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-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-authn-sssd/src/main/java/org/opendaylight/aaa/sssd/SssdClaimAuth.java b/odl-aaa-moon/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-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 properties = new HashMap(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. + * + *
+ *
ClientId
+ *
A string. + * + * @see org.opendaylight.aaa.api.Claim#clientId()
+ * + *
UserId
A string. + * @see org.opendaylight.aaa.api.Claim#userId()
+ * + *
User
A string. + * @see org.opendaylight.aaa.api.Claim#user()
+ * + *
Domain
A string. + * @see org.opendaylight.aaa.api.Claim#domain()
+ * + *
Roles
An array of strings. + * @see org.opendaylight.aaa.api.Claim#roles()
+ * + *
+ * + * @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 assertion) { + String assertionJson; + Map 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 roles = (List) 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: + * + *
    + *
  • String
  • + *
  • Integer
  • + *
  • Long
  • + *
  • Double
  • + *
  • Boolean
  • + *
  • null
  • + *
+ * + * 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 claim) { + StringWriter stringWriter = new StringWriter(); + JsonGenerator generator = generatorFactory.createGenerator(stringWriter); + + generator.writeStartObject(); + for (Map.Entry 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-authn-store/pom.xml b/odl-aaa-moon/aaa-authn-store/pom.xml new file mode 100644 index 00000000..744c4df1 --- /dev/null +++ b/odl-aaa-moon/aaa-authn-store/pom.xml @@ -0,0 +1,100 @@ + + + 4.0.0 + + org.opendaylight.aaa + aaa-parent + 0.3.1-Beryllium-SR1 + ../parent + + + aaa-authn-store + 0.3.1-Beryllium-SR1 + bundle + + + + net.sf.ehcache + ehcache + + + org.opendaylight.aaa + aaa-authn-api + + + org.slf4j + slf4j-api + + + org.osgi + org.osgi.core + provided + + + org.apache.felix + org.apache.felix.dependencymanager + provided + + + + junit + junit + test + + + org.mockito + mockito-all + test + + + org.slf4j + slf4j-simple + test + + + org.opendaylight.aaa + aaa-authn + test + + + + + + + org.apache.felix + maven-bundle-plugin + true + + + org.opendaylight.aaa.store.Activator + + ${project.basedir}/META-INF + + + + org.codehaus.mojo + build-helper-maven-plugin + + + attach-artifacts + package + + attach-artifact + + + + + ${project.build.directory}/classes/tokens.cfg + cfg + config + + + + + + + + + + diff --git a/odl-aaa-moon/aaa-authn-store/src/main/java/org/opendaylight/aaa/store/Activator.java b/odl-aaa-moon/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-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 addPid(Dictionary dict) { + dict.put(Constants.SERVICE_PID, TOKEN_PID); + return dict; + } +} diff --git a/odl-aaa-moon/aaa-authn-store/src/main/java/org/opendaylight/aaa/store/DefaultTokenStore.java b/odl-aaa-moon/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-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 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-authn-store/src/main/resources/OSGI-INF/metatype/metatype.properties b/odl-aaa-moon/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-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-authn-store/src/main/resources/OSGI-INF/metatype/metatype.xml b/odl-aaa-moon/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-authn-store/src/main/resources/OSGI-INF/metatype/metatype.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/odl-aaa-moon/aaa-authn-store/src/main/resources/tokens.cfg b/odl-aaa-moon/aaa-authn-store/src/main/resources/tokens.cfg new file mode 100644 index 00000000..d3dda90e --- /dev/null +++ b/odl-aaa-moon/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-authn-store/src/test/java/org/opendaylight/aaa/store/DefaultTokenStoreTest.java b/odl-aaa-moon/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-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 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-authn-sts/pom.xml b/odl-aaa-moon/aaa-authn-sts/pom.xml new file mode 100644 index 00000000..25ac0fe6 --- /dev/null +++ b/odl-aaa-moon/aaa-authn-sts/pom.xml @@ -0,0 +1,112 @@ + + + 4.0.0 + + org.opendaylight.aaa + aaa-parent + 0.3.1-Beryllium-SR1 + ../parent + + + aaa-authn-sts + bundle + + + + org.opendaylight.aaa + aaa-authn + + + org.opendaylight.aaa + aaa-authn-api + + + org.slf4j + slf4j-api + + + com.sun.jersey + jersey-server + provided + + + javax.servlet + javax.servlet-api + provided + + + org.apache.oltu.oauth2 + org.apache.oltu.oauth2.authzserver + provided + + + org.apache.oltu.oauth2 + org.apache.oltu.oauth2.common + provided + + + org.apache.oltu.oauth2 + org.apache.oltu.oauth2.resourceserver + provided + + + org.osgi + org.osgi.core + provided + + + org.apache.felix + org.apache.felix.dependencymanager + provided + + + + com.sun.jersey.jersey-test-framework + jersey-test-framework-grizzly2 + test + + + org.eclipse.jetty + jetty-servlet-tester + test + + + junit + junit + test + + + org.mockito + mockito-all + test + + + org.slf4j + slf4j-simple + test + + + + + + + org.apache.felix + maven-bundle-plugin + true + + + + *, + com.sun.jersey.spi.container.servlet + + /oauth2 + org.opendaylight.aaa.sts.Activator + ${project.basedir}/META-INF + + + + + + + diff --git a/odl-aaa-moon/aaa-authn-sts/src/main/java/org/opendaylight/aaa/sts/Activator.java b/odl-aaa-moon/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-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> 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> servicesBuilder = new ImmutableList.Builder>(); + + // Async ServiceTrackers to track and load AAA STS bundles + final ServiceTracker authenticationService = new ServiceTracker<>( + context, AuthenticationService.class, + new AAAServiceTrackerCustomizer( + new Function() { + @Override + public Void apply(AuthenticationService authenticationService) { + ServiceLocator.getInstance().setAuthenticationService( + authenticationService); + return null; + } + })); + servicesBuilder.add(authenticationService); + authenticationService.open(); + + final ServiceTracker idmService = new ServiceTracker<>(context, + IdMService.class, new AAAServiceTrackerCustomizer( + new Function() { + @Override + public Void apply(IdMService idmService) { + ServiceLocator.getInstance().setIdmService(idmService); + return null; + } + })); + servicesBuilder.add(idmService); + idmService.open(); + + final ServiceTracker tokenAuthService = new ServiceTracker<>(context, + TokenAuth.class, new AAAServiceTrackerCustomizer( + new Function() { + @Override + public Void apply(TokenAuth tokenAuth) { + final List tokenAuthCollection = (List) Lists.newArrayList(tokenAuth); + ServiceLocator.getInstance().setTokenAuthCollection( + tokenAuthCollection); + return null; + } + })); + servicesBuilder.add(tokenAuthService); + tokenAuthService.open(); + + final ServiceTracker tokenStoreService = new ServiceTracker<>( + context, TokenStore.class, new AAAServiceTrackerCustomizer( + new Function() { + @Override + public Void apply(TokenStore tokenStore) { + ServiceLocator.getInstance().setTokenStore(tokenStore); + return null; + } + })); + servicesBuilder.add(tokenStoreService); + tokenStoreService.open(); + + final ServiceTracker clientService = new ServiceTracker<>( + context, ClientService.class, new AAAServiceTrackerCustomizer( + new Function() { + @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 + */ + static final class AAAServiceTrackerCustomizer implements ServiceTrackerCustomizer { + + private Function callback; + + public AAAServiceTrackerCustomizer(final Function callback) { + this.callback = callback; + } + + @Override + public S addingService(ServiceReference 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 reference, S service) { + } + + @Override + public void removedService(ServiceReference 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-authn-sts/src/main/java/org/opendaylight/aaa/sts/AnonymousPasswordValidator.java b/odl-aaa-moon/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-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 { + + 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-authn-sts/src/main/java/org/opendaylight/aaa/sts/AnonymousRefreshTokenValidator.java b/odl-aaa-moon/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-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 { + + public AnonymousRefreshTokenValidator() { + requiredParams.add(OAuth.OAUTH_GRANT_TYPE); + requiredParams.add(OAuth.OAUTH_REFRESH_TOKEN); + + enforceClientAuthentication = false; + } +} diff --git a/odl-aaa-moon/aaa-authn-sts/src/main/java/org/opendaylight/aaa/sts/OAuthRequest.java b/odl-aaa-moon/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-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 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-authn-sts/src/main/java/org/opendaylight/aaa/sts/ServiceLocator.java b/odl-aaa-moon/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-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 tokenAuthCollection = new Vector<>(); + + protected volatile CredentialAuth 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 da) { + this.credentialAuth = da; + } + + protected void credentialAuthAddedRemoved(CredentialAuth da) { + this.credentialAuth = null; + } + + public List getTokenAuthCollection() { + return tokenAuthCollection; + } + + public void setTokenAuthCollection(List tokenAuthCollection) { + this.tokenAuthCollection = tokenAuthCollection; + } + + public CredentialAuth getCredentialAuth() { + return credentialAuth; + } + + public synchronized void setCredentialAuth(CredentialAuth 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-authn-sts/src/main/java/org/opendaylight/aaa/sts/TokenAuthFilter.java b/odl-aaa-moon/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-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 AAAFilter 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> headers = request.getRequestHeaders(); + + // Go through and invoke other TokenAuth first... + List 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 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-authn-sts/src/main/java/org/opendaylight/aaa/sts/TokenEndpoint.java b/odl-aaa-moon/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-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 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-authn-sts/src/main/resources/WEB-INF/web.xml b/odl-aaa-moon/aaa-authn-sts/src/main/resources/WEB-INF/web.xml new file mode 100644 index 00000000..83a9fa51 --- /dev/null +++ b/odl-aaa-moon/aaa-authn-sts/src/main/resources/WEB-INF/web.xml @@ -0,0 +1,23 @@ + + + + + STS + org.opendaylight.aaa.sts.TokenEndpoint + 1 + + + STS + /token + + + STS + /revoke + + + STS + /validate + + diff --git a/odl-aaa-moon/aaa-authn-sts/src/test/java/org/opendaylight/aaa/sts/RestFixture.java b/odl-aaa-moon/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-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-authn-sts/src/test/java/org/opendaylight/aaa/sts/TokenAuthTest.java b/odl-aaa-moon/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-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-authn-sts/src/test/java/org/opendaylight/aaa/sts/TokenEndpointTest.java b/odl-aaa-moon/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-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-authn/pom.xml b/odl-aaa-moon/aaa-authn/pom.xml new file mode 100644 index 00000000..06027a60 --- /dev/null +++ b/odl-aaa-moon/aaa-authn/pom.xml @@ -0,0 +1,103 @@ + + + + 4.0.0 + + org.opendaylight.aaa + aaa-parent + 0.3.1-Beryllium-SR1 + ../parent + + + aaa-authn + bundle + + + + org.opendaylight.aaa + aaa-authn-api + + + com.google.guava + guava + + + org.slf4j + slf4j-api + + + com.sun.jersey + jersey-server + provided + + + org.osgi + org.osgi.core + provided + + + org.osgi + org.osgi.compendium + provided + + + org.apache.felix + org.apache.felix.dependencymanager + provided + + + + junit + junit + test + + + org.slf4j + slf4j-simple + test + + + + + + + org.apache.felix + maven-bundle-plugin + true + + + org.opendaylight.aaa.Activator + + ${project.basedir}/META-INF + + + + org.codehaus.mojo + build-helper-maven-plugin + + + attach-artifacts + package + + attach-artifact + + + + + ${project.build.directory}/classes/authn.cfg + cfg + config + + + + + + + + + + diff --git a/odl-aaa-moon/aaa-authn/src/main/java/org/opendaylight/aaa/Activator.java b/odl-aaa-moon/aaa-authn/src/main/java/org/opendaylight/aaa/Activator.java new file mode 100644 index 00000000..cfe27ef0 --- /dev/null +++ b/odl-aaa-moon/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 addPid(Dictionary dict) { + dict.put(Constants.SERVICE_PID, AUTHN_PID); + return dict; + } +} diff --git a/odl-aaa-moon/aaa-authn/src/main/java/org/opendaylight/aaa/AuthenticationBuilder.java b/odl-aaa-moon/aaa-authn/src/main/java/org/opendaylight/aaa/AuthenticationBuilder.java new file mode 100644 index 00000000..948cbac6 --- /dev/null +++ b/odl-aaa-moon/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 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-authn/src/main/java/org/opendaylight/aaa/AuthenticationManager.java b/odl-aaa-moon/aaa-authn/src/main/java/org/opendaylight/aaa/AuthenticationManager.java new file mode 100644 index 00000000..5f6420a3 --- /dev/null +++ b/odl-aaa-moon/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 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 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 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-authn/src/main/java/org/opendaylight/aaa/ClaimBuilder.java b/odl-aaa-moon/aaa-authn/src/main/java/org/opendaylight/aaa/ClaimBuilder.java new file mode 100644 index 00000000..4e4a8ef3 --- /dev/null +++ b/odl-aaa-moon/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 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 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 roles; + + protected ImmutableClaim(ClaimBuilder base) { + clientId = base.clientId; + userId = base.userId; + user = base.user; + domain = base.domain; + roles = ImmutableSet. 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 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-authn/src/main/java/org/opendaylight/aaa/ClientManager.java b/odl-aaa-moon/aaa-authn/src/main/java/org/opendaylight/aaa/ClientManager.java new file mode 100644 index 00000000..e7e51424 --- /dev/null +++ b/odl-aaa-moon/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 :"; + private static final String UNAUTHORIZED_CLIENT_ERR = "Unauthorized client"; + + // Defaults (needed only for non-Karaf deployments) + static final Dictionary defaults = new Hashtable<>(); + static { + defaults.put(CLIENTS, "dlux:secrete"); + } + + private final Map 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 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 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-authn/src/main/java/org/opendaylight/aaa/EqualUtil.java b/odl-aaa-moon/aaa-authn/src/main/java/org/opendaylight/aaa/EqualUtil.java new file mode 100644 index 00000000..17204d0e --- /dev/null +++ b/odl-aaa-moon/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. + *

+ * + * Arrays are not handled by this class. This is because the + * Arrays.equals 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-authn/src/main/java/org/opendaylight/aaa/HashCodeUtil.java b/odl-aaa-moon/aaa-authn/src/main/java/org/opendaylight/aaa/HashCodeUtil.java new file mode 100644 index 00000000..c295b3ed --- /dev/null +++ b/odl-aaa-moon/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 hashCode. + * + * Example use case: + * + *

+ * 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;
+ * }
+ * 
+ */ +public final class HashCodeUtil { + + /** + * An initial value for a hashCode, to which is added contributions + * from fields. Using a non-zero value decreases collisions of + * hashCode 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)); + } + + /** + * aObject is a possibly-null object field, and possibly an array. + * + * If aObject 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-authn/src/main/java/org/opendaylight/aaa/PasswordCredentialBuilder.java b/odl-aaa-moon/aaa-authn/src/main/java/org/opendaylight/aaa/PasswordCredentialBuilder.java new file mode 100644 index 00000000..d8a2e87a --- /dev/null +++ b/odl-aaa-moon/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-authn/src/main/java/org/opendaylight/aaa/SecureBlockingQueue.java b/odl-aaa-moon/aaa-authn/src/main/java/org/opendaylight/aaa/SecureBlockingQueue.java new file mode 100644 index 00000000..3ded52da --- /dev/null +++ b/odl-aaa-moon/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 + * queue element type + */ +public class SecureBlockingQueue implements BlockingQueue { + private final BlockingQueue> queue; + + /** + * Constructor. + * + * @param queue + * blocking queue implementation to use + */ + public SecureBlockingQueue(BlockingQueue> 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 iterator() { + return new Iterator() { + Iterator> 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[] toArray(T[] a) { + return toData().toArray(a); + } + + @Override + public boolean containsAll(Collection c) { + return toData().containsAll(c); + } + + @Override + public boolean addAll(Collection 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(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> it = queue.iterator(); + while (it.hasNext()) { + SecureData sd = it.next(); + if (sd.data.equals(o)) { + return queue.remove(sd); + } + } + return false; + } + + @Override + public boolean contains(Object o) { + Iterator> it = queue.iterator(); + while (it.hasNext()) { + SecureData sd = it.next(); + if (sd.data.equals(o)) { + return true; + } + } + return false; + } + + @Override + public int drainTo(Collection c) { + Collection> sd = new ArrayList<>(); + int n = queue.drainTo(sd); + c.addAll(toData(sd)); + return n; + } + + @Override + public int drainTo(Collection c, int maxElements) { + Collection> sd = new ArrayList<>(); + int n = queue.drainTo(sd, maxElements); + c.addAll(toData(sd)); + return n; + } + + // Rehydrate security context + private T setAuth(SecureData i) { + AuthenticationManager.instance().set(i.auth); + return i.data; + } + + // Construct secure data collection from a plain old data collection + @SuppressWarnings("unchecked") + private Collection> fromData(Collection c) { + Collection> sd = new ArrayList<>(c.size()); + for (Object d : c) { + sd.add((SecureData) new SecureData<>(d)); + } + return sd; + } + + // Extract the data portion out from the secure data + @SuppressWarnings("unchecked") + private Collection toData() { + return toData(Arrays.> asList(queue.toArray(new SecureData[0]))); + } + + // Extract the data portion out from the secure data + private Collection toData(Collection> secureData) { + Collection data = new ArrayList<>(secureData.size()); + Iterator> it = secureData.iterator(); + while (it.hasNext()) { + data.add(it.next().data); + } + return data; + } + + // Inject security context + public static final class SecureData { + 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-authn/src/main/resources/OSGI-INF/metatype/metatype.properties b/odl-aaa-moon/aaa-authn/src/main/resources/OSGI-INF/metatype/metatype.properties new file mode 100644 index 00000000..75537f6b --- /dev/null +++ b/odl-aaa-moon/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 +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-authn/src/main/resources/OSGI-INF/metatype/metatype.xml b/odl-aaa-moon/aaa-authn/src/main/resources/OSGI-INF/metatype/metatype.xml new file mode 100644 index 00000000..10150587 --- /dev/null +++ b/odl-aaa-moon/aaa-authn/src/main/resources/OSGI-INF/metatype/metatype.xml @@ -0,0 +1,16 @@ + + + + + + + + + + \ No newline at end of file diff --git a/odl-aaa-moon/aaa-authn/src/main/resources/authn.cfg b/odl-aaa-moon/aaa-authn/src/main/resources/authn.cfg new file mode 100644 index 00000000..e7326f86 --- /dev/null +++ b/odl-aaa-moon/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-authn/src/test/java/org/opendaylight/aaa/AuthenticationBuilderTest.java b/odl-aaa-moon/aaa-authn/src/test/java/org/opendaylight/aaa/AuthenticationBuilderTest.java new file mode 100644 index 00000000..2f69fe5b --- /dev/null +++ b/odl-aaa-moon/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 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-authn/src/test/java/org/opendaylight/aaa/AuthenticationManagerTest.java b/odl-aaa-moon/aaa-authn/src/test/java/org/opendaylight/aaa/AuthenticationManagerTest.java new file mode 100644 index 00000000..540df287 --- /dev/null +++ b/odl-aaa-moon/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 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> fs = Executors.newFixedThreadPool(2).invokeAll( + Arrays.asList(new Worker(), new Worker())); + for (Future f : fs) { + assertEquals(auth, f.get()); + } + + as.clear(); + fs = Executors.newFixedThreadPool(2).invokeAll(Arrays.asList(new Worker(), new Worker())); + for (Future f : fs) { + assertNull(f.get()); + } + } + + @Test + public void testUpdatedValid() throws ConfigurationException { + Dictionary 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 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 props = new Hashtable<>(); + + props.put("Invalid Key", "true"); + as.updated(props); + } + + private class Worker implements Callable { + @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-authn/src/test/java/org/opendaylight/aaa/ClaimBuilderTest.java b/odl-aaa-moon/aaa-authn/src/test/java/org/opendaylight/aaa/ClaimBuilderTest.java new file mode 100644 index 00000000..372eb6d2 --- /dev/null +++ b/odl-aaa-moon/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-authn/src/test/java/org/opendaylight/aaa/ClientManagerTest.java b/odl-aaa-moon/aaa-authn/src/test/java/org/opendaylight/aaa/ClientManagerTest.java new file mode 100644 index 00000000..059ba9a3 --- /dev/null +++ b/odl-aaa-moon/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 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 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-authn/src/test/java/org/opendaylight/aaa/PasswordCredentialTest.java b/odl-aaa-moon/aaa-authn/src/test/java/org/opendaylight/aaa/PasswordCredentialTest.java new file mode 100644 index 00000000..2dabb77b --- /dev/null +++ b/odl-aaa-moon/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 pcs = new HashSet<>(); + pcs.add(pc1); + pcs.add(pc2); + pcs.add(pc3); + assertEquals(2, pcs.size()); + } + +} diff --git a/odl-aaa-moon/aaa-authn/src/test/java/org/opendaylight/aaa/SecureBlockingQueueTest.java b/odl-aaa-moon/aaa-authn/src/test/java/org/opendaylight/aaa/SecureBlockingQueueTest.java new file mode 100644 index 00000000..16627d9f --- /dev/null +++ b/odl-aaa-moon/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 queue = new SecureBlockingQueue<>( + new ArrayBlockingQueue>(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 queue = new ArrayBlockingQueue(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 queue = new SecureBlockingQueue<>( + new ArrayBlockingQueue>(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 queue = new SecureBlockingQueue<>( + new ArrayBlockingQueue>(6)); + for (int i = 1; i <= 3; i++) + queue.add("User" + i); + Iterator it = queue.iterator(); + while (it.hasNext()) + assertTrue(it.next().startsWith("User")); + assertEquals(3, queue.toArray().length); + List 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 queue = new SecureBlockingQueue<>( + new ArrayBlockingQueue>(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 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 { + 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 { + private final String name; + private final String userId; + private final String role; + private final BlockingQueue queue; + + Producer(String name, String userId, String role, BlockingQueue 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 { + private final BlockingQueue queue; + + Consumer(BlockingQueue 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-authz/aaa-authz-config/pom.xml b/odl-aaa-moon/aaa-authz/aaa-authz-config/pom.xml new file mode 100644 index 00000000..4e19ed42 --- /dev/null +++ b/odl-aaa-moon/aaa-authz/aaa-authz-config/pom.xml @@ -0,0 +1,43 @@ + + + 4.0.0 + + org.opendaylight.aaa + aaa-parent + 0.3.1-Beryllium-SR1 + ../../parent + + + authz-service-config + AuthZ Service Configuration files + jar + + + + org.codehaus.mojo + build-helper-maven-plugin + + + attach-artifacts + + attach-artifact + + package + + + + ${project.build.directory}/classes/initial/${config.authz.service.configfile} + xml + config + + + + + + + + + + diff --git a/odl-aaa-moon/aaa-authz/aaa-authz-config/src/main/resources/initial/08-authz-config.xml b/odl-aaa-moon/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-authz/aaa-authz-config/src/main/resources/initial/08-authz-config.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + authz:aaa-authz-service + aaa-authz-service + + + dom:dom-broker-osgi-registry + dom-broker + + + + binding:binding-data-broker + binding-data-broker + + + + RestConfService + Any + * + admin + + + + + + + + dom:dom-broker-osgi-registry + + authz-connector-default + + /modules/module[type='aaa-authz-service'][name='aaa-authz-service'] + + + + + + + + + + + urn:opendaylight:params:xml:ns:yang:controller:config:aaa-authz:srv?module=aaa-authz-service-impl&revision=2014-07-01 + + + diff --git a/odl-aaa-moon/aaa-authz/aaa-authz-model/pom.xml b/odl-aaa-moon/aaa-authz/aaa-authz-model/pom.xml new file mode 100644 index 00000000..a1d3a28f --- /dev/null +++ b/odl-aaa-moon/aaa-authz/aaa-authz-model/pom.xml @@ -0,0 +1,95 @@ + + + 4.0.0 + + + org.opendaylight.aaa + aaa-parent + 0.3.1-Beryllium-SR1 + ../../parent + + + aaa-authz-model + ${project.artifactId} + + + + org.opendaylight.mdsal + yang-binding + + + org.opendaylight.mdsal.model + ietf-inet-types + + + org.opendaylight.mdsal.model + ietf-yang-types + + + org.opendaylight.mdsal.model + yang-ext + + + + + + + org.apache.felix + maven-bundle-plugin + true + + + org.apache.maven.plugins + maven-javadoc-plugin + + maven + + + + + aggregate + + site + + + + + org.opendaylight.yangtools + yang-maven-plugin + ${yangtools.version} + + + + generate-sources + + + src/main/yang + + + + org.opendaylight.yangtools.maven.sal.api.gen.plugin.CodeGeneratorImpl + + ${salGeneratorPath} + + + true + + + + + + + org.opendaylight.mdsal + maven-sal-api-gen-plugin + ${yangtools.version} + jar + + + + + + bundle + + diff --git a/odl-aaa-moon/aaa-authz/aaa-authz-model/src/main/yang/authorization-schema.yang b/odl-aaa-moon/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-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-authz/aaa-authz-restconf-config/pom.xml b/odl-aaa-moon/aaa-authz/aaa-authz-restconf-config/pom.xml new file mode 100644 index 00000000..95db7458 --- /dev/null +++ b/odl-aaa-moon/aaa-authz/aaa-authz-restconf-config/pom.xml @@ -0,0 +1,43 @@ + + + 4.0.0 + + org.opendaylight.aaa + aaa-parent + 0.3.1-Beryllium-SR1 + ../../parent + + + authz-restconf-config + + AuthZ Restconf Connector Configuration file + jar + + + + org.codehaus.mojo + build-helper-maven-plugin + + + attach-artifacts + + attach-artifact + + package + + + + ${project.build.directory}/classes/initial/${config.restconf.configfile} + xml + config + + + + + + + + + diff --git a/odl-aaa-moon/aaa-authz/aaa-authz-restconf-config/src/main/resources/initial/09-rest-connector.xml b/odl-aaa-moon/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-authz/aaa-authz-restconf-config/src/main/resources/initial/09-rest-connector.xml @@ -0,0 +1,42 @@ + + + + + + + + + rest:rest-connector-impl + rest-connector-default-impl + 8185 + + dom:dom-broker-osgi-registry + authz-connector-default + + + + + + + rest:rest-connector + + rest-connector-default + + /modules/module[type='rest-connector-impl'][name='rest-connector-default-impl'] + + + + + + + + + urn:opendaylight:params:xml:ns:yang:controller:md:sal:rest:connector?module=opendaylight-rest-connector&revision=2014-07-24 + + diff --git a/odl-aaa-moon/aaa-authz/aaa-authz-service/pom.xml b/odl-aaa-moon/aaa-authz/aaa-authz-service/pom.xml new file mode 100644 index 00000000..a0afef82 --- /dev/null +++ b/odl-aaa-moon/aaa-authz/aaa-authz-service/pom.xml @@ -0,0 +1,152 @@ + + + + + + org.opendaylight.aaa + aaa-parent + 0.3.1-Beryllium-SR1 + ../../parent + + 4.0.0 + + aaa-authz-service + bundle + + + + org.opendaylight.controller + sal-binding-util + + + org.opendaylight.controller + sal-common-util + + + org.opendaylight.yangtools + yang-data-api + + + commons-codec + commons-codec + + + org.opendaylight.controller + sal-binding-api + + + org.opendaylight.controller + config-api + + + org.opendaylight.controller + sal-binding-config + + + org.opendaylight.aaa + aaa-authz-model + + + org.opendaylight.aaa + aaa-authn-api + + + org.opendaylight.controller + sal-core-api + + + org.opendaylight.controller + sal-core-spi + + + org.jboss.resteasy + jaxrs-api + provided + + + + + junit + junit + test + + + org.mockito + mockito-all + test + + + org.slf4j + slf4j-simple + test + + + + + + + + org.apache.felix + maven-bundle-plugin + true + + + + org.opendaylight.aaa.config.yang.aaa_srv, + + + + + + org.opendaylight.yangtools + yang-maven-plugin + ${yangtools.version} + + + config + + generate-sources + + + + + + org.opendaylight.controller.config.yangjmxgenerator.plugin.JMXGenerator + + ${jmxGeneratorPath} + + + urn:opendaylight:params:xml:ns:yang:controller==org.opendaylight.controller.config.yang + + + + + org.opendaylight.yangtools.maven.sal.api.gen.plugin.CodeGeneratorImpl + ${salGeneratorPath} + + + true + + + + + + org.opendaylight.controller + yang-jmx-generator-plugin + ${config.version} + + + org.opendaylight.mdsal + maven-sal-api-gen-plugin + ${yangtools.version} + + + + + + + diff --git a/odl-aaa-moon/aaa-authz/aaa-authz-service/src/main/java/org/opendaylight/aaa/authz/srv/AuthzBrokerImpl.java b/odl-aaa-moon/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-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 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 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 getConsumerFunctionality() { + return consumer.getConsumerFunctionality(); + } + } +} diff --git a/odl-aaa-moon/aaa-authz/aaa-authz-service/src/main/java/org/opendaylight/aaa/authz/srv/AuthzConsumerContextImpl.java b/odl-aaa-moon/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-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 getService(Class 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-authz/aaa-authz-service/src/main/java/org/opendaylight/aaa/authz/srv/AuthzDataReadWriteTransaction.java b/odl-aaa-moon/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-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 submit() { + if (AuthzServiceImpl.isAuthorized(ActionType.Submit)) { + return domDataReadWriteTransaction.submit(); + } + TransactionCommitFailedException e = new TransactionCommitFailedException( + "Unauthorized User"); + return Futures.immediateFailedCheckedFuture(e); + } + + @Deprecated + @Override + public ListenableFuture> commit() { + if (AuthzServiceImpl.isAuthorized(ActionType.Commit)) { + return domDataReadWriteTransaction.commit(); + } + TransactionCommitFailedException e = new TransactionCommitFailedException( + "Unauthorized User"); + return Futures.immediateFailedCheckedFuture(e); + } + + @Override + public CheckedFuture>, 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 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-authz/aaa-authz-service/src/main/java/org/opendaylight/aaa/authz/srv/AuthzDomDataBroker.java b/odl-aaa-moon/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-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, 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 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-authz/aaa-authz-service/src/main/java/org/opendaylight/aaa/authz/srv/AuthzProviderContextImpl.java b/odl-aaa-moon/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-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 getService(Class 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-authz/aaa-authz-service/src/main/java/org/opendaylight/aaa/authz/srv/AuthzReadOnlyTransaction.java b/odl-aaa-moon/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-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>, 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 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-authz/aaa-authz-service/src/main/java/org/opendaylight/aaa/authz/srv/AuthzServiceImpl.java b/odl-aaa-moon/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-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 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) { + + 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-authz/aaa-authz-service/src/main/java/org/opendaylight/aaa/authz/srv/AuthzWriteOnlyTransaction.java b/odl-aaa-moon/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-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 submit() { + if (AuthzServiceImpl.isAuthorized(ActionType.Submit)) { + return domDataWriteTransaction.submit(); + } + TransactionCommitFailedException e = new TransactionCommitFailedException( + "Unauthorized User"); + return Futures.immediateFailedCheckedFuture(e); + } + + @Deprecated + @Override + public ListenableFuture> 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-authz/aaa-authz-service/src/main/java/org/opendaylight/controller/config/yang/config/aaa_authz/srv/AuthzSrvModule.java b/odl-aaa-moon/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-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 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-authz/aaa-authz-service/src/main/java/org/opendaylight/controller/config/yang/config/aaa_authz/srv/AuthzSrvModuleFactory.java b/odl-aaa-moon/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-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-authz/aaa-authz-service/src/main/yang/aaa-authz-service-impl.yang b/odl-aaa-moon/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-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-authz/aaa-authz-service/src/test/java/org/opendaylight/aaa/authz/srv/AuthzConsumerContextImplTest.java b/odl-aaa-moon/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-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-authz/pom.xml b/odl-aaa-moon/aaa-authz/pom.xml new file mode 100644 index 00000000..bdc1852f --- /dev/null +++ b/odl-aaa-moon/aaa-authz/pom.xml @@ -0,0 +1,23 @@ + + + 4.0.0 + + org.opendaylight.aaa + aaa-parent + 0.3.1-Beryllium-SR1 + ../parent + + + aaa-authz + ${project.artifactId} + pom + + + aaa-authz-model + aaa-authz-service + aaa-authz-config + aaa-authz-restconf-config + + diff --git a/odl-aaa-moon/aaa-credential-store-api/pom.xml b/odl-aaa-moon/aaa-credential-store-api/pom.xml new file mode 100644 index 00000000..e7dfb81c --- /dev/null +++ b/odl-aaa-moon/aaa-credential-store-api/pom.xml @@ -0,0 +1,22 @@ + + + + 4.0.0 + + org.opendaylight.mdsal + binding-parent + 0.8.1-Beryllium-SR1 + + + + org.opendaylight.aaa + aaa-credential-store-api + 0.3.1-Beryllium-SR1 + bundle + diff --git a/odl-aaa-moon/aaa-credential-store-api/src/main/yang/credential-model.yang b/odl-aaa-moon/aaa-credential-store-api/src/main/yang/credential-model.yang new file mode 100644 index 00000000..7d1f55a3 --- /dev/null +++ b/odl-aaa-moon/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-h2-store/.gitignore b/odl-aaa-moon/aaa-h2-store/.gitignore new file mode 100644 index 00000000..1dd33310 --- /dev/null +++ b/odl-aaa-moon/aaa-h2-store/.gitignore @@ -0,0 +1,2 @@ +/target/ +/target/ diff --git a/odl-aaa-moon/aaa-h2-store/pom.xml b/odl-aaa-moon/aaa-h2-store/pom.xml new file mode 100644 index 00000000..2b31525c --- /dev/null +++ b/odl-aaa-moon/aaa-h2-store/pom.xml @@ -0,0 +1,160 @@ + + + 4.0.0 + + org.opendaylight.aaa + aaa-parent + 0.3.1-Beryllium-SR1 + ../parent + + + aaa-h2-store + bundle + + + + org.opendaylight.controller + config-api + ${config.version} + + + org.opendaylight.controller + sal-binding-config + + + org.opendaylight.controller + sal-binding-api + + + org.opendaylight.controller + sal-common-util + + + org.apache.commons + commons-lang3 + + + + org.opendaylight.aaa + aaa-authn-api + + + org.opendaylight.aaa + aaa-authn + + + org.slf4j + slf4j-api + + + org.apache.felix + org.apache.felix.dependencymanager + provided + + + org.mockito + mockito-all + test + + + + + com.h2database + h2 + + + + junit + junit + test + + + org.slf4j + slf4j-simple + test + + + + + + + org.apache.felix + maven-bundle-plugin + ${bundle.plugin.version} + true + + + com.google.*,org.opendaylight.aaa.api.*,org.apache.felix.*,org.slf4j.*,org.opendaylight.*,org.osgi.*,org.apache.commons.lang3 + org.h2.* + h2 + + + + + org.opendaylight.yangtools + yang-maven-plugin + ${yangtools.version} + + + config + + generate-sources + + + + + org.opendaylight.controller.config.yangjmxgenerator.plugin.JMXGenerator + ${jmxGeneratorPath} + + urn:opendaylight:params:xml:ns:yang:controller==org.opendaylight.controller.config.yang + + + + org.opendaylight.yangtools.maven.sal.api.gen.plugin.CodeGeneratorImpl + ${salGeneratorPath} + + + true + + + + + + org.opendaylight.mdsal + maven-sal-api-gen-plugin + ${yangtools.version} + jar + + + org.opendaylight.controller + yang-jmx-generator-plugin + ${config.version} + + + + + org.codehaus.mojo + build-helper-maven-plugin + + + attach-artifacts + + attach-artifact + + package + + + + ${project.build.directory}/classes/initial/08-aaa-h2-store-config.xml + xml + config + + + + + + + + + diff --git a/odl-aaa-moon/aaa-h2-store/src/main/java/org/opendaylight/aaa/h2/config/IdmLightConfig.java b/odl-aaa-moon/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-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-h2-store/src/main/java/org/opendaylight/aaa/h2/persistence/AbstractStore.java b/odl-aaa-moon/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-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 { + /** + * 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 listAll() throws StoreException { + List 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 listFromStatement(PreparedStatement ps) throws StoreException { + List 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 not 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-h2-store/src/main/java/org/opendaylight/aaa/h2/persistence/DomainStore.java b/odl-aaa-moon/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-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 { + 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-h2-store/src/main/java/org/opendaylight/aaa/h2/persistence/GrantStore.java b/odl-aaa-moon/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-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 { + 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-h2-store/src/main/java/org/opendaylight/aaa/h2/persistence/H2Store.java b/odl-aaa-moon/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-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-h2-store/src/main/java/org/opendaylight/aaa/h2/persistence/RoleStore.java b/odl-aaa-moon/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-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 { + 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-h2-store/src/main/java/org/opendaylight/aaa/h2/persistence/StoreException.java b/odl-aaa-moon/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-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-h2-store/src/main/java/org/opendaylight/aaa/h2/persistence/UserStore.java b/odl-aaa-moon/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-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 { + 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-h2-store/src/main/java/org/opendaylight/yang/gen/v1/config/aaa/authn/h2/store/rev151128/AAAH2StoreModule.java b/odl-aaa-moon/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-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-h2-store/src/main/java/org/opendaylight/yang/gen/v1/config/aaa/authn/h2/store/rev151128/AAAH2StoreModuleFactory.java b/odl-aaa-moon/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-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-h2-store/src/main/resources/initial/08-aaa-h2-store-config.xml b/odl-aaa-moon/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-h2-store/src/main/resources/initial/08-aaa-h2-store-config.xml @@ -0,0 +1,26 @@ + + + + + + + + + authn:aaa-h2-store + aaa-h2-store + + + + + + config:aaa:authn:h2:store?module=aaa-h2-store&revision=2015-11-28 + + + + diff --git a/odl-aaa-moon/aaa-h2-store/src/main/yang/aaa-h2-store.yang b/odl-aaa-moon/aaa-h2-store/src/main/yang/aaa-h2-store.yang new file mode 100644 index 00000000..af2d9bdc --- /dev/null +++ b/odl-aaa-moon/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-h2-store/src/test/java/org/opendaylight/aaa/h2/persistence/DomainStoreTest.java b/odl-aaa-moon/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-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-h2-store/src/test/java/org/opendaylight/aaa/h2/persistence/GrantStoreTest.java b/odl-aaa-moon/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-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-h2-store/src/test/java/org/opendaylight/aaa/h2/persistence/H2StoreTest.java b/odl-aaa-moon/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-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-h2-store/src/test/java/org/opendaylight/aaa/h2/persistence/RoleStoreTest.java b/odl-aaa-moon/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-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-h2-store/src/test/java/org/opendaylight/aaa/h2/persistence/UserStoreTest.java b/odl-aaa-moon/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-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-idmlight/pom.xml b/odl-aaa-moon/aaa-idmlight/pom.xml new file mode 100644 index 00000000..5a86b0d1 --- /dev/null +++ b/odl-aaa-moon/aaa-idmlight/pom.xml @@ -0,0 +1,229 @@ + + + 4.0.0 + + org.opendaylight.aaa + aaa-parent + 0.3.1-Beryllium-SR1 + ../parent + + + aaa-idmlight + bundle + + + + + org.opendaylight.controller + config-api + ${config.version} + + + org.opendaylight.controller + sal-binding-config + + + org.opendaylight.controller + sal-binding-api + + + org.opendaylight.controller + sal-common-util + + + + org.opendaylight.aaa + aaa-authn-api + + + org.opendaylight.aaa + aaa-authn + + + org.slf4j + slf4j-api + + + com.sun.jersey + jersey-server + provided + + + javax.servlet + javax.servlet-api + provided + + + org.apache.felix + org.apache.felix.dependencymanager + provided + + + org.mockito + mockito-all + test + + + org.osgi + org.osgi.core + + + + + com.fasterxml.jackson.core + jackson-core + + + com.fasterxml.jackson.core + jackson-annotations + + + com.fasterxml.jackson.core + jackson-databind + + + com.fasterxml.jackson.datatype + jackson-datatype-json-org + + + com.fasterxml.jackson.jaxrs + jackson-jaxrs-base + + + com.fasterxml.jackson.jaxrs + jackson-jaxrs-json-provider + + + com.fasterxml.jackson.module + jackson-module-jaxb-annotations + + + + org.eclipse.jetty + jetty-servlets + provided + + + + + com.sun.jersey.jersey-test-framework + jersey-test-framework-grizzly2 + test + + + junit + junit + test + + + org.slf4j + slf4j-simple + test + + + + + + + + org.opendaylight.yangtools + yang-maven-plugin + ${yangtools.version} + + + config + + generate-sources + + + + + org.opendaylight.controller.config.yangjmxgenerator.plugin.JMXGenerator + ${jmxGeneratorPath} + + urn:opendaylight:params:xml:ns:yang:controller==org.opendaylight.controller.config.yang + + + + org.opendaylight.yangtools.maven.sal.api.gen.plugin.CodeGeneratorImpl + ${salGeneratorPath} + + + true + + + + + + org.opendaylight.mdsal + maven-sal-api-gen-plugin + ${yangtools.version} + jar + + + org.opendaylight.controller + yang-jmx-generator-plugin + ${config.version} + + + + + org.codehaus.mojo + build-helper-maven-plugin + + + attach-artifacts + + attach-artifact + + package + + + + ${project.build.directory}/classes/initial/08-aaa-idmlight-config.xml + xml + config + + + + + + attach-artifacts-idmtool + + attach-artifact + + package + + + + ${project.build.directory}/classes/idmtool.py + py + config + + + + + + + + + org.apache.felix + maven-bundle-plugin + + + true + + + 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 + /auth + + + + ${project.basedir}/META-INF + + + + + + diff --git a/odl-aaa-moon/aaa-idmlight/src/main/java/org/opendaylight/aaa/idm/IdmLightApplication.java b/odl-aaa-moon/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-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: + * http://{HOST}:{PORT}/auth/v1/ + * + * For example, the users REST endpoint is: + * http://{HOST}:{PORT}/auth/v1/users + * + * This application is responsible for interaction with the backing h2 + * database store. + * + * @author liemmn + * @author Ryan Goulding (ryandgoulding@gmail.com) + * @see org.opendaylight.aaa.idm.rest.DomainHandler + * @see org.opendaylight.aaa.idm.rest.UserHandler + * @see org.opendaylight.aaa.idm.rest.RoleHandler + */ +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> getClasses() { + return new HashSet>(Arrays.asList(VersionHandler.class, + DomainHandler.class, + RoleHandler.class, + UserHandler.class)); + } +} diff --git a/odl-aaa-moon/aaa-idmlight/src/main/java/org/opendaylight/aaa/idm/IdmLightProxy.java b/odl-aaa-moon/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-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, 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 + * PasswordCredentials. + */ + private static Map> claimCache = new ConcurrentHashMap<>(); + + // adds a store for the default "sdn" domain + static { + claimCache.put(IIDMStore.DEFAULT_DOMAIN, + new ConcurrentHashMap()); + } + + @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 cache = claimCache.get(domain); + if (cache == null) { + cache = new ConcurrentHashMap(); + 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 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 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 roles = new ArrayList(); + Grants grants = AAAIDMLightModule.getStore().getGrants(domain.getDomainid(), + user.getUserid()); + List 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 listDomains(String userId) { + LOG.debug("list Domains for userId: {}", userId); + List domains = new ArrayList(); + try { + Grants grants = AAAIDMLightModule.getStore().getGrants(userId); + List 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 listRoles(String userId, String domainName) { + LOG.debug("listRoles"); + List roles = new ArrayList(); + + 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 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-idmlight/src/main/java/org/opendaylight/aaa/idm/StoreBuilder.java b/odl-aaa-moon/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-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 + * AAAIDMLightModule.createInstance(). StoreBuilder is responsible + * for initializing the H2 database with initial default user account + * information. By default, the following users are created: + *
    + *
  1. admin
  2. + *
  3. user
  4. + *
+ * + * By default, the following domain is created: + *
    + *
  1. sdn
  2. + *
+ * + * By default, the following grants are created: + *
    + *
  1. admin with admin role on sdn
  2. + *
  3. admin with user role on sdn
  4. + *
  5. user with user role on sdn
  6. + *
+ * + * @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-idmlight/src/main/java/org/opendaylight/aaa/idm/rest/DomainHandler.java b/odl-aaa-moon/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-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 /auth/v1/domains. + * + * The following provides examples of curl commands and payloads to utilize the + * domains REST endpoint: + * + * Get All Domains + * curl -u admin:admin http://{HOST}:{PORT}/auth/v1/domains + * + * Get A Specific Domain + * curl -u admin:admin http://{HOST}:{PORT}/auth/v1/domains/{id} + * + * Create A Domain + * curl -u admin:admin -X POST -H "Content-Type: application/json" --data-binary {@literal @}domain.json http://{HOST}:{PORT}/auth/v1/domains + * Example domain.json { + * "description": "new domain", + * "enabled", "true", + * "name", "not sdn" + * } + * + * Update A Domain + * curl -u admin:admin -X PUT -H "Content-Type: application/json" --data-binary {@literal @}domain.json http://{HOST}:{PORT}/auth/v1/domains + * Example domain.json { + * "description": "new domain description", + * "enabled", "true", + * "name", "not sdn" + * } + * + * @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 domainId. + * + * @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: + * enabled: false + * description: An empty string (""). + * + * @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 roleList = new ArrayList(); + + 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 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 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 roleList = new ArrayList(); + + 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 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-idmlight/src/main/java/org/opendaylight/aaa/idm/rest/RoleHandler.java b/odl-aaa-moon/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-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 /auth/v1/roles. + * + * The following provides examples of curl commands and payloads to utilize the + * roles REST endpoint: + * + * Get All Roles + * curl -u admin:admin http://{HOST}:{PORT}/auth/v1/roles + * + * Get A Specific Role + * curl -u admin:admin http://{HOST}:{PORT}/auth/v1/roles/{id} + * + * Create A Role + * curl -u admin:admin -X POST -H "Content-Type: application/json" --data-binary {@literal @}role.json http://{HOST}:{PORT}/auth/v1/roles + * An example of role.json: + * { + * "name":"IT Administrator", + * "description":"A user role for IT admins" + * } + * + * Update A Role + * curl -u admin:admin -X PUT -H "Content-Type: application/json" --data-binary {@literal @}role.json http://{HOST}:{PORT}/auth/v1/roles/{id} + * An example of role.json: + * { + * "name":"IT Administrator Limited", + * "description":"A user role for IT admins who can only do one thing" + * } + * + * @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 id + * + * @param id the String id for the role + * @return A response with the role identified by id, 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 id. + * + * @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-idmlight/src/main/java/org/opendaylight/aaa/idm/rest/UserHandler.java b/odl-aaa-moon/aaa-idmlight/src/main/java/org/opendaylight/aaa/idm/rest/UserHandler.java new file mode 100644 index 00000000..24fefd7b --- /dev/null +++ b/odl-aaa-moon/aaa-idmlight/src/main/java/org/opendaylight/aaa/idm/rest/UserHandler.java @@ -0,0 +1,419 @@ +/* + * 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 /auth/v1/users. + * + * The following provides examples of how curl commands and payloads to utilize + * the users REST endpoint: + * + * Get All Users + * curl -u admin:admin http://{HOST}:{PORT}/auth/v1/users + * + * Get A Specific User + * curl -u admin:admin http://{HOST}:{PORT}/auth/v1/users/{id} + * + * Create A User + * curl -u admin:admin -X POST -H "Content-type: application/json" --data-binary {@literal @}user.json http://{HOST}:{PORT}/auth/v1/users + * An example of user.json file is: + * { + * "name": "admin2", + * "password", "admin2", + * "domain": "sdn" + * } + * + * Update A User + * curl -u admin:admin -X PUT -H "Content-type: application/json" --data-binary {@literal @}user.json http://{HOST}:{PORT}/auth/v1/users/{id} + * An example of user.json file is: + * { + * "name": "admin2", + * "password", "admin2", + * "domain": "sdn", + * "description", "Simple description." + * } + * + * Delete A User + * curl -u admin:admin -X DELETE http://{HOST}:{PORT}/auth/v1/users/{id} + * + * @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 /auth/v1/users 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 /auth/v1/users, the + * password field is replaced with REDACTED_PASSWORD for + * security reasons. + */ + private static final String REDACTED_PASSWORD = "**********"; + + /** + * When an HTTP GET is performed on /auth/v1/users, the salt + * field is replaced with REDACTED_SALT 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 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 id. 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) { + return new IDMError(404, String.format("user not found! id: %d", id), "").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: + * description: An empty string (""). + * email: An empty string (""). + * password: changeme enabled: + * true + * + * 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 Response 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 Response 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 Response 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 fieldName + * @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 fieldName + * @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 user 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-idmlight/src/main/java/org/opendaylight/aaa/idm/rest/VersionHandler.java b/odl-aaa-moon/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-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-idmlight/src/main/java/org/opendaylight/yang/gen/v1/config/aaa/authn/idmlight/rev151204/AAAIDMLightModule.java b/odl-aaa-moon/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-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 storeServiceTracker = new ServiceTracker<>(bundleContext, IIDMStore.class, + new ServiceTrackerCustomizer() { + @Override + public IIDMStore addingService(ServiceReference 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 reference, IIDMStore service) { + } + + @Override + public void removedService(ServiceReference 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-idmlight/src/main/java/org/opendaylight/yang/gen/v1/config/aaa/authn/idmlight/rev151204/AAAIDMLightModuleFactory.java b/odl-aaa-moon/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-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-idmlight/src/main/resources/WEB-INF/web.xml b/odl-aaa-moon/aaa-idmlight/src/main/resources/WEB-INF/web.xml new file mode 100644 index 00000000..9a19155a --- /dev/null +++ b/odl-aaa-moon/aaa-idmlight/src/main/resources/WEB-INF/web.xml @@ -0,0 +1,79 @@ + + + + + IdmLight + com.sun.jersey.spi.container.servlet.ServletContainer + + javax.ws.rs.Application + org.opendaylight.aaa.idm.IdmLightApplication + + + com.sun.jersey.api.json.POJOMappingFeaturetrue + + 1 + + + + IdmLight + /* + + + + + shiroEnvironmentClass + org.opendaylight.aaa.shiro.web.env.KarafIniWebEnvironment + + + + org.apache.shiro.web.env.EnvironmentLoaderListener + + + + ShiroFilter + org.opendaylight.aaa.shiro.filters.AAAFilter + + + + ShiroFilter + /* + + + + cross-origin-restconf + org.eclipse.jetty.servlets.CrossOriginFilter + + allowedOrigins + * + + + allowedMethods + GET,POST,OPTIONS,DELETE,PUT,HEAD + + + allowedHeaders + origin, content-type, accept, authorization, Authorization + + + + + cross-origin-restconf + /* + + + + + NB api + /* + POST + GET + PUT + PATCH + DELETE + HEAD + + + + \ No newline at end of file diff --git a/odl-aaa-moon/aaa-idmlight/src/main/resources/idmtool.py b/odl-aaa-moon/aaa-idmlight/src/main/resources/idmtool.py new file mode 100644 index 00000000..d0a31ba2 --- /dev/null +++ b/odl-aaa-moon/aaa-idmlight/src/main/resources/idmtool.py @@ -0,0 +1,247 @@ +#!/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() +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-idmlight/src/main/resources/initial/08-aaa-idmlight-config.xml b/odl-aaa-moon/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-idmlight/src/main/resources/initial/08-aaa-idmlight-config.xml @@ -0,0 +1,26 @@ + + + + + + + + + authn:aaa-idmlight + aaa-idmlight + + + + + + config:aaa:authn:idmlight?module=aaa-idmlight&revision=2015-12-04 + + + + diff --git a/odl-aaa-moon/aaa-idmlight/src/main/yang/aaa-idmlight.yang b/odl-aaa-moon/aaa-idmlight/src/main/yang/aaa-idmlight.yang new file mode 100644 index 00000000..4f28d755 --- /dev/null +++ b/odl-aaa-moon/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-idmlight/src/test/java/org/opendaylight/aaa/idm/persistence/PasswordHashTest.java b/odl-aaa-moon/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-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 lu = new LinkedList<>(); + lu.add(user); + users.setUsers(lu); + + Grants grants = new Grants(); + Grant grant = new Grant(); + List 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-idmlight/tests/cleardb.sh b/odl-aaa-moon/aaa-idmlight/tests/cleardb.sh new file mode 100644 index 00000000..6385b48d --- /dev/null +++ b/odl-aaa-moon/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-idmlight/tests/domain.json b/odl-aaa-moon/aaa-idmlight/tests/domain.json new file mode 100644 index 00000000..4dfd25e9 --- /dev/null +++ b/odl-aaa-moon/aaa-idmlight/tests/domain.json @@ -0,0 +1,5 @@ +{ + "domainid": "1", + "name":"R&D", + "enabled":"true" +} diff --git a/odl-aaa-moon/aaa-idmlight/tests/domain2.json b/odl-aaa-moon/aaa-idmlight/tests/domain2.json new file mode 100644 index 00000000..69244b30 --- /dev/null +++ b/odl-aaa-moon/aaa-idmlight/tests/domain2.json @@ -0,0 +1,5 @@ +{ + "domainid": "1", + "name":"ATG", + "enabled":"true" +} diff --git a/odl-aaa-moon/aaa-idmlight/tests/grant.json b/odl-aaa-moon/aaa-idmlight/tests/grant.json new file mode 100644 index 00000000..0c4a9e90 --- /dev/null +++ b/odl-aaa-moon/aaa-idmlight/tests/grant.json @@ -0,0 +1,4 @@ +{ + "roleid":"2", + "description":"role grant" +} diff --git a/odl-aaa-moon/aaa-idmlight/tests/grant2.json b/odl-aaa-moon/aaa-idmlight/tests/grant2.json new file mode 100644 index 00000000..ad685b7a --- /dev/null +++ b/odl-aaa-moon/aaa-idmlight/tests/grant2.json @@ -0,0 +1,4 @@ +{ + "roleid":"3", + "description":"role grant" +} diff --git a/odl-aaa-moon/aaa-idmlight/tests/result.json b/odl-aaa-moon/aaa-idmlight/tests/result.json new file mode 100644 index 00000000..a3dd995d --- /dev/null +++ b/odl-aaa-moon/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-idmlight/tests/role-admin.json b/odl-aaa-moon/aaa-idmlight/tests/role-admin.json new file mode 100644 index 00000000..cf93caae --- /dev/null +++ b/odl-aaa-moon/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-idmlight/tests/role-user.json b/odl-aaa-moon/aaa-idmlight/tests/role-user.json new file mode 100644 index 00000000..78588c9a --- /dev/null +++ b/odl-aaa-moon/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-idmlight/tests/test.sh b/odl-aaa-moon/aaa-idmlight/tests/test.sh new file mode 100644 index 00000000..3589be58 --- /dev/null +++ b/odl-aaa-moon/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-idmlight/tests/user.json b/odl-aaa-moon/aaa-idmlight/tests/user.json new file mode 100644 index 00000000..6f30d705 --- /dev/null +++ b/odl-aaa-moon/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-idmlight/tests/user2.json b/odl-aaa-moon/aaa-idmlight/tests/user2.json new file mode 100644 index 00000000..9864cdb2 --- /dev/null +++ b/odl-aaa-moon/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-idmlight/tests/userpwd.json b/odl-aaa-moon/aaa-idmlight/tests/userpwd.json new file mode 100644 index 00000000..e5258b98 --- /dev/null +++ b/odl-aaa-moon/aaa-idmlight/tests/userpwd.json @@ -0,0 +1,4 @@ +{ + "username":"peter", + "userpwd":"foobar" +} diff --git a/odl-aaa-moon/aaa-idp-mapping/pom.xml b/odl-aaa-moon/aaa-idp-mapping/pom.xml new file mode 100644 index 00000000..99c2322d --- /dev/null +++ b/odl-aaa-moon/aaa-idp-mapping/pom.xml @@ -0,0 +1,84 @@ + + + 4.0.0 + + org.opendaylight.aaa + aaa-parent + 0.3.1-Beryllium-SR1 + ../parent + + + aaa-authn-idpmapping + 0.3.1-Beryllium-SR1 + bundle + + + 1.5.2 + + + + + org.glassfish + javax.json + + + org.osgi + org.osgi.core + provided + + + org.slf4j + slf4j-api + + + org.apache.felix + org.apache.felix.dependencymanager + provided + + + + + junit + junit + test + + + org.mockito + mockito-all + test + + + org.slf4j + slf4j-simple + test + + + org.powermock + powermock-api-mockito + ${powermock.version} + test + + + org.powermock + powermock-module-junit4 + ${powermock.version} + test + + + + + + + org.apache.felix + maven-bundle-plugin + true + + + org.opendaylight.aaa.idpmapping.Activator + + ${project.basedir}/META-INF + + + + + diff --git a/odl-aaa-moon/aaa-idp-mapping/src/main/java/org/opendaylight/aaa/idpmapping/Activator.java b/odl-aaa-moon/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-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-idp-mapping/src/main/java/org/opendaylight/aaa/idpmapping/IdpJson.java b/odl-aaa-moon/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-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 loadJsonArray(JsonParser parser, Event event) { + List list = new ArrayList(); + + if (event != Event.START_ARRAY) { + JsonLocation location = parser.getLocation(); + throw new IllegalStateException( + String.format( + "expected JSON parsing event to be START_ARRAY, not %s location(line=%d column=%d offset=%d)", + event, location.getLineNumber(), location.getColumnNumber(), + location.getStreamOffset())); + } + event = parser.next(); // consume START_ARRAY + while (event != Event.END_ARRAY) { + Object obj; + + obj = loadJsonItem(parser, event); + list.add(obj); + event = parser.next(); // next array item or END_ARRAY + } + return list; + } + + private Map loadJsonObject(JsonParser parser, Event event) { + Map map = new LinkedHashMap(); + + if (event != Event.START_OBJECT) { + JsonLocation location = parser.getLocation(); + throw new IllegalStateException(String.format( + "expected JSON parsing event to be START_OBJECT, not %s, ", + "location(line=%d column=%d offset=%d)", event, location.getLineNumber(), + location.getColumnNumber(), location.getStreamOffset())); + } + event = parser.next(); // consume START_OBJECT + while (event != Event.END_OBJECT) { + if (event == Event.KEY_NAME) { + String key; + Object value; + + key = parser.getString(); + event = parser.next(); // consume key + value = loadJsonItem(parser, event); + map.put(key, value); + } else { + JsonLocation location = parser.getLocation(); + throw new IllegalStateException( + String.format( + "expected JSON parsing event to be KEY_NAME, not %s, location(line=%d column=%d offset=%d)", + event, location.getLineNumber(), location.getColumnNumber(), + location.getStreamOffset())); + + } + event = parser.next(); // next key or END_OBJECT + } + return map; + } + + public String dumpJson(Object obj) { + Map properties = new HashMap(1); + properties.put(JsonGenerator.PRETTY_PRINTING, true); + JsonGeneratorFactory generatorFactory = Json.createGeneratorFactory(properties); + StringWriter stringWriter = new StringWriter(); + JsonGenerator generator = generatorFactory.createGenerator(stringWriter); + + dumpJsonItem(generator, obj); + generator.close(); + return stringWriter.toString(); + } + + private void dumpJsonItem(JsonGenerator generator, Object obj) { + // ordered by expected occurrence + if (obj instanceof String) { + generator.write((String) obj); + } else if (obj instanceof List) { + generator.writeStartArray(); + @SuppressWarnings("unchecked") + List list = (List) obj; + dumpJsonArray(generator, list); + } else if (obj instanceof Map) { + generator.writeStartObject(); + @SuppressWarnings("unchecked") + Map map = (Map) obj; + dumpJsonObject(generator, map); + } else if (obj instanceof Long) { + generator.write(((Long) obj).longValue()); + } else if (obj instanceof Boolean) { + generator.write(((Boolean) obj).booleanValue()); + } else if (obj == null) { + generator.writeNull(); + } else if (obj instanceof Double) { + generator.write(((Double) obj).doubleValue()); + } else { + throw new IllegalStateException( + String.format( + "unsupported data type, must be String, Long, Double, Boolean, List, Map, or null, not %s", + obj.getClass().getSimpleName())); + } + } + + private void dumpJsonArray(JsonGenerator generator, List list) { + for (Object obj : list) { + dumpJsonItem(generator, obj); + } + generator.writeEnd(); + } + + private void dumpJsonObject(JsonGenerator generator, Map map) { + + for (Map.Entry entry : map.entrySet()) { + String key = entry.getKey(); + Object obj = entry.getValue(); + + // ordered by expected occurrence + if (obj instanceof String) { + generator.write(key, (String) obj); + } else if (obj instanceof List) { + generator.writeStartArray(key); + @SuppressWarnings("unchecked") + List list = (List) obj; + dumpJsonArray(generator, list); + } else if (obj instanceof Map) { + generator.writeStartObject(key); + @SuppressWarnings("unchecked") + Map map1 = (Map) obj; + dumpJsonObject(generator, map1); + } else if (obj instanceof Long) { + generator.write(key, ((Long) obj).longValue()); + } else if (obj instanceof Boolean) { + generator.write(key, ((Boolean) obj).booleanValue()); + } else if (obj == null) { + generator.write(key, JsonValue.NULL); + } else if (obj instanceof Double) { + generator.write(key, ((Double) obj).doubleValue()); + } else { + throw new IllegalStateException( + String.format( + "unsupported data type, must be String, Long, Double, Boolean, List, Map, or null, not %s", + obj.getClass().getSimpleName())); + } + } + generator.writeEnd(); + } + +} diff --git a/odl-aaa-moon/aaa-idp-mapping/src/main/java/org/opendaylight/aaa/idpmapping/InvalidRuleException.java b/odl-aaa-moon/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-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-idp-mapping/src/main/java/org/opendaylight/aaa/idpmapping/InvalidTypeException.java b/odl-aaa-moon/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-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-idp-mapping/src/main/java/org/opendaylight/aaa/idpmapping/InvalidValueException.java b/odl-aaa-moon/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-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-idp-mapping/src/main/java/org/opendaylight/aaa/idpmapping/RuleProcessor.java b/odl-aaa-moon/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-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 = ""; + public String statementIdFormat = ""; + + /* + * Reserved variables + */ + public static final String ASSERTION = "assertion"; + public static final String RULE_NUMBER = "rule_number"; + public static final String RULE_NAME = "rule_name"; + public static final String BLOCK_NUMBER = "block_number"; + public static final String BLOCK_NAME = "block_name"; + public static final String STATEMENT_NUMBER = "statement_number"; + public static final String REGEXP_ARRAY_VARIABLE = "regexp_array"; + public static final String REGEXP_MAP_VARIABLE = "regexp_map"; + + private static final String REGEXP_NAMED_GROUP_PAT = "\\(\\?<([a-zA-Z][a-zA-Z0-9]*)>"; + private static final Pattern REGEXP_NAMED_GROUP_RE = Pattern.compile(REGEXP_NAMED_GROUP_PAT); + + List> rules = null; + boolean success = true; + Map> mappings = null; + + public RuleProcessor(java.io.Reader rulesIn, Map> mappings) { + this.mappings = mappings; + IdpJson json = new IdpJson(); + @SuppressWarnings("unchecked") + List> loadJson = (List>) json.loadJson(rulesIn); + rules = loadJson; + } + + public RuleProcessor(Path rulesIn, Map> mappings) + throws IOException { + this.mappings = mappings; + IdpJson json = new IdpJson(); + @SuppressWarnings("unchecked") + List> loadJson = (List>) json.loadJson(rulesIn); + rules = loadJson; + } + + public RuleProcessor(String rulesIn, Map> mappings) { + this.mappings = mappings; + IdpJson json = new IdpJson(); + @SuppressWarnings("unchecked") + List> loadJson = (List>) json.loadJson(rulesIn); + rules = loadJson; + } + + /* + * For some odd reason the Java Regular Expression API does not include a + * way to retrieve a map of the named groups and their values. The API only + * permits us to retrieve a named group if we already know the group names. + * So instead we parse the pattern string looking for named groups, extract + * the name, look up the value of the named group and build a map from that. + */ + + private Map regexpGroupMap(String pattern, Matcher matcher) { + Map groupMap = new HashMap(); + Matcher groupMatcher = REGEXP_NAMED_GROUP_RE.matcher(pattern); + + while (groupMatcher.find()) { + String groupName = groupMatcher.group(1); + + groupMap.put(groupName, matcher.group(groupName)); + } + return groupMap; + } + + static public String join(List list, String conjunction) { + StringBuilder sb = new StringBuilder(); + boolean first = true; + for (Object item : list) { + if (first) { + first = false; + } else { + sb.append(conjunction); + } + sb.append(item.toString()); + } + return sb.toString(); + } + + private List regexpGroupList(Matcher matcher) { + List groupList = new ArrayList(matcher.groupCount() + 1); + groupList.add(0, matcher.group(0)); + for (int i = 1; i < matcher.groupCount() + 1; i++) { + groupList.add(i, matcher.group(i)); + } + return groupList; + } + + private String objToString(Object obj) { + StringWriter sw = new StringWriter(); + objToStringItem(sw, obj); + return sw.toString(); + } + + private void objToStringItem(StringWriter sw, Object obj) { + // ordered by expected occurrence + if (obj instanceof String) { + sw.write('"'); + sw.write(((String) obj).replaceAll("\"", "\\\"")); + sw.write('"'); + } else if (obj instanceof List) { + @SuppressWarnings("unchecked") + List list = (List) obj; + boolean first = true; + + sw.write('['); + for (Object item : list) { + if (first) { + first = false; + } else { + sw.write(", "); + } + objToStringItem(sw, item); + } + sw.write(']'); + } else if (obj instanceof Map) { + @SuppressWarnings("unchecked") + Map map = (Map) obj; + boolean first = true; + + sw.write('{'); + for (Map.Entry entry : map.entrySet()) { + String key = entry.getKey(); + Object value = entry.getValue(); + + if (first) { + first = false; + } else { + sw.write(", "); + } + + objToStringItem(sw, key); + sw.write(": "); + objToStringItem(sw, value); + + } + sw.write('}'); + } else if (obj instanceof Long) { + sw.write(((Long) obj).toString()); + } else if (obj instanceof Boolean) { + sw.write(((Boolean) obj).toString()); + } else if (obj == null) { + sw.write("null"); + } else if (obj instanceof Double) { + sw.write(((Double) obj).toString()); + } else { + throw new IllegalStateException( + String.format( + "unsupported data type, must be String, Long, Double, Boolean, List, Map, or null, not %s", + obj.getClass().getSimpleName())); + } + } + + private Object deepCopy(Object obj) { + // ordered by expected occurrence + if (obj instanceof String) { + return obj; // immutable + } else if (obj instanceof List) { + List new_list = new ArrayList(); + @SuppressWarnings("unchecked") + List list = (List) obj; + for (Object item : list) { + new_list.add(deepCopy(item)); + } + return new_list; + } else if (obj instanceof Map) { + Map new_map = new LinkedHashMap(); + @SuppressWarnings("unchecked") + Map map = (Map) obj; + for (Map.Entry entry : map.entrySet()) { + String key = entry.getKey(); // immutable + Object value = entry.getValue(); + new_map.put(key, deepCopy(value)); + } + return new_map; + } else if (obj instanceof Long) { + return obj; // immutable + } else if (obj instanceof Boolean) { + return obj; // immutable + } else if (obj == null) { + return null; + } else if (obj instanceof Double) { + return obj; // immutable + } else { + throw new IllegalStateException( + String.format( + "unsupported data type, must be String, Long, Double, Boolean, List, Map, or null, not %s", + obj.getClass().getSimpleName())); + } + } + + public String ruleId(Map namespace) { + return substituteVariables(ruleIdFormat, namespace); + } + + public String statementId(Map namespace) { + return substituteVariables(statementIdFormat, namespace); + } + + public String substituteVariables(String string, Map namespace) { + StringBuffer sb = new StringBuffer(); + Matcher matcher = Token.VARIABLE_RE.matcher(string); + + while (matcher.find()) { + Token token = new Token(matcher.group(0), namespace); + token.load(); + String replacement; + if (token.type == TokenType.STRING) { + replacement = token.getStringValue(); + } else { + replacement = objToString(token.getObjectValue()); + } + + matcher.appendReplacement(sb, replacement); + } + matcher.appendTail(sb); + return sb.toString(); + } + + Map getMapping(Map namespace, Map rule) { + Map mapping = null; + String mappingName = null; + + try { + @SuppressWarnings("unchecked") + Map map = (Map) rule.get("mapping"); + mapping = map; + } catch (java.lang.ClassCastException e) { + throw new InvalidRuleException(String.format( + "%s rule defines 'mapping' but it is not a Map", this.ruleId(namespace), 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 statement) { + Token verb; + + if (statement.size() < 1) { + throw new InvalidRuleException("statement has no verb"); + } + + try { + verb = new Token(statement.get(0), null); + } catch (Exception e) { + throw new InvalidRuleException(String.format( + "statement first member (i.e. verb) error %s", e)); + } + + if (verb.type != TokenType.STRING) { + throw new InvalidRuleException(String.format( + "statement first member (i.e. verb) must be a string, not %s", verb.type)); + } + + return (verb.getStringValue()).toLowerCase(); + } + + private Token getToken(String verb, List statement, int index, + Map namespace, Set storageTypes, + Set tokenTypes) { + Object item; + Token token; + + try { + item = statement.get(index); + } catch (IndexOutOfBoundsException e) { + throw new InvalidRuleException(String.format( + "verb '%s' requires at least %d items but only %d are available.", verb, + index + 1, statement.size(), 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 statement, int index, + Map namespace, Set tokenTypes) { + Object item; + Token token; + + try { + item = statement.get(index); + } catch (IndexOutOfBoundsException e) { + throw new InvalidRuleException(String.format( + "verb '%s' requires at least %d items but only %d are available.", verb, + index + 1, statement.size(), 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 statement, int index, + Set tokenTypes) { + Object item; + + try { + item = statement.get(index); + } catch (IndexOutOfBoundsException e) { + throw new InvalidRuleException(String.format( + "verb '%s' requires at least %d items but only %d are available.", verb, + index + 1, statement.size(), 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 statement, int index, + Map namespace) { + Object item; + Token token; + + try { + item = statement.get(index); + } catch (IndexOutOfBoundsException e) { + throw new InvalidRuleException(String.format( + "verb '%s' requires at least %d items but only %d are available.", verb, + index + 1, statement.size(), 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 process(String assertionJson) { + ProcessResult result; + IdpJson json = new IdpJson(); + @SuppressWarnings("unchecked") + Map assertion = (Map) json.loadJson(assertionJson); + LOG.info("Assertion JSON: {}", json.dumpJson(assertion)); + this.success = true; + + for (int ruleNumber = 0; ruleNumber < this.rules.size(); ruleNumber++) { + Map namespace = new HashMap(); + Map rule = (Map) this.rules.get(ruleNumber); + namespace.put(RULE_NUMBER, Long.valueOf(ruleNumber)); + namespace.put(RULE_NAME, ""); + namespace.put(ASSERTION, deepCopy(assertion)); + + result = processRule(namespace, rule); + + if (result == ProcessResult.RULE_SUCCESS) { + Map mapped = new LinkedHashMap(); + Map mapping = getMapping(namespace, rule); + for (Map.Entry entry : ((Map) mapping).entrySet()) { + String key = entry.getKey(); + Object value = entry.getValue(); + Object newValue = null; + try { + Token token = new Token(value, namespace); + newValue = token.get(); + } catch (Exception e) { + throw new InvalidRuleException(String.format( + "%s unable to get value for mapping %s=%s, %s", ruleId(namespace), + key, value, e), e); + } + mapped.put(key, newValue); + } + return mapped; + } + } + return null; + } + + private ProcessResult processRule(Map namespace, Map rule) { + ProcessResult result = ProcessResult.BLOCK_CONTINUE; + @SuppressWarnings("unchecked") + List>> statementBlocks = (List>>) rule.get("statement_blocks"); + if (statementBlocks == null) { + throw new InvalidRuleException("rule missing 'statement_blocks'"); + + } + for (int blockNumber = 0; blockNumber < statementBlocks.size(); blockNumber++) { + List> block = (List>) statementBlocks.get(blockNumber); + namespace.put(BLOCK_NUMBER, 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 namespace, List> block) { + ProcessResult result = ProcessResult.STATEMENT_CONTINUE; + + for (int statementNumber = 0; statementNumber < block.size(); statementNumber++) { + List statement = (List) block.get(statementNumber); + namespace.put(STATEMENT_NUMBER, 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 namespace, List statement) { + ProcessResult result = ProcessResult.STATEMENT_CONTINUE; + String verb = getVerb(statement); + + switch (verb) { + case "set": + result = verbSet(verb, namespace, statement); + break; + case "length": + result = verbLength(verb, namespace, statement); + break; + case "interpolate": + result = verbInterpolate(verb, namespace, statement); + break; + case "append": + result = verbAppend(verb, namespace, statement); + break; + case "unique": + result = verbUnique(verb, namespace, statement); + break; + case "split": + result = verbSplit(verb, namespace, statement); + break; + case "join": + result = verbJoin(verb, namespace, statement); + break; + case "lower": + result = verbLower(verb, namespace, statement); + break; + case "upper": + result = verbUpper(verb, namespace, statement); + break; + case "in": + result = verbIn(verb, namespace, statement); + break; + case "not_in": + result = verbNotIn(verb, namespace, statement); + break; + case "compare": + result = verbCompare(verb, namespace, statement); + break; + case "regexp": + result = verbRegexp(verb, namespace, statement); + break; + case "regexp_replace": + result = verbRegexpReplace(verb, namespace, statement); + break; + case "exit": + result = verbExit(verb, namespace, statement); + break; + case "continue": + result = verbContinue(verb, namespace, statement); + break; + default: + throw new InvalidRuleException(String.format("unknown verb '%s'", verb)); + } + + return result; + } + + private ProcessResult verbSet(String verb, Map namespace, List statement) { + Token variable = getVariable(verb, statement, 1, namespace); + Token parameter = getParameter(verb, statement, 2, namespace, null); + + variable.set(parameter.getObjectValue()); + this.success = true; + + if (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 namespace, + List statement) { + Token variable = getVariable(verb, statement, 1, namespace); + Token parameter = getParameter(verb, statement, 2, namespace, + EnumSet.of(TokenType.ARRAY, TokenType.MAP, TokenType.STRING)); + long length; + + switch (parameter.type) { + case ARRAY: { + length = parameter.getListValue().size(); + } + break; + case MAP: { + length = parameter.getMapValue().size(); + } + break; + case STRING: { + length = parameter.getStringValue().length(); + } + break; + default: + throw new IllegalStateException(String.format("unexpected token type: %s", + parameter.type)); + } + + variable.set(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 namespace, + List statement) { + Token variable = getVariable(verb, statement, 1, namespace); + String string = (String) getRawParameter(verb, statement, 2, EnumSet.of(TokenType.STRING)); + String newValue = null; + + try { + newValue = substituteVariables(string, namespace); + } catch (Exception e) { + throw new InvalidValueException(String.format( + "verb '%s' failed, variable='%s' string='%s': %s", verb, variable, string, e)); + } + variable.set(newValue); + this.success = true; + + if (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 namespace, + List statement) { + Token variable = getToken(verb, statement, 1, namespace, + EnumSet.of(TokenStorageType.VARIABLE), EnumSet.of(TokenType.ARRAY)); + Token item = getParameter(verb, statement, 2, namespace, null); + + try { + List list = variable.getListValue(); + list.add(item.getObjectValue()); + } catch (Exception e) { + throw new InvalidValueException(String.format( + "verb '%s' failed, variable='%s' item='%s': %s", verb, + variable.getObjectValue(), item.getObjectValue(), e)); + } + this.success = true; + + if (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 namespace, + List statement) { + Token variable = getVariable(verb, statement, 1, namespace); + Token array = getParameter(verb, statement, 2, namespace, EnumSet.of(TokenType.ARRAY)); + + List newValue = new ArrayList(); + Set seen = new HashSet(); + + for (Object member : array.getListValue()) { + if (seen.contains(member)) { + continue; + } else { + newValue.add(member); + seen.add(member); + } + } + + variable.set(newValue); + this.success = true; + + if (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 namespace, + List statement) { + Token variable = getVariable(verb, statement, 1, namespace); + Token string = getParameter(verb, statement, 2, namespace, EnumSet.of(TokenType.STRING)); + Token pattern = getParameter(verb, statement, 3, namespace, EnumSet.of(TokenType.STRING)); + + Pattern regexp; + List newValue; + + try { + regexp = Pattern.compile(pattern.getStringValue()); + } catch (Exception e) { + throw new InvalidValueException(String.format( + "verb '%s' failed, bad regular expression pattern '%s', %s", verb, + pattern.getObjectValue(), e)); + } + try { + newValue = new ArrayList( + Arrays.asList(regexp.split((String) string.getStringValue()))); + } catch (Exception e) { + throw new InvalidValueException(String.format( + "verb '%s' failed, string='%s' pattern='%s', %s", verb, + string.getObjectValue(), pattern.getObjectValue(), e)); + } + + variable.set(newValue); + this.success = true; + + if (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 namespace, + List statement) { + Token variable = getVariable(verb, statement, 1, namespace); + Token array = getParameter(verb, statement, 2, namespace, EnumSet.of(TokenType.ARRAY)); + Token conjunction = getParameter(verb, statement, 3, namespace, + EnumSet.of(TokenType.STRING)); + String newValue; + + try { + newValue = join(array.getListValue(), conjunction.getStringValue()); + } catch (Exception e) { + throw new InvalidValueException(String.format( + "verb '%s' failed, array=%s conjunction='%s', %s", verb, + array.getObjectValue(), conjunction.getObjectValue(), e)); + } + + variable.set(newValue); + this.success = true; + + if (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 namespace, + List statement) { + Token variable = getVariable(verb, statement, 1, namespace); + Token parameter = getParameter(verb, statement, 2, namespace, + EnumSet.of(TokenType.STRING, TokenType.ARRAY, TokenType.MAP)); + + try { + switch (parameter.type) { + case STRING: { + String oldValue = parameter.getStringValue(); + String newValue; + newValue = oldValue.toLowerCase(); + variable.set(newValue); + } + break; + case ARRAY: { + List oldValue = parameter.getListValue(); + List newValue = new ArrayList(oldValue.size()); + String oldItem; + String newItem; + + for (Object item : oldValue) { + try { + oldItem = (String) item; + } catch (ClassCastException e) { + throw new InvalidValueException(String.format( + "verb '%s' failed, array item (%s) is not a string, array=%s", + verb, item, parameter.getObjectValue(), e)); + } + newItem = oldItem.toLowerCase(); + newValue.add(newItem); + } + variable.set(newValue); + } + break; + case MAP: { + Map oldValue = parameter.getMapValue(); + Map newValue = new LinkedHashMap(oldValue.size()); + + for (Map.Entry entry : oldValue.entrySet()) { + String oldKey; + String newKey; + Object value = entry.getValue(); + + oldKey = entry.getKey(); + newKey = oldKey.toLowerCase(); + newValue.put(newKey, value); + } + variable.set(newValue); + } + break; + default: + throw new IllegalStateException(String.format("unexpected token type: %s", + parameter.type)); + } + } catch (Exception e) { + throw new InvalidValueException(String.format( + "verb '%s' failed, variable='%s' parameter='%s': %s", verb, variable, + parameter.getObjectValue(), e), e); + } + this.success = true; + + if (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 namespace, + List statement) { + Token variable = getVariable(verb, statement, 1, namespace); + Token parameter = getParameter(verb, statement, 2, namespace, + EnumSet.of(TokenType.STRING, TokenType.ARRAY, TokenType.MAP)); + + try { + switch (parameter.type) { + case STRING: { + String oldValue = parameter.getStringValue(); + String newValue; + newValue = oldValue.toUpperCase(); + variable.set(newValue); + } + break; + case ARRAY: { + List oldValue = parameter.getListValue(); + List newValue = new ArrayList(oldValue.size()); + String oldItem; + String newItem; + + for (Object item : oldValue) { + try { + oldItem = (String) item; + } catch (ClassCastException e) { + throw new InvalidValueException(String.format( + "verb '%s' failed, array item (%s) is not a string, array=%s", + verb, item, parameter.getObjectValue(), e)); + } + newItem = oldItem.toUpperCase(); + newValue.add(newItem); + } + variable.set(newValue); + } + break; + case MAP: { + Map oldValue = parameter.getMapValue(); + Map newValue = new LinkedHashMap(oldValue.size()); + + for (Map.Entry entry : oldValue.entrySet()) { + String oldKey; + String newKey; + Object value = entry.getValue(); + + oldKey = entry.getKey(); + newKey = oldKey.toUpperCase(); + newValue.put(newKey, value); + } + variable.set(newValue); + } + break; + default: + throw new IllegalStateException(String.format("unexpected token type: %s", + parameter.type)); + } + } catch (Exception e) { + throw new InvalidValueException(String.format( + "verb '%s' failed, variable='%s' parameter='%s': %s", verb, variable, + parameter.getObjectValue(), e), e); + } + this.success = true; + + if (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 namespace, List statement) { + Token member = getParameter(verb, statement, 1, namespace, null); + Token collection = getParameter(verb, statement, 2, namespace, + EnumSet.of(TokenType.ARRAY, TokenType.MAP, TokenType.STRING)); + + switch (collection.type) { + case ARRAY: { + this.success = collection.getListValue().contains(member.getObjectValue()); + } + break; + case MAP: { + if (member.type != TokenType.STRING) { + throw new InvalidTypeException(String.format( + "verb '%s' requires parameter #1 to be a %swhen parameter #2 is a %s", + TokenType.STRING, collection.type)); + } + this.success = collection.getMapValue().containsKey(member.getObjectValue()); + } + break; + case STRING: { + if (member.type != TokenType.STRING) { + throw new InvalidTypeException(String.format( + "verb '%s' requires parameter #1 to be a %swhen parameter #2 is a %s", + TokenType.STRING, collection.type)); + } + this.success = (collection.getStringValue()).contains(member.getStringValue()); + } + break; + default: + throw new IllegalStateException(String.format("unexpected token type: %s", + collection.type)); + } + + if (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 namespace, + List statement) { + Token member = getParameter(verb, statement, 1, namespace, null); + Token collection = getParameter(verb, statement, 2, namespace, + EnumSet.of(TokenType.ARRAY, TokenType.MAP, TokenType.STRING)); + + switch (collection.type) { + case ARRAY: { + this.success = !collection.getListValue().contains(member.getObjectValue()); + } + break; + case MAP: { + if (member.type != TokenType.STRING) { + throw new InvalidTypeException(String.format( + "verb '%s' requires parameter #1 to be a %swhen parameter #2 is a %s", + TokenType.STRING, collection.type)); + } + this.success = !collection.getMapValue().containsKey(member.getObjectValue()); + } + break; + case STRING: { + if (member.type != TokenType.STRING) { + throw new InvalidTypeException(String.format( + "verb '%s' requires parameter #1 to be a %swhen parameter #2 is a %s", + TokenType.STRING, collection.type)); + } + this.success = !(collection.getStringValue()).contains(member.getStringValue()); + } + break; + default: + throw new IllegalStateException(String.format("unexpected token type: %s", + collection.type)); + } + + if (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 namespace, + List statement) { + Token left = getParameter(verb, statement, 1, namespace, null); + Token op = getParameter(verb, statement, 2, namespace, EnumSet.of(TokenType.STRING)); + Token right = getParameter(verb, statement, 3, namespace, null); + String invalidOp = "operator %s not supported for type %s"; + TokenType tokenType; + String opValue = op.getStringValue(); + boolean result; + + if (left.type != right.type) { + throw new InvalidTypeException(String.format( + "verb '%s' both items must have the same type left is %s and right is %s", + verb, left.type, right.type)); + } else { + tokenType = left.type; + } + + switch (opValue) { + case "==": + case "!=": { + switch (tokenType) { + case STRING: { + String leftValue = left.getStringValue(); + String rightValue = right.getStringValue(); + result = leftValue.equals(rightValue); + } + break; + case INTEGER: { + Long leftValue = left.getLongValue(); + Long rightValue = right.getLongValue(); + result = leftValue.equals(rightValue); + } + break; + case REAL: { + Double leftValue = left.getDoubleValue(); + Double rightValue = right.getDoubleValue(); + result = leftValue.equals(rightValue); + } + break; + case ARRAY: { + List leftValue = left.getListValue(); + List rightValue = right.getListValue(); + result = leftValue.equals(rightValue); + } + break; + case MAP: { + Map leftValue = left.getMapValue(); + Map rightValue = right.getMapValue(); + result = leftValue.equals(rightValue); + } + break; + case BOOLEAN: { + Boolean leftValue = left.getBooleanValue(); + Boolean rightValue = right.getBooleanValue(); + result = leftValue.equals(rightValue); + } + break; + case NULL: { + result = (left.getNullValue() == right.getNullValue()); + } + break; + default: { + throw new IllegalStateException(String.format("unexpected token type: %s", + tokenType)); + } + } + if (opValue.equals("!=")) { // negate the sense of the test + result = !result; + } + } + break; + case "<": + case ">=": { + switch (tokenType) { + case STRING: { + String leftValue = left.getStringValue(); + String rightValue = right.getStringValue(); + result = leftValue.compareTo(rightValue) < 0; + } + break; + case INTEGER: { + Long leftValue = left.getLongValue(); + Long rightValue = right.getLongValue(); + result = leftValue < rightValue; + } + break; + case REAL: { + Double leftValue = left.getDoubleValue(); + Double rightValue = right.getDoubleValue(); + result = leftValue < rightValue; + } + break; + case ARRAY: + case MAP: + case BOOLEAN: + case NULL: { + throw new InvalidRuleException(String.format(invalidOp, opValue, tokenType)); + } + default: { + throw new IllegalStateException(String.format("unexpected token type: %s", + tokenType)); + } + } + if (opValue.equals(">=")) { // negate the sense of the test + result = !result; + } + } + break; + case ">": + case "<=": { + switch (tokenType) { + case STRING: { + String leftValue = left.getStringValue(); + String rightValue = right.getStringValue(); + result = leftValue.compareTo(rightValue) > 0; + } + break; + case INTEGER: { + Long leftValue = left.getLongValue(); + Long rightValue = right.getLongValue(); + result = leftValue > rightValue; + } + break; + case REAL: { + Double leftValue = left.getDoubleValue(); + Double rightValue = right.getDoubleValue(); + result = leftValue > rightValue; + } + break; + case ARRAY: + case MAP: + case BOOLEAN: + case NULL: { + throw new InvalidRuleException(String.format(invalidOp, opValue, tokenType)); + } + default: { + throw new IllegalStateException(String.format("unexpected token type: %s", + tokenType)); + } + } + if (opValue.equals("<=")) { // negate the sense of the test + result = !result; + } + } + break; + default: { + throw new InvalidRuleException(String.format( + "verb '%s' has unknown comparison operator '%s'", verb, op.getObjectValue())); + } + } + this.success = result; + + if (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 namespace, + List statement) { + Token string = getParameter(verb, statement, 1, namespace, EnumSet.of(TokenType.STRING)); + Token pattern = getParameter(verb, statement, 2, namespace, EnumSet.of(TokenType.STRING)); + + Pattern regexp; + Matcher matcher; + + try { + regexp = Pattern.compile(pattern.getStringValue()); + } catch (Exception e) { + throw new InvalidValueException(String.format( + "verb '%s' failed, bad regular expression pattern '%s', %s", verb, + pattern.getObjectValue(), e)); + } + matcher = regexp.matcher(string.getStringValue()); + + if (matcher.find()) { + this.success = true; + namespace.put(REGEXP_ARRAY_VARIABLE, regexpGroupList(matcher)); + namespace.put(REGEXP_MAP_VARIABLE, regexpGroupMap(pattern.getStringValue(), matcher)); + } else { + this.success = false; + namespace.put(REGEXP_ARRAY_VARIABLE, new ArrayList()); + namespace.put(REGEXP_MAP_VARIABLE, new HashMap()); + } + + if (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 namespace, + List statement) { + Token variable = getVariable(verb, statement, 1, namespace); + Token string = getParameter(verb, statement, 2, namespace, EnumSet.of(TokenType.STRING)); + Token pattern = getParameter(verb, statement, 3, namespace, EnumSet.of(TokenType.STRING)); + Token replacement = getParameter(verb, statement, 4, namespace, + EnumSet.of(TokenType.STRING)); + + Pattern regexp; + Matcher matcher; + String newValue; + + try { + regexp = Pattern.compile(pattern.getStringValue()); + } catch (Exception e) { + throw new InvalidValueException(String.format( + "verb '%s' failed, bad regular expression pattern '%s', %s", verb, + pattern.getObjectValue(), e)); + } + matcher = regexp.matcher(string.getStringValue()); + + newValue = matcher.replaceAll(replacement.getStringValue()); + variable.set(newValue); + this.success = true; + + if (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 namespace, + List statement) { + ProcessResult statementResult = ProcessResult.STATEMENT_CONTINUE; + + Token exitStatusParam = getParameter(verb, statement, 1, namespace, + EnumSet.of(TokenType.STRING)); + Token criteriaParam = getParameter(verb, statement, 2, namespace, + EnumSet.of(TokenType.STRING)); + String exitStatus = (exitStatusParam.getStringValue()).toLowerCase(); + String criteria = (criteriaParam.getStringValue()).toLowerCase(); + ProcessResult result; + boolean doExit; + + if (exitStatus.equals("rule_succeeds")) { + result = ProcessResult.RULE_SUCCESS; + } else if (exitStatus.equals("rule_fails")) { + result = ProcessResult.RULE_FAIL; + } else { + throw new InvalidRuleException(String.format("verb='%s' unknown exit status '%s'", + verb, exitStatus)); + } + + if (criteria.equals("if_success")) { + if (this.success) { + doExit = true; + } else { + doExit = false; + } + } else if (criteria.equals("if_not_success")) { + if (!this.success) { + doExit = true; + } else { + doExit = false; + } + } else if (criteria.equals("always")) { + doExit = true; + } else if (criteria.equals("never")) { + doExit = false; + } else { + throw new InvalidRuleException(String.format("verb='%s' unknown exit criteria '%s'", + verb, criteria)); + } + + if (doExit) { + statementResult = result; + } + + if (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 namespace, + List statement) { + ProcessResult statementResult = ProcessResult.STATEMENT_CONTINUE; + Token criteriaParam = getParameter(verb, statement, 1, namespace, + EnumSet.of(TokenType.STRING)); + String criteria = (criteriaParam.getStringValue()).toLowerCase(); + boolean doContinue; + + if (criteria.equals("if_success")) { + if (this.success) { + doContinue = true; + } else { + doContinue = false; + } + } else if (criteria.equals("if_not_success")) { + if (!this.success) { + doContinue = true; + } else { + doContinue = false; + } + } else if (criteria.equals("always")) { + doContinue = true; + } else if (criteria.equals("never")) { + doContinue = false; + } else { + throw new InvalidRuleException(String.format( + "verb='%s' unknown continue criteria '%s'", verb, criteria)); + } + + if (doContinue) { + statementResult = ProcessResult.BLOCK_CONTINUE; + } + + if (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-idp-mapping/src/main/java/org/opendaylight/aaa/idpmapping/StatementErrorException.java b/odl-aaa-moon/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-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-idp-mapping/src/main/java/org/opendaylight/aaa/idpmapping/Token.java b/odl-aaa-moon/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-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 + */ + +class Token { + + /* + * Regexp to identify a variable beginning with $ Supports array notation, + * e.g. $foo[bar] Optional delimiting braces may be used to separate + * variable from surrounding text. + * + * Examples: $foo ${foo} $foo[bar] ${foo[bar] where foo is the variable name + * and bar is the array index. + * + * Identifer is any alphabetic followed by alphanumeric or underscore + */ + private static final String VARIABLE_PAT = "(? namespace = null; + public TokenStorageType storageType = TokenStorageType.UNKNOWN; + public TokenType type = TokenType.UNKNOWN; + public String name = null; + public String index = null; + + Token(Object input, Map namespace) { + this.namespace = namespace; + if (input instanceof String) { + parseVariable((String) input); + if (this.storageType == TokenStorageType.CONSTANT) { + this.value = input; + this.type = classify(input); + } + } else { + this.storageType = TokenStorageType.CONSTANT; + this.value = input; + this.type = classify(input); + } + } + + @Override + public String toString() { + if (this.storageType == TokenStorageType.CONSTANT) { + return String.format("%s", this.value); + } else if (this.storageType == TokenStorageType.VARIABLE) { + if (this.index == null) { + return String.format("$%s", this.name); + } else { + return String.format("$%s[%s]", this.name, this.index); + } + } else { + return "UNKNOWN"; + } + } + + void parseVariable(String string) { + Matcher matcher = VARIABLE_ONLY_RE.matcher(string); + if (matcher.find()) { + String name = matcher.group(1); + String index = matcher.group(3); + + this.storageType = TokenStorageType.VARIABLE; + this.name = name; + this.index = index; + } else { + this.storageType = TokenStorageType.CONSTANT; + } + } + + public static TokenType classify(Object value) { + TokenType tokenType = TokenType.UNKNOWN; + // ordered by expected occurrence + if (value instanceof String) { + tokenType = TokenType.STRING; + } else if (value instanceof List) { + tokenType = TokenType.ARRAY; + } else if (value instanceof Map) { + tokenType = TokenType.MAP; + } else if (value instanceof Long) { + tokenType = TokenType.INTEGER; + } else if (value instanceof Boolean) { + tokenType = TokenType.BOOLEAN; + } else if (value == null) { + tokenType = TokenType.NULL; + } else if (value instanceof Double) { + tokenType = TokenType.REAL; + } else { + throw new InvalidRuleException(String.format( + "Type must be String, Long, Double, Boolean, List, Map, or null, not %s", + value.getClass().getSimpleName(), value)); + } + return tokenType; + } + + Object get() { + return get(null); + } + + Object get(Object index) { + Object base = null; + + if (this.storageType == TokenStorageType.CONSTANT) { + return this.value; + } + + if (this.namespace.containsKey(this.name)) { + base = this.namespace.get(this.name); + } else { + throw new UndefinedValueException(String.format("variable '%s' not defined", this.name)); + } + + if (index == null) { + index = this.index; + } + + if (index == null) { // scalar types + value = base; + } else { + if (base instanceof List) { + @SuppressWarnings("unchecked") + List list = (List) base; + Integer idx = null; + + if (index instanceof Long) { + idx = new Integer(((Long) index).intValue()); + } else if (index instanceof String) { + try { + idx = new Integer((String) index); + } catch (NumberFormatException e) { + throw new InvalidTypeException( + String.format( + "variable '%s' is an array indexed by '%s', however the index cannot be converted to an integer", + this.name, index, 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 map = (Map) base; + String idx = null; + if (index instanceof String) { + idx = (String) index; + } else { + throw new InvalidTypeException( + String.format( + "variable '%s' is a map indexed by '%s', however the index must be a string not %s", + this.name, index, index.getClass().getSimpleName())); + } + if (!map.containsKey(idx)) { + throw new UndefinedValueException( + String.format( + "variable '%s' is a map indexed by '%s', however the index does not exist", + this.name, index)); + } + value = map.get(idx); + } else { + throw new InvalidTypeException( + String.format( + "variable '%s' is indexed by '%s', variable must be an array or map, not %s", + this.name, index, base.getClass().getSimpleName())); + + } + } + this.type = classify(value); + return value; + } + + void set(Object value) { + set(value, null); + } + + void set(Object value, Object index) { + + if (this.storageType == TokenStorageType.CONSTANT) { + throw new InvalidTypeException("cannot assign to a constant"); + } + + if (index == null) { + index = this.index; + } + + if (index == null) { // scalar types + this.namespace.put(this.name, value); + } else { + Object base = null; + + if (this.namespace.containsKey(this.name)) { + base = this.namespace.get(this.name); + } else { + throw new UndefinedValueException(String.format("variable '%s' not defined", + this.name)); + } + + if (base instanceof List) { + @SuppressWarnings("unchecked") + List list = (List) base; + Integer idx = null; + + if (index instanceof Long) { + idx = new Integer(((Long) index).intValue()); + } else if (index instanceof String) { + try { + idx = new Integer((String) index); + } catch (NumberFormatException e) { + throw new InvalidTypeException( + String.format( + "variable '%s' is an array indexed by '%s', however the index cannot be converted to an integer", + this.name, index, 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 map = (Map) base; + String idx = null; + if (index instanceof String) { + idx = (String) index; + } else { + throw new InvalidTypeException( + String.format( + "variable '%s' is a map indexed by '%s', however the index must be a string not %s", + this.name, index, index.getClass().getSimpleName())); + } + if (!map.containsKey(idx)) { + throw new UndefinedValueException( + String.format( + "variable '%s' is a map indexed by '%s', however the index does not exist", + this.name, index)); + } + value = map.put(idx, value); + } else { + throw new InvalidTypeException( + String.format( + "variable '%s' is indexed by '%s', variable must be an array or map, not %s", + this.name, index, base.getClass().getSimpleName())); + + } + } + } + + public Object load() { + this.value = get(); + return this.value; + } + + public Object load(Object index) { + this.value = get(index); + return this.value; + } + + public String getStringValue() { + if (this.type == TokenType.STRING) { + return (String) this.value; + } else { + throw new InvalidTypeException(String.format("expected %s value but token type is %s", + TokenType.STRING, this.type)); + } + } + + public List getListValue() { + if (this.type == TokenType.ARRAY) { + @SuppressWarnings("unchecked") + List list = (List) this.value; + return list; + } else { + throw new InvalidTypeException(String.format("expected %s value but token type is %s", + TokenType.ARRAY, this.type)); + } + } + + public Map getMapValue() { + if (this.type == TokenType.MAP) { + @SuppressWarnings("unchecked") + Map map = (Map) this.value; + return map; + } else { + throw new InvalidTypeException(String.format("expected %s value but token type is %s", + TokenType.MAP, this.type)); + } + } + + public Long getLongValue() { + if (this.type == TokenType.INTEGER) { + return (Long) this.value; + } else { + throw new InvalidTypeException(String.format("expected %s value but token type is %s", + TokenType.INTEGER, this.type)); + } + } + + public Boolean getBooleanValue() { + if (this.type == TokenType.BOOLEAN) { + return (Boolean) this.value; + } else { + throw new InvalidTypeException(String.format("expected %s value but token type is %s", + TokenType.BOOLEAN, this.type)); + } + } + + public Double getDoubleValue() { + if (this.type == TokenType.REAL) { + return (Double) this.value; + } else { + throw new InvalidTypeException(String.format("expected %s value but token type is %s", + TokenType.REAL, this.type)); + } + } + + public Object getNullValue() { + if (this.type == TokenType.NULL) { + return this.value; + } else { + throw new InvalidTypeException(String.format("expected %s value but token type is %s", + TokenType.NULL, this.type)); + } + } + + public Object getObjectValue() { + return this.value; + } +} diff --git a/odl-aaa-moon/aaa-idp-mapping/src/main/java/org/opendaylight/aaa/idpmapping/UndefinedValueException.java b/odl-aaa-moon/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-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-idp-mapping/src/test/java/org/opendaylight/aaa/idpmapping/RuleProcessorTest.java b/odl-aaa-moon/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-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 list = new ArrayList(); + list.add("str1"); + list.add("str2"); + list.add("str3"); + assertEquals("str1/str2/str3", RuleProcessor.join(list, "/")); + } + + @Test + public void testSubstituteVariables() { + Map namespace = new HashMap() { + { + put("foo1", new HashMap() { + { + put("0", "1"); + } + }); + } + }; + String str = "foo1[0]"; + String subVariable = ruleProcess.substituteVariables(str, namespace); + assertNotNull(subVariable); + assertEquals(subVariable, str); + } + + @Test + public void testGetMapping() { + Map namespace = new HashMap() { + { + put("foo1", new HashMap() { + { + put("0", "1"); + } + }); + } + }; + final Map item = new HashMap() { + { + put("str", "val"); + } + }; + Map rules = new HashMap() { + { + put("mapping", item); + put("mapping_name", "mapping"); + } + }; + Map 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 mapping = new HashMap() { + { + put("Name", "Admin"); + } + }; + List> internalRules = new ArrayList>(); + Map internalRule = new HashMap() { + { + 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-idp-mapping/src/test/java/org/opendaylight/aaa/idpmapping/TokenTest.java b/odl-aaa-moon/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-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 namespace = new HashMap() { + { + put("foo1", new HashMap() { + { + 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()), 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-shiro-act/pom.xml b/odl-aaa-moon/aaa-shiro-act/pom.xml new file mode 100644 index 00000000..d8507c6d --- /dev/null +++ b/odl-aaa-moon/aaa-shiro-act/pom.xml @@ -0,0 +1,84 @@ + + + 4.0.0 + + org.opendaylight.aaa + aaa-parent + 0.3.1-Beryllium-SR1 + ../parent + + + aaa-shiro-act + bundle + + + + org.opendaylight.aaa + aaa-shiro + + + org.apache.felix + org.apache.felix.dependencymanager + + + + org.slf4j + slf4j-api + + + commons-beanutils + commons-beanutils + 1.8.3 + + + + + junit + junit + test + + + org.mockito + mockito-all + test + + + + + + + org.apache.felix + maven-bundle-plugin + ${bundle.plugin.version} + true + + + ${project.groupId}.${project.artifactId} + + ${project.basedir}/META-INF + + + + + + + org.apache.felix + maven-bundle-plugin + true + + + org.opendaylight.aaa.shiroact.Activator + + + + + org.apache.maven.plugins + maven-jar-plugin + + + + diff --git a/odl-aaa-moon/aaa-shiro-act/src/main/java/org/opendaylight/aaa/shiroact/Activator.java b/odl-aaa-moon/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-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 + * odl-aaa-shiro feature in your feature definition. + * + * Offers contextual DEBUG level clues concerning the activation of + * the aaa-shiro-act bundle. To enable the enhanced debugging issue + * the following line in the karaf shell: + * log:set debug org.opendaylight.aaa.shiroact.Activator + * + * @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-shiro-act/src/test/java/org/opendaylight/aaa/shiroact/ActivatorTest.java b/odl-aaa-moon/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-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-shiro/pom.xml b/odl-aaa-moon/aaa-shiro/pom.xml new file mode 100644 index 00000000..2f848215 --- /dev/null +++ b/odl-aaa-moon/aaa-shiro/pom.xml @@ -0,0 +1,156 @@ + + + 4.0.0 + + org.opendaylight.aaa + aaa-parent + 0.3.1-Beryllium-SR1 + ../parent + + + aaa-shiro + bundle + + + + + com.sun.jersey + jersey-client + provided + + + org.json + json + 20140107 + + + + org.apache.oltu.oauth2 + org.apache.oltu.oauth2.authzserver + provided + + + org.apache.oltu.oauth2 + org.apache.oltu.oauth2.common + provided + + + org.apache.oltu.oauth2 + org.apache.oltu.oauth2.resourceserver + provided + + + org.apache.felix + org.apache.felix.dependencymanager + + + org.opendaylight.aaa + aaa-authn-sts + + + org.opendaylight.aaa + aaa-authn-basic + + + org.apache.shiro + shiro-core + + + org.apache.shiro + shiro-web + + + org.slf4j + slf4j-api + + + commons-beanutils + commons-beanutils + 1.8.3 + + + javax.servlet + javax.servlet-api + + + com.google.guava + guava + + + + + junit + junit + test + + + org.mockito + mockito-all + test + + + + + + + org.apache.felix + maven-bundle-plugin + ${bundle.plugin.version} + true + + + ${project.groupId}.${project.artifactId} + + ${project.basedir}/META-INF + + + + + + + org.apache.felix + maven-bundle-plugin + true + + + + * + + /moon + org.opendaylight.aaa.shiro.Activator + + + + + org.apache.maven.plugins + maven-jar-plugin + + + org.codehaus.mojo + build-helper-maven-plugin + + + attach-artifacts + package + + attach-artifact + + + + + ${project.build.directory}/classes/shiro.ini + cfg + configuration + + + + + + + + + diff --git a/odl-aaa-moon/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/Activator.java b/odl-aaa-moon/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-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 + * odl-restconf-noauth feature. + * + * This class is also responsible for offering contextual DEBUG + * level clues concerning the activation of the aaa-shiro bundle. + * To enable these debug messages, issue the following command in the karaf + * shell: log:set debug org.opendaylight.aaa.shiro.Activator + * + * @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-shiro/src/main/java/org/opendaylight/aaa/shiro/ServiceProxy.java b/odl-aaa-moon/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-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 + * ServiceProxy.getInstance().setEnabled(true). AuthN and AuthZ are + * disabled by default in order to support workflows such as the feature + * odl-restconf-noauth. + * + * The AAA service is enabled through installing the odl-aaa-shiro + * feature. The org.opendaylight.aaa.shiroact.Activator() + * 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 getInstance() function. + * + * @author Ryan Goulding (ryandgoulding@gmail.com) + * @see resconf + * web,xml + * @see org.opendaylight.aaa.shiro.Activator + * @see org.opendaylight.aaa.shiro.filters.AAAFilter + */ +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 odl-restconf-noauth + */ + 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-shiro/src/main/java/org/opendaylight/aaa/shiro/accounting/Accounter.java b/odl-aaa-moon/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-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 Logger.output("message"). + * + * @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 + * Accounter.getInstance().output("message"). + */ + private Accounter() { + } + + /** + * Account for a particular message + * + * @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-shiro/src/main/java/org/opendaylight/aaa/shiro/authorization/DefaultRBACRules.java b/odl-aaa-moon/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-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: + * $KARAF_HOME/shiro.ini + * + * An important distinction to consider is that Shiro URL rules work to protect + * the system at the Web layer, and AuthzDomDataBroker 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 rbacRules = new HashSet(); + + /** + * 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 DefaultRBACRules. + */ + public final Collection 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 DefaultRBACRules. + // + // 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-shiro/src/main/java/org/opendaylight/aaa/shiro/authorization/RBACRule.java b/odl-aaa-moon/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-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: + * urlPattern=roles[atLeastOneCommaSeperatedRole] + * + * 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: + * RBACRule.createRBACRule() + * + * @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 roles = new HashSet(); + + /** + * 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 urlPattern or roles is null + * @throws IllegalArgumentException + * if urlPattern is an empty string or + * roles is an empty collection. + */ + private RBACRule(final String urlPattern, final Collection 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 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 RBACRule. + */ + public Collection 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 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 roles) throws NullPointerException, + IllegalArgumentException { + + Preconditions.checkNotNull(roles); + checkRolesCollectionSize(roles); + this.roles = roles; + } + + /** + * Generates a string representation of the RBACRule roles in + * shiro form. + * + * @return roles string representation in the form + * roles[roleOne,roleTwo] + */ + public String getRolesInShiroFormat() { + final String ROLES_STRING = "roles"; + return ROLES_STRING + Arrays.toString(roles.toArray()); + } + + /** + * Generates the string representation of the RBACRule in shiro + * form. For example: urlPattern=roles[admin,user] + */ + @Override + public String toString() { + return String.format("%s=%s", urlPattern, getRolesInShiroFormat()); + } +} diff --git a/odl-aaa-moon/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/filters/AAAFilter.java b/odl-aaa-moon/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/filters/AAAFilter.java new file mode 100644 index 00000000..b53588d8 --- /dev/null +++ b/odl-aaa-moon/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 default 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: + * + * log:set debug org.opendaylight.aaa.shiro.filters.AAAFilter + * + * @author Ryan Goulding (ryandgoulding@gmail.com) + * @see javax.servlet.Filter + * @see org.apache.shiro.web.servlet.ShiroFilter + */ +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 + * ServiceProxy.getInstance.getEnabled(). + * + * @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-shiro/src/main/java/org/opendaylight/aaa/shiro/filters/MoonOAuthFilter.java b/odl-aaa-moon/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/filters/MoonOAuthFilter.java new file mode 100644 index 00000000..06038c54 --- /dev/null +++ b/odl-aaa-moon/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/filters/MoonOAuthFilter.java @@ -0,0 +1,187 @@ +/* + * 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.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; + + +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 { + /** + * Here, we will call three functions depending on whether user wants to: + * create Token + * refresh token + * delete token + */ + 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)) { + //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); + } + 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 + } + } + +} \ No newline at end of file diff --git a/odl-aaa-moon/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/filters/ODLHttpAuthenticationFilter.java b/odl-aaa-moon/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-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 BasicHttpAuthenticationFilter to include ability to + * authenticate OAuth2 tokens, which is needed for backwards compatibility with + * TokenAuthFilter. + * + * This behavior is enabled by default for backwards compatibility. To disable + * OAuth2 functionality, just comment out the following line from the + * etc/shiro.ini file: + * authcBasic = org.opendaylight.aaa.shiro.filters.ODLHttpAuthenticationFilter + * 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-shiro/src/main/java/org/opendaylight/aaa/shiro/moon/MoonPrincipal.java b/odl-aaa-moon/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/moon/MoonPrincipal.java new file mode 100644 index 00000000..a95b4e7f --- /dev/null +++ b/odl-aaa-moon/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/moon/MoonPrincipal.java @@ -0,0 +1,155 @@ +/* + * 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; + +public class MoonPrincipal { + + private final String username; + private final String domain; + private final String userId; + private final Set roles; + private final String token; + + + public MoonPrincipal(String username, String domain, String userId, Set 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 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 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 roles; + + public MoonClaim(String clientId, String userId, String user, String domain, Set roles) { + this.clientId = clientId; + this.userId = userId; + this.user = user; + this.domain = domain; + this.roles = ImmutableSet. 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 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 getRoles() { + return roles; + } + + public void setRoles(ImmutableSet 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-shiro/src/main/java/org/opendaylight/aaa/shiro/moon/MoonTokenEndpoint.java b/odl-aaa-moon/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-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-shiro/src/main/resources/WEB-INF/web.xml b/odl-aaa-moon/aaa-shiro/src/main/resources/WEB-INF/web.xml new file mode 100644 index 00000000..63288c23 --- /dev/null +++ b/odl-aaa-moon/aaa-shiro/src/main/resources/WEB-INF/web.xml @@ -0,0 +1,48 @@ + + + + + MOON + org.opendaylight.aaa.shiro.moon.MoonTokenEndpoint + 1 + + + + MOON + /token + + + MOON + /revoke + + + MOON + /validate + + + MOON + /* + + + + + shiroEnvironmentClass + org.opendaylight.aaa.shiro.web.env.KarafIniWebEnvironment + + + + org.apache.shiro.web.env.EnvironmentLoaderListener + + + + ShiroFilter + org.opendaylight.aaa.shiro.filters.AAAFilter + + + + ShiroFilter + /* + + \ No newline at end of file diff --git a/odl-aaa-moon/aaa-shiro/src/main/resources/shiro.ini b/odl-aaa-moon/aaa-shiro/src/main/resources/shiro.ini new file mode 100644 index 00000000..d84f9fa0 --- /dev/null +++ b/odl-aaa-moon/aaa-shiro/src/main/resources/shiro.ini @@ -0,0 +1,95 @@ +# +# 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://: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://: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 +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 = $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 + + + +[urls] +############################################################################### +# url authorization section # +# # +# This section is dedicated to defining url-based authorization according to: # +# http://shiro.apache.org/web.html # +############################################################################### +#Filtering REST requests with AAAFilter +/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-shiro/src/test/java/org/opendaylight/aaa/shiro/ServiceProxyTest.java b/odl-aaa-moon/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-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-shiro/src/test/java/org/opendaylight/aaa/shiro/authorization/DefaultRBACRulesTest.java b/odl-aaa-moon/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-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 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-shiro/src/test/java/org/opendaylight/aaa/shiro/authorization/RBACRuleTest.java b/odl-aaa-moon/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-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 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 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())); + } + + @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-shiro/src/test/java/org/opendaylight/aaa/shiro/realm/ODLJndiLdapRealmTest.java b/odl-aaa-moon/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-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 { + + /** + * state variable + */ + boolean first = true; + + /** + * returned the first time next() or + * nextElement() 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 searchResult 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 searchResult 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 collection = new Vector(); + + public TestPrincipalCollection(String element) { + collection.add(element); + } + + @Override + public Iterator iterator() { + return collection.iterator(); + } + + @Override + public List asList() { + return collection; + } + + @Override + public Set asSet() { + HashSet set = new HashSet(); + set.addAll(collection); + return set; + } + + @Override + public Collection byType(Class arg0) { + return null; + } + + @Override + public Collection fromRealm(String arg0) { + return collection; + } + + @Override + public Object getPrimaryPrincipal() { + return collection.firstElement(); + } + + @Override + public Set getRealmNames() { + return null; + } + + @Override + public boolean isEmpty() { + return collection.isEmpty(); + } + + @Override + public T oneByType(Class 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 roleNames = new HashSet(); + 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 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-shiro/src/test/java/org/opendaylight/aaa/shiro/realm/TokenAuthRealmTest.java b/odl-aaa-moon/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-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> expectedHeaders = new HashMap>(); + expectedHeaders.put("Authorization", Lists.newArrayList(authHeader)); + final Map> actualHeaders = formHeadersWithToken(authHeader); + List 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> expectedHeaders = new HashMap>(); + expectedHeaders.put("Authorization", Lists.newArrayList(authHeader)); + final Map> actualHeaders = formHeaders(username, password, domain); + List 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-shiro/src/test/java/org/opendaylight/aaa/shiro/web/env/KarafIniWebEnvironmentTest.java b/odl-aaa-moon/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-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/artifacts/pom.xml b/odl-aaa-moon/artifacts/pom.xml new file mode 100644 index 00000000..8efad137 --- /dev/null +++ b/odl-aaa-moon/artifacts/pom.xml @@ -0,0 +1,231 @@ + + + + + 4.0.0 + + + org.opendaylight.odlparent + odlparent-lite + 1.6.1-Beryllium-SR1 + + + + org.opendaylight.aaa + aaa-artifacts + 0.3.1-Beryllium-SR1 + pom + + + + + ${project.groupId} + aaa-authn + ${project.version} + + + ${project.groupId} + aaa-authn + ${project.version} + cfg + config + + + ${project.groupId} + aaa-authn-api + ${project.version} + + + ${project.groupId} + aaa-authn-basic + ${project.version} + + + ${project.groupId} + aaa-authn-federation + ${project.version} + + + ${project.groupId} + aaa-authn-federation + ${project.version} + cfg + config + + + ${project.groupId} + aaa-authn-keystone + ${project.version} + + + + ${project.groupId} + aaa-authn-mdsal-api + ${project.version} + + + ${project.groupId} + aaa-authn-mdsal-store-impl + ${project.version} + + + ${project.groupId} + aaa-authn-mdsal-config + ${project.version} + xml + config + + + ${project.groupId} + aaa-shiro + ${project.version} + + + ${project.groupId} + aaa-shiro-act + ${project.version} + + + ${project.groupId} + aaa-authn-sssd + ${project.version} + + + ${project.groupId} + aaa-authn-store + ${project.version} + + + ${project.groupId} + aaa-authn-store + ${project.version} + cfg + config + + + ${project.groupId} + aaa-authn-sts + ${project.version} + + + + ${project.groupId} + aaa-authz-model + ${project.version} + + + ${project.groupId} + aaa-authz-service + ${project.version} + + + ${project.groupId} + authz-service-config + ${project.version} + xml + config + + + ${project.groupId} + authz-restconf-config + ${project.version} + xml + config + + + + ${project.groupId} + aaa-credential-store-api + ${project.version} + + + ${project.groupId} + aaa-idmlight + ${project.version} + + + ${project.groupId} + aaa-idmlight + ${project.version} + xml + config + + + ${project.groupId} + aaa-authn-idpmapping + ${project.version} + + + + ${project.groupId} + features-aaa-api + ${project.version} + features + xml + + + ${project.groupId} + features-aaa-authn + ${project.version} + features + xml + + + ${project.groupId} + features-aaa-authz + ${project.version} + features + xml + + + ${project.groupId} + aaa-h2-store + ${project.version} + + + ${project.groupId} + aaa-h2-store + ${project.version} + config + xml + + + ${project.groupId} + features-aaa-shiro + ${project.version} + features + xml + + + ${project.groupId} + features-aaa + ${project.version} + features + xml + + + + + + http://nexus.opendaylight.org/content + + + + + + opendaylight-release + ${nexusproxy}/repositories/opendaylight.release/ + + + + opendaylight-snapshot + ${nexusproxy}/repositories/opendaylight.snapshot/ + + + diff --git a/odl-aaa-moon/commons/docs/AuthNusecases.vsd b/odl-aaa-moon/commons/docs/AuthNusecases.vsd new file mode 100644 index 0000000000000000000000000000000000000000..ddd59fb3f05301f2a2f652757404ccb089e3fd4e GIT binary patch literal 206336 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�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%0D6T_>Ysn;e#FOGgoZPps=plmVa~;Yi0) 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 Jtx5$J{(mDnHedh% literal 0 HcmV?d00001 diff --git a/odl-aaa-moon/commons/docs/federated_authn1.png b/odl-aaa-moon/commons/docs/federated_authn1.png new file mode 100644 index 0000000000000000000000000000000000000000..199f6f4d691a4227248d5f9a4d946a51ffeb6237 GIT binary patch literal 36542 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 (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 + + AuthType Kerberos + AuthName "Kerberos Login" + KrbMethodNegotiate On + KrbMethodK5Passwd on + KrbAuthRealms EXAMPLE.COM + Krb5KeyTab /etc/krb5.keytab + require valid-user + + + + + + 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 + + +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 + + + + + + + + + + + + + + + + + + + + + + 300000 + 2 + false + 8443 + 20000 + 5000 + + + + + + + + 127.0.0.1 + 8383 + 300000 + 2 + false + 8445 + federationConn + 20000 + 5000 + + + + + + + + + + + + + + karaf + karaf + + + org.apache.karaf.jaas.boot.principal.RolePrincipal + + + + + + + + + + default + karaf + + + org.apache.karaf.jaas.boot.principal.RolePrincipal + + + + + + + + + + +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=&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 ' http://odl.brcd-sssd-tb.com:8181/restconf/streams/ + diff --git a/odl-aaa-moon/commons/federation/idp_mapping_rules.json.example b/odl-aaa-moon/commons/federation/idp_mapping_rules.json.example new file mode 100644 index 00000000..98bacb0a --- /dev/null +++ b/odl-aaa-moon/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/commons/federation/jetty.xml.example b/odl-aaa-moon/commons/federation/jetty.xml.example new file mode 100644 index 00000000..c4cb2a7d --- /dev/null +++ b/odl-aaa-moon/commons/federation/jetty.xml.example @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + + + + + + + 300000 + 2 + false + 8443 + 20000 + 5000 + + + + + + + + 127.0.0.1 + 8383 + 300000 + 2 + false + 8445 + federationConn + 20000 + 5000 + + + + + + + + + + + + + + karaf + karaf + + + org.apache.karaf.jaas.boot.principal.RolePrincipal + + + + + + + + + + default + karaf + + + org.apache.karaf.jaas.boot.principal.RolePrincipal + + + + + + + + diff --git a/odl-aaa-moon/commons/federation/my_app.conf.example b/odl-aaa-moon/commons/federation/my_app.conf.example new file mode 100644 index 00000000..71c8ad87 --- /dev/null +++ b/odl-aaa-moon/commons/federation/my_app.conf.example @@ -0,0 +1,31 @@ +LoadModule lookup_identity_module modules/mod_lookup_identity.so + + + AuthType Kerberos + AuthName "Kerberos Login" + KrbMethodNegotiate On + KrbMethodK5Passwd on + KrbAuthRealms EXAMPLE.COM + Krb5KeyTab /etc/krb5.keytab + require valid-user + + + + + + 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 + + +ProxyPass / http://localhost:8383/ +ProxyPassReverse / http://localhost:8383/ diff --git a/odl-aaa-moon/commons/postman_examples/AAA_AuthZ_MDSAL.json.postman_collection b/odl-aaa-moon/commons/postman_examples/AAA_AuthZ_MDSAL.json.postman_collection new file mode 100644 index 00000000..15193a70 --- /dev/null +++ b/odl-aaa-moon/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/distribution-karaf/pom.xml b/odl-aaa-moon/distribution-karaf/pom.xml new file mode 100644 index 00000000..dc65d84f --- /dev/null +++ b/odl-aaa-moon/distribution-karaf/pom.xml @@ -0,0 +1,274 @@ + + + 4.0.0 + + org.opendaylight.aaa + aaa-parent + 0.3.1-Beryllium-SR1 + ../parent + + + distribution-karaf + pom + + 3.0 + + + + + + org.apache.karaf.features + framework + ${karaf.version} + kar + + + org.apache.karaf.features + standard + ${karaf.version} + features + xml + runtime + + + + + org.opendaylight.controller + karaf.branding + ${karaf.branding.version} + compile + + + + + org.opendaylight.controller + opendaylight-karaf-resources + ${karaf.resources.version} + + + + + org.opendaylight.aaa + features-aaa-api + features + ${project.version} + xml + runtime + + + org.opendaylight.aaa + features-aaa + features + ${project.version} + xml + runtime + + + org.opendaylight.aaa + features-aaa-authz + features + ${project.version} + xml + runtime + + + org.opendaylight.aaa + features-aaa-shiro + features + ${project.version} + xml + runtime + + + + + + + + org.eclipse.m2e + lifecycle-mapping + 1.0.0 + + + + + + org.apache.felix + maven-bundle-plugin + [0,) + + cleanVersions + + + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + [0,) + + copy + unpack + + + + + + + + + org.apache.karaf.tooling + karaf-maven-plugin + [0,) + + commands-generate-help + + + + + + + + + org.fusesource.scalate + maven-scalate-plugin + [0,) + + sitegen + + + + + + + + + org.apache.servicemix.tooling + depends-maven-plugin + [0,) + + generate-depends-file + + + + + + + + + + + + + + + org.apache.karaf.tooling + karaf-maven-plugin + true + + + standard + + + + + + + process-resources + + install-kars + + process-resources + + + package + + instance-create-archive + + + + + + org.apache.maven.plugins + maven-dependency-plugin + 2.6 + + + copy + + copy + + generate-resources + + + + org.opendaylight.controller + karaf.branding + ${karaf.branding.version} + target/assembly/lib + karaf.branding-${karaf.branding.version}.jar + + + + + + unpack-karaf-resources + + unpack-dependencies + + prepare-package + + ${project.build.directory}/assembly + org.opendaylight.controller + opendaylight-karaf-resources + META-INF\/** + true + false + + + + + + org.apache.maven.plugins + maven-antrun-plugin + + + prepare-package + + run + + + + + + + + + + + + + + + + + + + + + + + scm:git:ssh://git.opendaylight.org:29418/aaa.git + scm:git:ssh://git.opendaylight.org:29418/aaa.git + HEAD + https://git.opendaylight.org/gerrit/gitweb?p=aaa.git;a=summary + + diff --git a/odl-aaa-moon/features/api/pom.xml b/odl-aaa-moon/features/api/pom.xml new file mode 100644 index 00000000..b64242ae --- /dev/null +++ b/odl-aaa-moon/features/api/pom.xml @@ -0,0 +1,91 @@ + + + + 4.0.0 + + org.opendaylight.odlparent + features-parent + 1.6.1-Beryllium-SR1 + + + + org.opendaylight.aaa + features-aaa-api + 0.3.1-Beryllium-SR1 + jar + + + 0.8.1-Beryllium-SR1 + 2.0.1-Beryllium-SR1 + + + + + + + org.opendaylight.aaa + aaa-artifacts + ${project.version} + import + pom + + + + + org.opendaylight.yangtools + yangtools-artifacts + ${yangtools.version} + import + pom + + + + + + + org.slf4j + slf4j-api + + + org.slf4j + slf4j-simple + + + com.sun.jersey + jersey-server + provided + + + org.opendaylight.aaa + aaa-authn-api + + + org.opendaylight.aaa + aaa-credential-store-api + + + org.opendaylight.yangtools + features-yangtools + features + xml + + + org.opendaylight.mdsal + features-mdsal + 2.0.1-Beryllium-SR1 + features + xml + + + + + scm:git:ssh://git.opendaylight.org:29418/aaa.git + scm:git:ssh://git.opendaylight.org:29418/aaa.git + HEAD + https://git.opendaylight.org/gerrit/gitweb?p=aaa.git;a=summary + + diff --git a/odl-aaa-moon/features/api/src/main/features/features.xml b/odl-aaa-moon/features/api/src/main/features/features.xml new file mode 100644 index 00000000..c526e174 --- /dev/null +++ b/odl-aaa-moon/features/api/src/main/features/features.xml @@ -0,0 +1,21 @@ + + + + + mvn:org.opendaylight.yangtools/features-yangtools/{{VERSION}}/xml/features + mvn:org.opendaylight.mdsal/features-mdsal/{{VERSION}}/xml/features + + mvn:com.sun.jersey/jersey-server/{{VERSION}} + mvn:com.sun.jersey/jersey-core/{{VERSION}} + mvn:org.opendaylight.aaa/aaa-authn-api/{{VERSION}} + mvn:org.opendaylight.aaa/aaa-credential-store-api/{{VERSION}} + odl-yangtools-common + odl-mdsal-binding-base + + diff --git a/odl-aaa-moon/features/authn/pom.xml b/odl-aaa-moon/features/authn/pom.xml new file mode 100644 index 00000000..d7d0def8 --- /dev/null +++ b/odl-aaa-moon/features/authn/pom.xml @@ -0,0 +1,304 @@ + + + + 4.0.0 + + org.opendaylight.odlparent + features-parent + 1.6.1-Beryllium-SR1 + + + + org.opendaylight.aaa + features-aaa + 0.3.1-Beryllium-SR1 + jar + + + 0.4.1-Beryllium-SR1 + 2.0.1-Beryllium-SR1 + 1.3.1-Beryllium-SR1 + 0.8.1-Beryllium-SR1 + 0.3.1-Beryllium-SR1 + + + + + + + org.opendaylight.aaa + aaa-parent + ${project.version} + import + pom + + + + + + + + org.opendaylight.aaa + aaa-shiro + ${shiro.version} + + + org.opendaylight.aaa + aaa-shiro-act + ${shiro.version} + + + org.apache.shiro + shiro-core + + + org.apache.shiro + shiro-web + + + + com.sun.jersey + jersey-servlet + + + com.sun.jersey + jersey-core + + + com.sun.jersey + jersey-server + + + + com.sun.jersey + jersey-client + + + com.sun.jersey + jersey-json + + + org.apache.commons + commons-lang3 + + + org.apache.felix + org.apache.felix.dependencymanager + + + org.apache.felix + org.apache.felix.metatype + + + net.sf.ehcache + ehcache + + + org.apache.geronimo.specs + geronimo-jta_1.1_spec + + + org.apache.oltu.oauth2 + org.apache.oltu.oauth2.common + + + org.apache.oltu.oauth2 + org.apache.oltu.oauth2.authzserver + + + org.apache.oltu.oauth2 + org.apache.oltu.oauth2.resourceserver + + + commons-codec + commons-codec + + + org.json + json + + + org.glassfish + javax.json + + + com.fasterxml.jackson.core + jackson-core + + + com.fasterxml.jackson.core + jackson-annotations + + + com.fasterxml.jackson.core + jackson-databind + + + com.fasterxml.jackson.datatype + jackson-datatype-json-org + + + com.fasterxml.jackson.jaxrs + jackson-jaxrs-base + + + com.fasterxml.jackson.jaxrs + jackson-jaxrs-json-provider + + + com.fasterxml.jackson.module + jackson-module-jaxb-annotations + + + com.google.guava + guava + + + com.h2database + h2 + + + org.opendaylight.aaa + features-aaa-api + features + xml + + + org.opendaylight.aaa + aaa-authn + + + org.opendaylight.aaa + aaa-authn-sts + + + org.opendaylight.aaa + aaa-authn-store + + + org.opendaylight.aaa + aaa-authn-basic + + + org.opendaylight.aaa + aaa-idmlight + + + org.opendaylight.aaa + aaa-idmlight + xml + config + + + org.opendaylight.aaa + aaa-idmlight + ${project.version} + py + config + + + org.opendaylight.aaa + aaa-authn-federation + + + org.opendaylight.aaa + aaa-authn-mdsal-config + xml + config + + + org.opendaylight.aaa + aaa-authn + cfg + config + + + org.opendaylight.aaa + aaa-authn-store + cfg + config + + + org.opendaylight.aaa + aaa-authn-federation + cfg + config + + + org.opendaylight.aaa + aaa-h2-store + + + org.opendaylight.aaa + aaa-h2-store + xml + config + + + + + org.osgi + org.osgi.enterprise + 4.2.0 + + + + + + org.opendaylight.aaa + aaa-authn-mdsal-store-impl + + + org.opendaylight.aaa + aaa-authn-mdsal-api + + + org.opendaylight.yangtools + features-yangtools + features + xml + + + org.opendaylight.controller + features-mdsal + features + xml + + + org.opendaylight.controller + features-config + features + xml + + + org.opendaylight.controller + sal-common-impl + + + + + org.opendaylight.aaa + aaa-authn-sssd + + + + org.opendaylight.aaa + aaa-authn-idpmapping + + + + org.opendaylight.aaa + aaa-authn-keystone + + + + scm:git:ssh://git.opendaylight.org:29418/aaa.git + scm:git:ssh://git.opendaylight.org:29418/aaa.git + HEAD + https://git.opendaylight.org/gerrit/gitweb?p=aaa.git;a=summary + + diff --git a/odl-aaa-moon/features/authn/src/main/features/features.xml b/odl-aaa-moon/features/authn/src/main/features/features.xml new file mode 100644 index 00000000..2c48d2c1 --- /dev/null +++ b/odl-aaa-moon/features/authn/src/main/features/features.xml @@ -0,0 +1,247 @@ + + + + + mvn:org.opendaylight.aaa/features-aaa-api/{{VERSION}}/xml/features + mvn:org.opendaylight.yangtools/features-yangtools/{{VERSION}}/xml/features + mvn:org.opendaylight.controller/features-config/{{VERSION}}/xml/features + mvn:org.opendaylight.mdsal/features-mdsal/{{VERSION}}/xml/features + mvn:org.opendaylight.controller/features-mdsal/{{VERSION}}/xml/features + + + odl-aaa-api + + + odl-yangtools-common + odl-mdsal-binding-base + odl-mdsal-broker + odl-config-core + + + war + mvn:com.sun.jersey/jersey-servlet/{{VERSION}} + mvn:com.sun.jersey/jersey-core/{{VERSION}} + mvn:com.sun.jersey/jersey-server/{{VERSION}} + + + mvn:org.apache.felix/org.apache.felix.dependencymanager/{{VERSION}} + mvn:org.apache.felix/org.apache.felix.metatype/{{VERSION}} + + + mvn:net.sf.ehcache/ehcache/{{VERSION}} + mvn:org.apache.geronimo.specs/geronimo-jta_1.1_spec/{{VERSION}} + + + mvn:org.apache.oltu.oauth2/org.apache.oltu.oauth2.common/{{VERSION}} + mvn:org.apache.oltu.oauth2/org.apache.oltu.oauth2.authzserver/{{VERSION}} + mvn:org.apache.oltu.oauth2/org.apache.oltu.oauth2.resourceserver/{{VERSION}} + mvn:commons-codec/commons-codec/{{VERSION}} + wrap:mvn:org.json/json/{{VERSION}} + + + wrap:mvn:org.apache.commons/commons-lang3/{{VERSION}} + + + mvn:org.opendaylight.aaa/aaa-shiro/{{VERSION}} + mvn:org.opendaylight.aaa/aaa-shiro-act/{{VERSION}} + mvn:org.apache.shiro/shiro-core/{{VERSION}} + mvn:org.apache.shiro/shiro-web/{{VERSION}} + mvn:org.opendaylight.aaa/aaa-authn/{{VERSION}} + mvn:org.opendaylight.aaa/aaa-authn-sts/{{VERSION}} + mvn:org.opendaylight.aaa/aaa-authn-store/{{VERSION}} + mvn:org.opendaylight.aaa/aaa-authn-basic/{{VERSION}} + mvn:com.google.guava/guava/{{VERSION}} + + + mvn:org.osgi/org.osgi.enterprise/4.2.0 + wrap:mvn:com.h2database/h2/{{VERSION}} + mvn:org.opendaylight.aaa/aaa-h2-store/{{VERSION}} + mvn:org.opendaylight.aaa/aaa-h2-store/{{VERSION}}/xml/config + + + mvn:org.opendaylight.aaa/aaa-idmlight/{{VERSION}} + mvn:org.opendaylight.aaa/aaa-idmlight/{{VERSION}}/xml/config + mvn:org.opendaylight.aaa/aaa-idmlight/{{VERSION}}/py/config + + mvn:com.fasterxml.jackson.core/jackson-core/{{VERSION}} + mvn:com.fasterxml.jackson.core/jackson-annotations/{{VERSION}} + mvn:com.fasterxml.jackson.core/jackson-databind/{{VERSION}} + mvn:com.fasterxml.jackson.datatype/jackson-datatype-json-org/{{VERSION}} + mvn:com.fasterxml.jackson.jaxrs/jackson-jaxrs-base/{{VERSION}} + mvn:com.fasterxml.jackson.jaxrs/jackson-jaxrs-json-provider/{{VERSION}} + mvn:com.fasterxml.jackson.module/jackson-module-jaxb-annotations/{{VERSION}} + + + mvn:org.opendaylight.aaa/aaa-authn-federation/{{VERSION}} + mvn:org.opendaylight.aaa/aaa-authn-idpmapping/{{VERSION}} + mvn:org.glassfish/javax.json/{{VERSION}} + + mvn:org.opendaylight.aaa/aaa-authn/{{VERSION}}/cfg/config + mvn:org.opendaylight.aaa/aaa-authn-store/{{VERSION}}/cfg/config + mvn:org.opendaylight.aaa/aaa-authn-federation/{{VERSION}}/cfg/config + + + + odl-aaa-api + + + odl-yangtools-common + odl-mdsal-binding-base + odl-mdsal-broker + odl-config-core + + + war + mvn:com.sun.jersey/jersey-servlet/{{VERSION}} + mvn:com.sun.jersey/jersey-core/{{VERSION}} + mvn:com.sun.jersey/jersey-server/{{VERSION}} + mvn:com.sun.jersey/jersey-client/${jersey.version} + + + mvn:org.apache.felix/org.apache.felix.dependencymanager/{{VERSION}} + mvn:org.apache.felix/org.apache.felix.metatype/{{VERSION}} + + + mvn:net.sf.ehcache/ehcache/{{VERSION}} + mvn:org.apache.geronimo.specs/geronimo-jta_1.1_spec/{{VERSION}} + + + mvn:org.opendaylight.aaa/aaa-shiro/{{VERSION}} + mvn:org.opendaylight.aaa/aaa-shiro-act/{{VERSION}} + mvn:org.apache.shiro/shiro-core/{{VERSION}} + mvn:org.apache.shiro/shiro-web/{{VERSION}} + mvn:org.apache.oltu.oauth2/org.apache.oltu.oauth2.common/{{VERSION}} + mvn:org.apache.oltu.oauth2/org.apache.oltu.oauth2.authzserver/{{VERSION}} + mvn:org.apache.oltu.oauth2/org.apache.oltu.oauth2.resourceserver/{{VERSION}} + mvn:commons-codec/commons-codec/{{VERSION}} + wrap:mvn:org.json/json/{{VERSION}} + + + wrap:mvn:org.apache.commons/commons-lang3/{{VERSION}} + + + mvn:org.opendaylight.aaa/aaa-authn/{{VERSION}} + mvn:org.opendaylight.aaa/aaa-authn-sts/{{VERSION}} + mvn:org.opendaylight.aaa/aaa-authn-store/{{VERSION}} + mvn:org.opendaylight.aaa/aaa-authn-basic/{{VERSION}} + mvn:com.google.guava/guava/{{VERSION}} + + + mvn:org.osgi/org.osgi.enterprise/4.2.0 + wrap:mvn:com.h2database/h2/{{VERSION}} + mvn:org.opendaylight.aaa/aaa-h2-store/{{VERSION}} + mvn:org.opendaylight.aaa/aaa-h2-store/{{VERSION}}/xml/config + + + mvn:org.opendaylight.aaa/aaa-idmlight/{{VERSION}} + mvn:org.opendaylight.aaa/aaa-idmlight/{{VERSION}}/xml/config + mvn:org.opendaylight.aaa/aaa-idmlight/{{VERSION}}/py/config + + mvn:com.fasterxml.jackson.core/jackson-core/{{VERSION}} + mvn:com.fasterxml.jackson.core/jackson-annotations/{{VERSION}} + mvn:com.fasterxml.jackson.core/jackson-databind/{{VERSION}} + mvn:com.fasterxml.jackson.datatype/jackson-datatype-json-org/{{VERSION}} + mvn:com.fasterxml.jackson.jaxrs/jackson-jaxrs-base/{{VERSION}} + mvn:com.fasterxml.jackson.jaxrs/jackson-jaxrs-json-provider/{{VERSION}} + mvn:com.fasterxml.jackson.module/jackson-module-jaxb-annotations/{{VERSION}} + + + mvn:org.opendaylight.aaa/aaa-authn-federation/{{VERSION}} + mvn:org.opendaylight.aaa/aaa-authn-idpmapping/{{VERSION}} + mvn:org.glassfish/javax.json/{{VERSION}} + + mvn:org.opendaylight.aaa/aaa-authn/{{VERSION}}/cfg/config + mvn:org.opendaylight.aaa/aaa-authn-store/{{VERSION}}/cfg/config + mvn:org.opendaylight.aaa/aaa-authn-federation/{{VERSION}}/cfg/config + + + + + + odl-yangtools-common + odl-mdsal-binding-base + odl-mdsal-broker + odl-config-core + + + + mvn:org.apache.felix/org.apache.felix.dependencymanager/{{VERSION}} + mvn:org.apache.felix/org.apache.felix.metatype/{{VERSION}} + + + mvn:org.apache.oltu.oauth2/org.apache.oltu.oauth2.common/{{VERSION}} + mvn:org.apache.oltu.oauth2/org.apache.oltu.oauth2.authzserver/{{VERSION}} + mvn:org.apache.oltu.oauth2/org.apache.oltu.oauth2.resourceserver/{{VERSION}} + mvn:commons-codec/commons-codec/1.8 + wrap:mvn:org.json/json/{{VERSION}} + + + mvn:org.opendaylight.aaa/aaa-shiro/{{VERSION}} + mvn:org.opendaylight.aaa/aaa-shiro-act/{{VERSION}} + mvn:org.apache.shiro/shiro-core/{{VERSION}} + mvn:org.apache.shiro/shiro-web/{{VERSION}} + mvn:org.opendaylight.aaa/aaa-authn-api/{{VERSION}} + mvn:org.opendaylight.aaa/aaa-authn/{{VERSION}} + mvn:org.opendaylight.aaa/aaa-authn-sts/{{VERSION}} + mvn:org.opendaylight.aaa/aaa-authn-mdsal-api/{{VERSION}} + mvn:org.opendaylight.aaa/aaa-authn-mdsal-store-impl/{{VERSION}} + mvn:org.opendaylight.aaa/aaa-authn-basic/{{VERSION}} + mvn:com.google.guava/guava/{{VERSION}} + + + mvn:org.opendaylight.aaa/aaa-idmlight/{{VERSION}} + mvn:org.opendaylight.aaa/aaa-idmlight/{{VERSION}}/xml/config + mvn:com.fasterxml.jackson.core/jackson-core/{{VERSION}} + mvn:com.fasterxml.jackson.core/jackson-annotations/{{VERSION}} + mvn:com.fasterxml.jackson.core/jackson-databind/{{VERSION}} + mvn:com.fasterxml.jackson.datatype/jackson-datatype-json-org/{{VERSION}} + mvn:com.fasterxml.jackson.jaxrs/jackson-jaxrs-base/{{VERSION}} + mvn:com.fasterxml.jackson.jaxrs/jackson-jaxrs-json-provider/{{VERSION}} + mvn:com.fasterxml.jackson.module/jackson-module-jaxb-annotations/{{VERSION}} + wrap:mvn:com.h2database/h2/{{VERSION}} + + + mvn:org.opendaylight.aaa/aaa-authn-federation/{{VERSION}} + mvn:org.opendaylight.aaa/aaa-authn-idpmapping/{{VERSION}} + mvn:org.glassfish/javax.json/1.0.4 + + + war + mvn:com.sun.jersey/jersey-servlet/{{VERSION}} + mvn:com.sun.jersey/jersey-core/{{VERSION}} + mvn:com.sun.jersey/jersey-server/{{VERSION}} + + mvn:org.opendaylight.aaa/aaa-authn-mdsal-config/{{VERSION}}/xml/config + mvn:org.opendaylight.aaa/aaa-authn/{{VERSION}}/cfg/config + mvn:org.opendaylight.aaa/aaa-authn-federation/{{VERSION}}/cfg/config + + + + + odl-aaa-authn + mvn:org.apache.httpcomponents/httpclient-osgi/{{VERSION}} + mvn:org.apache.httpcomponents/httpcore-osgi/{{VERSION}} + mvn:org.opendaylight.aaa/aaa-authn-keystone/{{VERSION}} + + + + odl-aaa-authn + mvn:org.opendaylight.aaa/aaa-authn-sssd/{{VERSION}} + + + + odl-aaa-authn-no-cluster + mvn:org.opendaylight.aaa/aaa-authn-sssd/{{VERSION}} + + diff --git a/odl-aaa-moon/features/authz/pom.xml b/odl-aaa-moon/features/authz/pom.xml new file mode 100644 index 00000000..41808378 --- /dev/null +++ b/odl-aaa-moon/features/authz/pom.xml @@ -0,0 +1,101 @@ + + + + 4.0.0 + + org.opendaylight.odlparent + features-parent + 1.6.1-Beryllium-SR1 + + + + org.opendaylight.aaa + features-aaa-authz + 0.3.1-Beryllium-SR1 + jar + + + 0.4.1-Beryllium-SR1 + 2.0.1-Beryllium-SR1 + 1.3.1-Beryllium-SR1 + 0.8.1-Beryllium-SR1 + + + + + + + org.opendaylight.aaa + aaa-parent + ${project.version} + import + pom + + + + + + + org.opendaylight.aaa + features-aaa-api + features + xml + + + + org.opendaylight.yangtools + features-yangtools + features + xml + + + org.opendaylight.mdsal + features-mdsal + features + ${mdsal.version} + xml + + + org.opendaylight.controller + features-config + features + xml + + + org.opendaylight.controller + features-mdsal + features + xml + + + org.opendaylight.aaa + authz-restconf-config + xml + config + + + org.opendaylight.aaa + aaa-authz-model + + + org.opendaylight.aaa + aaa-authz-service + + + org.opendaylight.aaa + authz-service-config + xml + config + + + + scm:git:ssh://git.opendaylight.org:29418/aaa.git + scm:git:ssh://git.opendaylight.org:29418/aaa.git + HEAD + https://git.opendaylight.org/gerrit/gitweb?p=aaa.git;a=summary + + diff --git a/odl-aaa-moon/features/authz/src/main/features/features.xml b/odl-aaa-moon/features/authz/src/main/features/features.xml new file mode 100644 index 00000000..c5239045 --- /dev/null +++ b/odl-aaa-moon/features/authz/src/main/features/features.xml @@ -0,0 +1,31 @@ + + + + + mvn:org.opendaylight.yangtools/features-yangtools/{{VERSION}}/xml/features + mvn:org.opendaylight.controller/features-config/{{VERSION}}/xml/features + mvn:org.opendaylight.mdsal/features-mdsal/{{VERSION}}/xml/features + mvn:org.opendaylight.controller/features-mdsal/{{VERSION}}/xml/features + mvn:org.opendaylight.aaa/features-aaa-api/{{VERSION}}/xml/features + + + odl-aaa-api + odl-yangtools-common + odl-mdsal-binding-base + odl-mdsal-broker + odl-config-core + mvn:org.opendaylight.aaa/aaa-authz-model/{{VERSION}} + mvn:org.opendaylight.aaa/aaa-authz-service/{{VERSION}} + mvn:org.opendaylight.aaa/authz-service-config/{{VERSION}}/xml/config + mvn:org.opendaylight.aaa/authz-restconf-config/{{VERSION}}/xml/config + + + diff --git a/odl-aaa-moon/features/pom.xml b/odl-aaa-moon/features/pom.xml new file mode 100644 index 00000000..261f73c4 --- /dev/null +++ b/odl-aaa-moon/features/pom.xml @@ -0,0 +1,19 @@ + + + 4.0.0 + + org.opendaylight.aaa + aaa-parent + 0.3.1-Beryllium-SR1 + ../parent + + org.opendaylight.aaa + features-aggregator + pom + + shiro + api + authn + authz + + diff --git a/odl-aaa-moon/features/shiro/pom.xml b/odl-aaa-moon/features/shiro/pom.xml new file mode 100644 index 00000000..50a9971e --- /dev/null +++ b/odl-aaa-moon/features/shiro/pom.xml @@ -0,0 +1,179 @@ + + + + 4.0.0 + + org.opendaylight.odlparent + features-parent + 1.6.1-Beryllium-SR1 + + + + org.opendaylight.aaa + features-aaa-shiro + 0.3.1-Beryllium-SR1 + jar + + + 1.2 + 1.8.3_2 + + + + + + org.opendaylight.aaa + aaa-parent + ${project.version} + import + pom + + + + + + + com.google.code.findbugs + jsr305 + + + org.opendaylight.aaa + features-aaa + 0.3.1-Beryllium-SR1 + features + xml + + + org.opendaylight.aaa + aaa-shiro-act + 0.3.1-Beryllium-SR1 + + + org.opendaylight.aaa + aaa-shiro + 0.3.1-Beryllium-SR1 + cfg + configuration + + + org.opendaylight.aaa + aaa-shiro + + + org.opendaylight.aaa + aaa-authn-sts + 0.3.1-Beryllium-SR1 + + + org.opendaylight.aaa + aaa-authn-api + 0.3.1-Beryllium-SR1 + + + com.sun.jersey + jersey-servlet + + + com.sun.jersey + jersey-core + + + com.sun.jersey + jersey-server + provided + + + javax.servlet + javax.servlet-api + + + org.apache.felix + org.apache.felix.dependencymanager + + + org.apache.felix + org.apache.felix.metatype + + + com.google.guava + guava + + + org.opendaylight.aaa + aaa-shiro + + + org.opendaylight.aaa + aaa-authn + + + org.opendaylight.aaa + aaa-authn-api + + + org.opendaylight.aaa + aaa-authn-sts + + + javax.annotation + javax.annotation-api + ${javax.annotation.api.version} + + + org.apache.felix + org.apache.felix.dependencymanager + + + org.apache.felix + org.apache.felix.metatype + + + org.apache.shiro + shiro-web + + + org.apache.shiro + shiro-core + + + org.apache.servicemix.bundles + org.apache.servicemix.bundles.commons-beanutils + ${servicemix.version} + + + org.apache.oltu.oauth2 + org.apache.oltu.oauth2.resourceserver + + + org.apache.oltu.oauth2 + org.apache.oltu.oauth2.authzserver + + + org.apache.oltu.oauth2 + org.apache.oltu.oauth2.common + + + javax.ws.rs + javax.ws.rs-api + + + org.json + json + + + commons-codec + commons-codec + + + + + scm:git:ssh://git.opendaylight.org:29418/aaa.git + scm:git:ssh://git.opendaylight.org:29418/aaa.git + HEAD + https://git.opendaylight.org/gerrit/gitweb?p=aaa.git;a=summary + + diff --git a/odl-aaa-moon/features/shiro/src/main/features/features.xml b/odl-aaa-moon/features/shiro/src/main/features/features.xml new file mode 100644 index 00000000..c6073a2a --- /dev/null +++ b/odl-aaa-moon/features/shiro/src/main/features/features.xml @@ -0,0 +1,41 @@ + + + + + mvn:org.opendaylight.aaa/features-aaa/{{VERSION}}/xml/features + + + + + + mvn:org.apache.felix/org.apache.felix.dependencymanager/{{VERSION}} + mvn:org.apache.felix/org.apache.felix.metatype/{{VERSION}} + + + odl-aaa-authn + + mvn:org.apache.shiro/shiro-web/{{VERSION}} + mvn:org.apache.shiro/shiro-core/{{VERSION}} + + mvn:com.google.guava/guava/{{VERSION}} + wrap:mvn:javax.annotation/javax.annotation-api/{{VERSION}} + wrap:mvn:com.google.code.findbugs/jsr305/{{VERSION}} + wrap:mvn:commons-codec/commons-codec/{{VERSION}} + wrap:mvn:org.apache.oltu.oauth2/org.apache.oltu.oauth2.resourceserver/{{VERSION}} + wrap:mvn:org.apache.oltu.oauth2/org.apache.oltu.oauth2.authzserver/{{VERSION}} + wrap:mvn:org.apache.oltu.oauth2/org.apache.oltu.oauth2.common/{{VERSION}} + wrap:mvn:org.json/json/{{VERSION}} + mvn:org.apache.servicemix.bundles/org.apache.servicemix.bundles.commons-beanutils/{{VERSION}} + mvn:org.opendaylight.aaa/aaa-shiro/{{VERSION}} + + + mvn:org.opendaylight.aaa/aaa-shiro/{{VERSION}}/cfg/configuration + + + diff --git a/odl-aaa-moon/parent/pom.xml b/odl-aaa-moon/parent/pom.xml new file mode 100644 index 00000000..37bf3ad2 --- /dev/null +++ b/odl-aaa-moon/parent/pom.xml @@ -0,0 +1,278 @@ + + + 4.0.0 + + org.opendaylight.odlparent + odlparent + 1.6.1-Beryllium-SR1 + + + + org.opendaylight.aaa + aaa-parent + 0.3.1-Beryllium-SR1 + pom + + 3.0.4 + + + + + 1.2.1-Beryllium-SR1 + 1.6.1-Beryllium-SR1 + + + 1.0.10 + + + ${project.version} + ${basedir} + + + 0.8.1-Beryllium-SR1 + src/main/yang-gen-config + src/main/yang-gen-sal + 2.0.1-Beryllium-SR1 + 0.8.1-Beryllium-SR1 + 1.3.1-Beryllium-SR1 + 1.3.1-Beryllium-SR1 + 0.4.1-Beryllium-SR1 + 08-authz-config.xml + 09-rest-connector.xml + etc/opendaylight/karaf + + + 1.0.4 + 2.8.3 + 1.1.1 + 1.0.0 + + 08-authn-config.xml + + + 1.4.185 + + + 4.4 + + + 1 + 7.0.0.M2 + 1.6.1-Beryllium-SR1 + + + + + + + org.opendaylight.aaa + aaa-artifacts + ${aaa.version} + pom + import + + + org.opendaylight.yangtools + yangtools-artifacts + ${yangtools.version} + pom + import + + + org.opendaylight.mdsal + mdsal-artifacts + ${mdsal.version} + import + pom + + + org.opendaylight.mdsal.model + mdsal-model-artifacts + ${mdsal.model.version} + import + pom + + + org.opendaylight.controller + mdsal-artifacts + ${controller.mdsal.version} + import + pom + + + org.opendaylight.controller + config-artifacts + ${config.version} + pom + import + + + + + org.glassfish + javax.json + ${glassfish.json.version} + + + org.apache.felix + org.apache.felix.metatype + ${osgi.metatype.version} + + + net.sf.ehcache + ehcache + ${ehcache.version} + + + org.apache.geronimo.specs + geronimo-jta_1.1_spec + ${jta.version} + + + org.apache.oltu.oauth2 + org.apache.oltu.oauth2.common + ${oltu.version} + + + org.apache.oltu.oauth2 + org.apache.oltu.oauth2.authzserver + ${oltu.version} + + + org.apache.oltu.oauth2 + org.apache.oltu.oauth2.resourceserver + ${oltu.version} + + + com.h2database + h2 + ${h2.version} + + + + + org.opendaylight.odlparent + features-test + ${features.test.version} + test + + + javax.inject + javax.inject + ${javax.inject.version} + test + + + org.eclipse.jetty + jetty-servlet-tester + ${servlet.tester.version} + test + + + + + + + + org.jacoco + jacoco-maven-plugin + + + org.opendaylight.aaa.* + + + + + pre-test + + prepare-agent + + + + post-test + + report + + test + + + + + org.apache.maven.plugins + maven-checkstyle-plugin + + + false + true + checkstyle-logging.xml + true + true + ${project.basedir} + **\/*.java,**\/*.xml,**\/*.ini,**\/*.sh,**\/*.bat,**\/*.yang + **\/target\/,**\/bin\/,**\/target-ide\/,**\/src/main/yang-gen-config\/,**\/src/main/yang-gen-sal\/ + + + + + check + + process-sources + + + + + org.opendaylight.yangtools + checkstyle-logging + ${yangtools.version} + + + + + org.codehaus.mojo + build-helper-maven-plugin + + + generate-sources + + add-source + + + + ${jmxGeneratorPath} + ${salGeneratorPath} + + + + + + + + + https://wiki.opendaylight.org/view/AAA:Main + + scm:git:ssh://git.opendaylight.org:29418/aaa.git + scm:git:ssh://git.opendaylight.org:29418/aaa.git + HEAD + + + + + + org.codehaus.mojo + findbugs-maven-plugin + ${findbugs.maven.plugin.version} + + Max + Low + site + + + + org.codehaus.mojo + jdepend-maven-plugin + ${jdepend.maven.plugin.version} + + + + diff --git a/odl-aaa-moon/pom.xml b/odl-aaa-moon/pom.xml new file mode 100644 index 00000000..3d6591e2 --- /dev/null +++ b/odl-aaa-moon/pom.xml @@ -0,0 +1,50 @@ + + + 4.0.0 + + org.opendaylight.aaa + aaa-parent + 0.3.1-Beryllium-SR1 + parent + + + org.opendaylight.aaa + aaa.project + 0.3.1-Beryllium-SR1 + pom + aaa + + 3.0 + + + + aaa-authn-api + aaa-authn + aaa-idp-mapping + aaa-authn-sts + aaa-authn-store + aaa-authn-federation + aaa-authn-sssd + aaa-authn-keystone + aaa-authn-basic + aaa-idmlight + aaa-authn-mdsal-store + aaa-authz + aaa-credential-store-api + artifacts + features + distribution-karaf + parent + aaa-shiro + aaa-shiro-act + aaa-h2-store + + + + scm:git:ssh://git.opendaylight.org:29418/aaa.git + scm:git:ssh://git.opendaylight.org:29418/aaa.git + HEAD + https://wiki.opendaylight.org/view/AAA:Main + + + -- 2.16.6