keycloak-memoizeit
Changes
adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/AbstractPolicyEnforcer.java 56(+32 -24)
adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/KeycloakAdapterPolicyEnforcer.java 53(+25 -28)
adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/PathMatcher.java 12(+9 -3)
adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/PolicyEnforcer.java 33(+20 -13)
adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeploymentBuilder.java 1(+1 -0)
adapters/pom.xml 2(+1 -1)
adapters/saml/core/src/main/java/org/keycloak/adapters/saml/profile/AbstractSamlAuthenticationHandler.java 6(+1 -5)
authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/aggregated/AggregatePolicyProvider.java 22(+8 -14)
authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/aggregated/AggregatePolicyProviderFactory.java 7(+4 -3)
authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/client/ClientPolicyProvider.java 23(+7 -16)
authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/client/ClientPolicyProviderFactory.java 61(+35 -26)
authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/js/JSPolicyProvider.java 20(+11 -9)
authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/js/JSPolicyProviderFactory.java 17(+14 -3)
authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/resource/ResourcePolicyProvider.java 3(+1 -2)
authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/resource/ResourcePolicyProviderFactory.java 7(+4 -3)
authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/role/RolePolicyProvider.java 37(+12 -25)
authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/role/RolePolicyProviderFactory.java 90(+59 -31)
authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/scope/ScopePolicyProvider.java 7(+0 -7)
authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/scope/ScopePolicyProviderFactory.java 7(+4 -3)
authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/time/TimePolicyProvider.java 40(+20 -20)
authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/time/TimePolicyProviderFactory.java 7(+4 -3)
authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/user/UserPolicyProvider.java 9(+2 -7)
authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/user/UserPolicyProviderFactory.java 72(+44 -28)
authz/policy/drools/src/main/java/org/keycloak/authorization/policy/provider/drools/DroolsPolicyProvider.java 13(+8 -5)
authz/policy/drools/src/main/java/org/keycloak/authorization/policy/provider/drools/DroolsPolicyProviderFactory.java 42(+15 -27)
core/src/main/java/org/keycloak/representations/idm/authorization/PolicyRepresentation.java 23(+0 -23)
core/src/main/java/org/keycloak/representations/idm/authorization/ResourceRepresentation.java 31(+15 -16)
core/src/main/java/org/keycloak/TokenVerifier.java 170(+170 -0)
examples/demo-template/pom.xml 2(+1 -1)
examples/fuse/pom.xml 2(+1 -1)
examples/ldap/ldaprealm.json 28(+28 -0)
examples/pom.xml 2(+1 -1)
examples/providers/rest/pom.xml 2(+1 -1)
examples/saml/pom.xml 2(+1 -1)
federation/ldap/src/main/java/org/keycloak/storage/ldap/idm/store/ldap/LDAPIdentityStore.java 27(+24 -3)
federation/ldap/src/main/java/org/keycloak/storage/ldap/idm/store/ldap/LDAPOperationManager.java 11(+10 -1)
federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/AbstractLDAPStorageMapper.java 1(+1 -0)
federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/membership/CommonLDAPGroupMapperConfig.java 9(+9 -0)
federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/membership/group/GroupLDAPStorageMapper.java 29(+23 -6)
federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/membership/group/GroupLDAPStorageMapperFactory.java 15(+14 -1)
federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/membership/group/GroupMapperConfig.java 5(+0 -5)
federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/membership/MembershipType.java 34(+32 -2)
federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/membership/role/RoleLDAPStorageMapper.java 23(+19 -4)
federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/membership/role/RoleLDAPStorageMapperFactory.java 14(+13 -1)
federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/membership/UserRolesRetrieveStrategy.java 10(+6 -4)
federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/UserAttributeLDAPStorageMapper.java 77(+67 -10)
federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/UserAttributeLDAPStorageMapperFactory.java 22(+21 -1)
integration/admin-client/src/main/java/org/keycloak/admin/client/resource/BearerAuthFilter.java 6(+4 -2)
integration/admin-client/src/main/java/org/keycloak/admin/client/resource/PolicyResource.java 33(+31 -2)
integration/admin-client/src/main/java/org/keycloak/admin/client/resource/ResourceResource.java 10(+10 -0)
integration/admin-client/src/main/java/org/keycloak/admin/client/resource/ResourceScopeResource.java 9(+9 -0)
integration/client-cli/admin-cli/pom.xml 177(+177 -0)
integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/aesh/AeshConsoleCallbackImpl.java 117(+117 -0)
integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/aesh/AeshEnhancer.java 41(+41 -0)
integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/aesh/Globals.java 31(+31 -0)
integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/aesh/ValveInputStream.java 89(+89 -0)
integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/AbstractAuthOptionsCmd.java 267(+267 -0)
integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/AbstractGlobalOptionsCmd.java 113(+113 -0)
integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/AbstractRequestCmd.java 435(+435 -0)
integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/AddRolesCmd.java 334(+334 -0)
integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/ConfigCmd.java 95(+95 -0)
integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/ConfigCredentialsCmd.java 275(+275 -0)
integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/ConfigTruststoreCmd.java 200(+200 -0)
integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/CreateCmd.java 167(+167 -0)
integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/DeleteCmd.java 104(+104 -0)
integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/GetCmd.java 168(+168 -0)
integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/GetRolesCmd.java 325(+325 -0)
integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/HelpCmd.java 107(+107 -0)
integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/KcAdmCmd.java 101(+101 -0)
integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/NewObjectCmd.java 204(+204 -0)
integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/RemoveRolesCmd.java 334(+334 -0)
integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/SetPasswordCmd.java 177(+177 -0)
integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/UpdateCmd.java 165(+165 -0)
integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/common/AttributeKey.java 170(+170 -0)
integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/common/AttributeOperation.java 58(+58 -0)
integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/common/CmdStdinContext.java 44(+44 -0)
integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/config/ConfigData.java 176(+176 -0)
integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/config/ConfigHandler.java 28(+28 -0)
integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/config/ConfigUpdateOperation.java 26(+26 -0)
integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/config/FileConfigHandler.java 135(+135 -0)
integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/config/InMemoryConfigHandler.java 39(+39 -0)
integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/config/RealmConfigData.java 172(+172 -0)
integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/httpcomponents/HttpDelete.java 39(+39 -0)
integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/KcAdmMain.java 94(+94 -0)
integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/operations/ClientOperations.java 29(+29 -0)
integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/operations/GroupOperations.java 58(+58 -0)
integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/operations/LocalSearch.java 60(+60 -0)
integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/operations/RoleOperations.java 129(+129 -0)
integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/operations/UserOperations.java 96(+96 -0)
integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/AccessibleBufferOutputStream.java 66(+66 -0)
integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/AttributeException.java 39(+39 -0)
integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/AuthUtil.java 202(+202 -0)
integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/ConfigUtil.java 116(+116 -0)
integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/FilterUtil.java 59(+59 -0)
integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/Header.java 39(+39 -0)
integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/Headers.java 55(+55 -0)
integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/HeadersBody.java 72(+72 -0)
integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/HeadersBodyStatus.java 68(+68 -0)
integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/HttpResponseException.java 34(+34 -0)
integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/HttpUtil.java 450(+450 -0)
integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/IoUtil.java 255(+255 -0)
integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/OsArch.java 71(+71 -0)
integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/OsUtil.java 64(+64 -0)
integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/OutputFormat.java 25(+25 -0)
integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/OutputUtil.java 107(+107 -0)
integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/ParseUtil.java 111(+111 -0)
integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/ReflectionUtil.java 228(+228 -0)
integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/ReturnFields.java 333(+333 -0)
integration/client-cli/admin-cli/src/main/resources/META-INF/services/javax.ws.rs.ext.Providers 23(+23 -0)
integration/client-cli/admin-cli/src/test/java/org/keycloak/client/admin/cli/util/MergeAttributesTest.java 101(+101 -0)
integration/client-cli/admin-cli/src/test/java/org/keycloak/client/admin/cli/util/ReturnFieldsTest.java 142(+142 -0)
integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/commands/ConfigTruststoreCmd.java 20(+18 -2)
integration/client-cli/pom.xml 11(+11 -0)
model/infinispan/src/main/java/org/keycloak/models/authorization/infinispan/CachedPolicyStore.java 243(+135 -108)
model/infinispan/src/main/java/org/keycloak/models/authorization/infinispan/CachedResourceServerStore.java 52(+28 -24)
model/infinispan/src/main/java/org/keycloak/models/authorization/infinispan/CachedResourceStore.java 183(+103 -80)
model/infinispan/src/main/java/org/keycloak/models/authorization/infinispan/CachedScopeStore.java 85(+55 -30)
model/infinispan/src/main/java/org/keycloak/models/authorization/infinispan/InfinispanStoreFactoryProvider.java 19(+15 -4)
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedRealm.java 15(+12 -3)
model/mongo/src/main/java/org/keycloak/authorization/mongo/store/MongoAuthorizationStoreFactory.java 3(+1 -2)
model/mongo/src/main/java/org/keycloak/authorization/mongo/store/MongoResourceStore.java 36(+25 -11)
model/mongo/src/main/java/org/keycloak/connections/mongo/updater/impl/updates/Update2_5_0.java 14(+13 -1)
model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserProvider.java 35(+29 -6)
pom.xml 5(+5 -0)
proxy/pom.xml 2(+1 -1)
saml-core/src/main/java/org/keycloak/saml/processing/core/parsers/saml/metadata/SAMLEntityDescriptorParser.java 7(+5 -2)
saml-core/src/main/java/org/keycloak/saml/processing/core/parsers/util/SAML11ParserUtil.java 8(+5 -3)
saml-core/src/main/java/org/keycloak/saml/processing/core/saml/v2/util/SignatureUtil.java 20(+11 -9)
saml-core/src/main/java/org/keycloak/saml/processing/core/saml/v2/util/StaxWriterUtil.java 20(+11 -9)
saml-core/src/main/java/org/keycloak/saml/processing/web/util/RedirectBindingSignatureUtil.java 10(+5 -5)
saml-core/src/test/java/org/keycloak/saml/processing/core/parsers/saml/SAMLParserTest.java 165(+108 -57)
saml-core/src/test/resources/org/keycloak/saml/processing/core/parsers/saml/KEYCLOAK-3971-8859-2-in-header-authnresponse.xml 31(+31 -0)
saml-core/src/test/resources/org/keycloak/saml/processing/core/parsers/saml/KEYCLOAK-3971-utf-8-no-header-authnresponse.xml 30(+30 -0)
saml-core/src/test/resources/org/keycloak/saml/processing/core/parsers/saml/KEYCLOAK-4040-sharefile-metadata.xml 33(+33 -0)
server-spi-private/src/main/java/org/keycloak/authorization/permission/evaluator/Evaluators.java 11(+4 -7)
server-spi-private/src/main/java/org/keycloak/authorization/permission/evaluator/ScheduledPermissionEvaluator.java 7(+3 -4)
server-spi-private/src/main/java/org/keycloak/authorization/policy/evaluation/DecisionResultCollector.java 6(+4 -2)
server-spi-private/src/main/java/org/keycloak/authorization/policy/evaluation/DefaultEvaluation.java 28(+14 -14)
server-spi-private/src/main/java/org/keycloak/authorization/policy/evaluation/DefaultPolicyEvaluator.java 102(+50 -52)
server-spi-private/src/main/java/org/keycloak/authorization/policy/evaluation/Evaluation.java 11(+11 -0)
server-spi-private/src/main/java/org/keycloak/authorization/policy/provider/PolicyProviderFactory.java 2(+1 -1)
server-spi-private/src/main/java/org/keycloak/authorization/store/syncronization/RealmSynchronizer.java 2(+1 -1)
server-spi-private/src/main/java/org/keycloak/authorization/store/syncronization/UserSynchronizer.java 33(+23 -10)
server-spi-private/src/main/java/org/keycloak/provider/ConfigurationValidationHelper.java 22(+22 -0)
services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpCreateUserIfUniqueAuthenticator.java 2(+1 -1)
services/src/main/java/org/keycloak/authentication/authenticators/browser/AbstractUsernameFormAuthenticator.java 6(+3 -3)
services/src/main/java/org/keycloak/authentication/authenticators/browser/ScriptBasedAuthenticatorFactory.java 2(+1 -1)
services/src/main/java/org/keycloak/authentication/authenticators/directgrant/ValidateOTP.java 9(+6 -3)
services/src/main/java/org/keycloak/authentication/authenticators/directgrant/ValidatePassword.java 9(+6 -3)
services/src/main/java/org/keycloak/authentication/authenticators/directgrant/ValidateUsername.java 8(+6 -2)
services/src/main/java/org/keycloak/authentication/authenticators/resetcred/ResetCredentialChooseUser.java 9(+5 -4)
services/src/main/java/org/keycloak/authorization/admin/representation/PolicyEvaluationRequest.java 39(+3 -36)
services/src/main/java/org/keycloak/authorization/admin/representation/PolicyEvaluationResponse.java 57(+43 -14)
services/src/main/java/org/keycloak/authorization/authorization/AuthorizationTokenService.java 10(+5 -5)
services/src/main/java/org/keycloak/authorization/DefaultAuthorizationProviderFactory.java 29(+26 -3)
services/src/main/java/org/keycloak/authorization/protection/permission/AbstractPermissionService.java 6(+3 -3)
services/src/main/java/org/keycloak/authorization/protection/resource/ResourceService.java 12(+5 -7)
services/src/main/java/org/keycloak/protocol/oidc/installation/KeycloakOIDCClientInstallation.java 2(+1 -1)
services/src/main/java/org/keycloak/protocol/saml/installation/SamlIDPDescriptorClientInstallation.java 9(+5 -4)
services/src/main/java/org/keycloak/services/clientregistration/ClientRegistrationTokenUtils.java 4(+2 -2)
services/src/main/java/org/keycloak/services/resources/admin/ClientAttributeCertificateResource.java 5(+2 -3)
testsuite/integration/pom.xml 27(+0 -27)
testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/AdapterTestStrategy.java 49(+49 -0)
testsuite/integration/src/test/java/org/keycloak/testsuite/authorization/AbstractPhotozAdminTest.java 2(+1 -1)
testsuite/integration/src/test/java/org/keycloak/testsuite/authorization/ResourceManagementTest.java 6(+3 -3)
testsuite/integration/src/test/java/org/keycloak/testsuite/authorization/ResourcePermissionManagementTest.java 16(+11 -5)
testsuite/integration/src/test/java/org/keycloak/testsuite/authorization/ScopeManagementTest.java 6(+3 -3)
testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/ldap/LDAPBinaryAttributesTest.java 287(+287 -0)
testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/ldap/LDAPGroupMapperSyncTest.java 8(+4 -4)
testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/ldap/LDAPGroupMapperTest.java 8(+4 -4)
testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/ldap/LDAPLegacyImportTest.java 7(+6 -1)
testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/ldap/LDAPMultipleAttributesTest.java 11(+10 -1)
testsuite/integration/src/test/resources/adapter-test/product-autodetect-bearer-only-keycloak.json 11(+11 -0)
testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/authorization/TestPolicyProviderFactory.java 9(+3 -6)
testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestingResourceProvider.java 9(+9 -0)
testsuite/integration-arquillian/test-apps/photoz/photoz-html5-client/src/main/webapp/index.html 7(+6 -1)
testsuite/integration-arquillian/test-apps/photoz/photoz-html5-client/src/main/webapp/js/app.js 11(+10 -1)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/PhotozClientAuthzTestApp.java 18(+17 -1)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/provider/URLProvider.java 2(+1 -1)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/cli/exec/AbstractExec.java 245(+245 -0)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/cli/exec/AbstractExecBuilder.java 44(+44 -0)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/cli/exec/ExecutionException.java 2(+1 -1)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/cli/exec/InteractiveInputStream.java 120(+120 -0)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/cli/exec/LoggingOutputStream.java 53(+53 -0)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/cli/exec/NullInputStream.java 12(+12 -0)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/cli/exec/StreamReaderThread.java 33(+33 -0)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/cli/KcAdmExec.java 58(+58 -0)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/cli/KcRegExec.java 443(+14 -429)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestingResource.java 5(+5 -0)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/ProfileAssume.java 4(+2 -2)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountTest.java 120(+102 -18)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionEmailVerificationTest.java 33(+32 -1)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractPhotozExampleAdapterTest.java 21(+21 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractOIDCPublicKeyRotationAdapterTest.java 30(+18 -12)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractSAMLServletsAdapterTest.java 58(+52 -6)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/ProvidersTest.java 5(+2 -3)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/AbstractClientTest.java 24(+22 -2)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/EnforcerConfigTest.java 5(+3 -2)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/GenericPolicyManagementTest.java 93(+54 -39)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/ClientDescriptionConverterTest.java 41(+41 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/InstallationTest.java 32(+32 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/group/GroupMappersTest.java 9(+3 -6)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/group/GroupTest.java 28(+27 -1)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/partialimport/PartialImportTest.java 55(+55 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/PermissionsTest.java 4(+3 -1)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/realm/RealmTest.java 2(+2 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerConfiguration.java 2(+1 -1)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/AbstractCliTest.java 54(+54 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/admin/AbstractAdmCliTest.java 387(+387 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/admin/KcAdmCreateTest.java 131(+131 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/admin/KcAdmTest.java 561(+561 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/admin/KcAdmTruststoreTest.java 115(+115 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/admin/KcAdmUpdateTest.java 130(+130 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/registration/AbstractRegCliTest.java 43(+2 -41)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/registration/KcRegConfigTest.java 2(+1 -1)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/registration/KcRegCreateTest.java 2(+1 -1)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/registration/KcRegTest.java 6(+3 -3)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/registration/KcRegTruststoreTest.java 2(+1 -1)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/registration/KcRegUpdateTest.java 2(+1 -1)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/registration/KcRegUpdateTokenTest.java 2(+1 -1)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/exportimport/ExportImportTest.java 4(+2 -2)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/exportimport/ExportImportUtil.java 2(+1 -1)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/RegisterTest.java 59(+56 -3)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/keys/GeneratedHmacKeyProviderTest.java 182(+182 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/keys/GeneratedRsaKeyProviderTest.java 12(+6 -6)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/keys/ImportedRsaKeyProviderTest.java 23(+12 -11)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/keys/JavaKeystoreKeyProviderTest.java 3(+2 -1)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/keys/KeyRotationTest.java 26(+20 -6)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/AccessTokenDuplicateEmailsNotCleanedUpTest.java 100(+100 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/AccessTokenDuplicateEmailsTest.java 128(+128 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/AccessTokenNoEmailLoginTest.java 83(+83 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/AccessTokenTest.java 8(+8 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OIDCProtocolMappersTest.java 28(+17 -11)
testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/jboss-deployment-structure.xml 9(+3 -6)
testsuite/integration-arquillian/tests/base/src/test/resources/cli/kcadm/admin-cli-keystore.jks 0(+0 -0)
testsuite/integration-arquillian/tests/base/src/test/resources/org/keycloak/testsuite/admin/client/KEYCLOAK-4040-sharefile-metadata.xml 33(+33 -0)
testsuite/integration-arquillian/tests/base/src/test/resources/testrealm-duplicate-emails.json 142(+142 -0)
testsuite/integration-arquillian/tests/other/adapters/jboss/remote/src/test/resources/xslt/arquillian.xsl 1(+1 -0)
testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/federation/CreateKerberosUserProvider.java 2(+1 -1)
testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/federation/CreateLdapUserProvider.java 2(+1 -1)
testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/federation/KerberosUserProviderForm.java 2(+0 -2)
testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/AbstractConsoleTest.java 4(+2 -2)
testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/federation/KerberosUserFederationTest.java 20(+11 -9)
testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/federation/LdapUserFederationTest.java 73(+37 -36)
testsuite/integration-arquillian/tests/other/nodejs_adapter/src/test/java/org/keycloak/testsuite/adapter/nodejs/NodejsAdapterTest.java 1(+0 -1)
themes/src/main/resources/theme/base/admin/resources/partials/authz/permission/provider/resource-server-policy-resource-detail.html 8(+2 -6)
themes/src/main/resources/theme/base/admin/resources/partials/authz/permission/provider/resource-server-policy-scope-detail.html 26(+6 -20)
themes/src/main/resources/theme/base/admin/resources/partials/authz/permission/resource-server-permission-list.html 50(+41 -9)
themes/src/main/resources/theme/base/admin/resources/partials/authz/policy/provider/resource-server-policy-aggregate-detail.html 5(+1 -4)
themes/src/main/resources/theme/base/admin/resources/partials/authz/policy/resource-server-policy-evaluate.html 26(+7 -19)
themes/src/main/resources/theme/base/admin/resources/partials/authz/policy/resource-server-policy-list.html 45(+44 -1)
themes/src/main/resources/theme/base/admin/resources/partials/authz/resource-server-resource-detail.html 4(+1 -3)
themes/src/main/resources/theme/base/admin/resources/partials/authz/resource-server-resource-list.html 77(+55 -22)
themes/src/main/resources/theme/base/admin/resources/partials/authz/resource-server-scope-list.html 73(+52 -21)
Details
diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/AbstractPolicyEnforcer.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/AbstractPolicyEnforcer.java
index 0186e18..1673aa6 100644
--- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/AbstractPolicyEnforcer.java
+++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/AbstractPolicyEnforcer.java
@@ -17,6 +17,13 @@
*/
package org.keycloak.adapters.authorization;
+import java.net.URI;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
import org.jboss.logging.Logger;
import org.keycloak.AuthorizationContext;
import org.keycloak.KeycloakSecurityContext;
@@ -32,13 +39,6 @@ import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.Enforce
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.PathConfig;
import org.keycloak.representations.idm.authorization.Permission;
-import java.net.URI;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Set;
-
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
@@ -48,7 +48,7 @@ public abstract class AbstractPolicyEnforcer {
private final PolicyEnforcerConfig enforcerConfig;
private final PolicyEnforcer policyEnforcer;
- private List<PathConfig> paths;
+ private Map<String, PathConfig> paths;
private AuthzClient authzClient;
private PathMatcher pathMatcher;
@@ -57,7 +57,7 @@ public abstract class AbstractPolicyEnforcer {
this.enforcerConfig = policyEnforcer.getEnforcerConfig();
this.authzClient = policyEnforcer.getClient();
this.pathMatcher = new PathMatcher();
- this.paths = new ArrayList<>(policyEnforcer.getPaths());
+ this.paths = policyEnforcer.getPaths();
}
public AuthorizationContext authorize(OIDCHttpFacade httpFacade) {
@@ -75,8 +75,7 @@ public abstract class AbstractPolicyEnforcer {
if (accessToken != null) {
Request request = httpFacade.getRequest();
Response response = httpFacade.getResponse();
- String pathInfo = URI.create(request.getURI()).getPath().substring(1);
- String path = pathInfo.substring(pathInfo.indexOf('/'), pathInfo.length());
+ String path = getPath(request);
PathConfig pathConfig = this.pathMatcher.matches(path, this.paths);
LOGGER.debugf("Checking permissions for path [%s] with config [%s].", request.getURI(), pathConfig);
@@ -122,12 +121,9 @@ public abstract class AbstractPolicyEnforcer {
protected boolean isAuthorized(PathConfig actualPathConfig, Set<String> requiredScopes, AccessToken accessToken, OIDCHttpFacade httpFacade) {
Request request = httpFacade.getRequest();
PolicyEnforcerConfig enforcerConfig = getEnforcerConfig();
- String accessDeniedPath = enforcerConfig.getOnDenyRedirectTo();
- if (accessDeniedPath != null) {
- if (request.getURI().contains(accessDeniedPath)) {
- return true;
- }
+ if (isDefaultAccessDeniedUri(request, enforcerConfig)) {
+ return true;
}
AccessToken.Authorization authorization = accessToken.getAuthorization();
@@ -173,6 +169,17 @@ public abstract class AbstractPolicyEnforcer {
return false;
}
+ private boolean isDefaultAccessDeniedUri(Request request, PolicyEnforcerConfig enforcerConfig) {
+ String accessDeniedPath = enforcerConfig.getOnDenyRedirectTo();
+
+ if (accessDeniedPath != null) {
+ if (request.getURI().contains(accessDeniedPath)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
private boolean hasResourceScopePermission(Set<String> requiredScopes, Permission permission, PathConfig actualPathConfig) {
Set<String> allowedScopes = permission.getScopes();
return (allowedScopes.containsAll(requiredScopes) || allowedScopes.isEmpty());
@@ -220,27 +227,23 @@ public abstract class AbstractPolicyEnforcer {
}
private PathConfig resolvePathConfig(PathConfig originalConfig, Request request) {
+ String path = getPath(request);
+
if (originalConfig.hasPattern()) {
- String pathInfo = URI.create(request.getURI()).getPath().substring(1);
- String path = pathInfo.substring(pathInfo.indexOf('/'), pathInfo.length());
ProtectedResource resource = this.authzClient.protection().resource();
Set<String> search = resource.findByFilter("uri=" + path);
if (!search.isEmpty()) {
// resource does exist on the server, cache it
ResourceRepresentation targetResource = resource.findById(search.iterator().next()).getResourceDescription();
- PathConfig config = new PathConfig();
+ PathConfig config = PolicyEnforcer.createPathConfig(targetResource);
- config.setId(targetResource.getId());
- config.setName(targetResource.getName());
- config.setType(targetResource.getType());
- config.setPath(targetResource.getUri());
config.setScopes(originalConfig.getScopes());
config.setMethods(originalConfig.getMethods());
config.setParentConfig(originalConfig);
config.setEnforcementMode(originalConfig.getEnforcementMode());
- this.paths.add(config);
+ this.policyEnforcer.addPath(config);
return config;
}
@@ -249,6 +252,11 @@ public abstract class AbstractPolicyEnforcer {
return originalConfig;
}
+ private String getPath(Request request) {
+ String pathInfo = URI.create(request.getURI()).getPath().substring(1);
+ return pathInfo.substring(pathInfo.indexOf('/'), pathInfo.length());
+ }
+
private Set<String> getRequiredScopes(PathConfig pathConfig, Request request) {
Set<String> requiredScopes = new HashSet<>();
diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/KeycloakAdapterPolicyEnforcer.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/KeycloakAdapterPolicyEnforcer.java
index a12fc84..b6df2ea 100644
--- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/KeycloakAdapterPolicyEnforcer.java
+++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/KeycloakAdapterPolicyEnforcer.java
@@ -17,6 +17,10 @@
*/
package org.keycloak.adapters.authorization;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Set;
+
import org.jboss.logging.Logger;
import org.keycloak.adapters.KeycloakDeployment;
import org.keycloak.adapters.OIDCHttpFacade;
@@ -34,10 +38,6 @@ import org.keycloak.representations.AccessToken;
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.PathConfig;
import org.keycloak.representations.idm.authorization.Permission;
-import java.util.ArrayList;
-import java.util.HashSet;
-import java.util.Set;
-
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
@@ -51,39 +51,34 @@ public class KeycloakAdapterPolicyEnforcer extends AbstractPolicyEnforcer {
@Override
protected boolean isAuthorized(PathConfig pathConfig, Set<String> requiredScopes, AccessToken accessToken, OIDCHttpFacade httpFacade) {
- int retry = 2;
AccessToken original = accessToken;
- while (retry > 0) {
- if (super.isAuthorized(pathConfig, requiredScopes, accessToken, httpFacade)) {
- return true;
- }
-
- accessToken = requestAuthorizationToken(pathConfig, requiredScopes, httpFacade);
-
- if (accessToken == null) {
- return false;
- }
+ if (super.isAuthorized(pathConfig, requiredScopes, accessToken, httpFacade)) {
+ return true;
+ }
- AccessToken.Authorization authorization = original.getAuthorization();
+ accessToken = requestAuthorizationToken(pathConfig, requiredScopes, httpFacade);
- if (authorization == null) {
- authorization = new AccessToken.Authorization();
- authorization.setPermissions(new ArrayList<Permission>());
- }
+ if (accessToken == null) {
+ return false;
+ }
- AccessToken.Authorization newAuthorization = accessToken.getAuthorization();
+ AccessToken.Authorization authorization = original.getAuthorization();
- if (newAuthorization != null) {
- authorization.getPermissions().addAll(newAuthorization.getPermissions());
- }
+ if (authorization == null) {
+ authorization = new AccessToken.Authorization();
+ authorization.setPermissions(new ArrayList<Permission>());
+ }
- original.setAuthorization(authorization);
+ AccessToken.Authorization newAuthorization = accessToken.getAuthorization();
- retry--;
+ if (newAuthorization != null) {
+ authorization.getPermissions().addAll(newAuthorization.getPermissions());
}
- return false;
+ original.setAuthorization(authorization);
+
+ return super.isAuthorized(pathConfig, requiredScopes, accessToken, httpFacade);
}
@Override
@@ -108,7 +103,7 @@ public class KeycloakAdapterPolicyEnforcer extends AbstractPolicyEnforcer {
KeycloakDeployment deployment = getPolicyEnforcer().getDeployment();
if (getEnforcerConfig().getUserManagedAccess() != null) {
- LOGGER.debug("Obtaining authorization for authenticated user.");
+ LOGGER.debug("Obtaining authorization for authenticated user.");
PermissionRequest permissionRequest = new PermissionRequest();
permissionRequest.setResourceSetId(pathConfig.getId());
@@ -136,12 +131,14 @@ public class KeycloakAdapterPolicyEnforcer extends AbstractPolicyEnforcer {
permissionRequest.setResourceSetId(pathConfig.getId());
permissionRequest.setResourceSetName(pathConfig.getName());
permissionRequest.setScopes(new HashSet<>(pathConfig.getScopes()));
+ LOGGER.debugf("Sending entitlements request: resource_set_id [%s], resource_set_name [%s], scopes [%s].", permissionRequest.getResourceSetId(), permissionRequest.getResourceSetName(), permissionRequest.getScopes());
request.addPermission(permissionRequest);
EntitlementResponse authzResponse = authzClient.entitlement(accessToken).get(authzClient.getConfiguration().getClientId(), request);
return AdapterRSATokenVerifier.verifyToken(authzResponse.getRpt(), deployment);
}
}
} catch (AuthorizationDeniedException e) {
+ LOGGER.debug("Authorization denied", e);
return null;
} catch (Exception e) {
throw new RuntimeException("Unexpected error during authorization request.", e);
diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/PathMatcher.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/PathMatcher.java
index 5ac1b79..8865892 100644
--- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/PathMatcher.java
+++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/PathMatcher.java
@@ -19,7 +19,7 @@ package org.keycloak.adapters.authorization;
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.PathConfig;
-import java.util.List;
+import java.util.Map;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
@@ -28,10 +28,16 @@ class PathMatcher {
private static final String ANY_RESOURCE_PATTERN = "/*";
- PathConfig matches(final String requestedUri, List<PathConfig> paths) {
+ PathConfig matches(final String requestedUri, Map<String, PathConfig> paths) {
+ PathConfig pathConfig = paths.get(requestedUri);
+
+ if (pathConfig != null) {
+ return pathConfig;
+ }
+
PathConfig actualConfig = null;
- for (PathConfig entry : paths) {
+ for (PathConfig entry : paths.values()) {
String protectedUri = entry.getPath();
String selectedUri = null;
diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/PolicyEnforcer.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/PolicyEnforcer.java
index 37b8f3d..f8a5d29 100644
--- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/PolicyEnforcer.java
+++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/PolicyEnforcer.java
@@ -34,8 +34,10 @@ import org.keycloak.representations.idm.authorization.Permission;
import java.util.ArrayList;
import java.util.Collections;
+import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
+import java.util.Map;
import java.util.Set;
/**
@@ -48,7 +50,7 @@ public class PolicyEnforcer {
private final KeycloakDeployment deployment;
private final AuthzClient authzClient;
private final PolicyEnforcerConfig enforcerConfig;
- private final List<PathConfig> paths;
+ private final Map<String, PathConfig> paths;
public PolicyEnforcer(KeycloakDeployment deployment, AdapterConfig adapterConfig) {
this.deployment = deployment;
@@ -58,7 +60,7 @@ public class PolicyEnforcer {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Initialization complete. Path configurations:");
- for (PathConfig pathConfig : this.paths) {
+ for (PathConfig pathConfig : this.paths.values()) {
LOGGER.debug(pathConfig);
}
}
@@ -96,15 +98,19 @@ public class PolicyEnforcer {
return authzClient;
}
- public List<PathConfig> getPaths() {
- return Collections.unmodifiableList(paths);
+ public Map<String, PathConfig> getPaths() {
+ return paths;
+ }
+
+ void addPath(PathConfig pathConfig) {
+ paths.put(pathConfig.getPath(), pathConfig);
}
KeycloakDeployment getDeployment() {
return deployment;
}
- private List<PathConfig> configurePaths(ProtectedResource protectedResource, PolicyEnforcerConfig enforcerConfig) {
+ private Map<String, PathConfig> configurePaths(ProtectedResource protectedResource, PolicyEnforcerConfig enforcerConfig) {
boolean loadPathsFromServer = true;
for (PathConfig pathConfig : enforcerConfig.getPaths()) {
@@ -123,8 +129,8 @@ public class PolicyEnforcer {
}
}
- private List<PathConfig> configureDefinedPaths(ProtectedResource protectedResource, PolicyEnforcerConfig enforcerConfig) {
- List<PathConfig> paths = new ArrayList<>();
+ private Map<String, PathConfig> configureDefinedPaths(ProtectedResource protectedResource, PolicyEnforcerConfig enforcerConfig) {
+ Map<String, PathConfig> paths = Collections.synchronizedMap(new HashMap<String, PathConfig>());
for (PathConfig pathConfig : enforcerConfig.getPaths()) {
Set<String> search;
@@ -172,7 +178,7 @@ public class PolicyEnforcer {
PathConfig existingPath = null;
- for (PathConfig current : paths) {
+ for (PathConfig current : paths.values()) {
if (current.getId().equals(pathConfig.getId()) && current.getPath().equals(pathConfig.getPath())) {
existingPath = current;
break;
@@ -180,7 +186,7 @@ public class PolicyEnforcer {
}
if (existingPath == null) {
- paths.add(pathConfig);
+ paths.put(pathConfig.getPath(), pathConfig);
} else {
existingPath.getMethods().addAll(pathConfig.getMethods());
existingPath.getScopes().addAll(pathConfig.getScopes());
@@ -190,23 +196,24 @@ public class PolicyEnforcer {
return paths;
}
- private List<PathConfig> configureAllPathsForResourceServer(ProtectedResource protectedResource) {
+ private Map<String, PathConfig> configureAllPathsForResourceServer(ProtectedResource protectedResource) {
LOGGER.info("Querying the server for all resources associated with this application.");
- List<PathConfig> paths = new ArrayList<>();
+ Map<String, PathConfig> paths = Collections.synchronizedMap(new HashMap<String, PathConfig>());
for (String id : protectedResource.findAll()) {
RegistrationResponse response = protectedResource.findById(id);
ResourceRepresentation resourceDescription = response.getResourceDescription();
if (resourceDescription.getUri() != null) {
- paths.add(createPathConfig(resourceDescription));
+ PathConfig pathConfig = createPathConfig(resourceDescription);
+ paths.put(pathConfig.getPath(), pathConfig);
}
}
return paths;
}
- private PathConfig createPathConfig(ResourceRepresentation resourceDescription) {
+ static PathConfig createPathConfig(ResourceRepresentation resourceDescription) {
PathConfig pathConfig = new PathConfig();
pathConfig.setId(resourceDescription.getId());
diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeployment.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeployment.java
index b9ee4c6..8664800 100755
--- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeployment.java
+++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeployment.java
@@ -57,6 +57,7 @@ public class KeycloakDeployment {
protected String resourceName;
protected boolean bearerOnly;
+ protected boolean autodetectBearerOnly;
protected boolean enableBasicAuth;
protected boolean publicClient;
protected Map<String, Object> resourceCredentials = new HashMap<>();
@@ -201,6 +202,14 @@ public class KeycloakDeployment {
this.bearerOnly = bearerOnly;
}
+ public boolean isAutodetectBearerOnly() {
+ return autodetectBearerOnly;
+ }
+
+ public void setAutodetectBearerOnly(boolean autodetectBearerOnly) {
+ this.autodetectBearerOnly = autodetectBearerOnly;
+ }
+
public boolean isEnableBasicAuth() {
return enableBasicAuth;
}
diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeploymentBuilder.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeploymentBuilder.java
index 85b19ca..65e9456 100755
--- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeploymentBuilder.java
+++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeploymentBuilder.java
@@ -99,6 +99,7 @@ public class KeycloakDeploymentBuilder {
}
deployment.setBearerOnly(adapterConfig.isBearerOnly());
+ deployment.setAutodetectBearerOnly(adapterConfig.isAutodetectBearerOnly());
deployment.setEnableBasicAuth(adapterConfig.isEnableBasicAuth());
deployment.setAlwaysRefreshToken(adapterConfig.isAlwaysRefreshToken());
deployment.setRegisterNodeAtStartup(adapterConfig.isRegisterNodeAtStartup());
diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/RequestAuthenticator.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/RequestAuthenticator.java
index c04f21c..0cbe687 100755
--- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/RequestAuthenticator.java
+++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/RequestAuthenticator.java
@@ -17,6 +17,8 @@
package org.keycloak.adapters;
+import java.util.Collections;
+import java.util.List;
import org.jboss.logging.Logger;
import org.keycloak.KeycloakPrincipal;
import org.keycloak.adapters.spi.AuthChallenge;
@@ -116,6 +118,12 @@ public abstract class RequestAuthenticator {
return AuthOutcome.NOT_ATTEMPTED;
}
+ if (isAutodetectedBearerOnly(facade.getRequest())) {
+ challenge = bearer.getChallenge();
+ log.debug("NOT_ATTEMPTED: Treating as bearer only");
+ return AuthOutcome.NOT_ATTEMPTED;
+ }
+
if (log.isTraceEnabled()) {
log.trace("try oauth");
}
@@ -158,6 +166,36 @@ public abstract class RequestAuthenticator {
return false;
}
+ protected boolean isAutodetectedBearerOnly(HttpFacade.Request request) {
+ if (!deployment.isAutodetectBearerOnly()) return false;
+
+ String headerValue = facade.getRequest().getHeader("X-Requested-With");
+ if (headerValue != null && headerValue.equalsIgnoreCase("XMLHttpRequest")) {
+ return true;
+ }
+
+ headerValue = facade.getRequest().getHeader("Faces-Request");
+ if (headerValue != null && headerValue.startsWith("partial/")) {
+ return true;
+ }
+
+ headerValue = facade.getRequest().getHeader("SOAPAction");
+ if (headerValue != null) {
+ return true;
+ }
+
+ List<String> accepts = facade.getRequest().getHeaders("Accept");
+ if (accepts == null) accepts = Collections.emptyList();
+
+ for (String accept : accepts) {
+ if (accept.contains("text/html") || accept.contains("text/*") || accept.contains("*/*")) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
protected abstract OAuthRequestAuthenticator createOAuthAuthenticator();
protected BearerTokenRequestAuthenticator createBearerTokenAuthenticator() {
adapters/pom.xml 2(+1 -1)
diff --git a/adapters/pom.xml b/adapters/pom.xml
index a9179ed..105aa01 100755
--- a/adapters/pom.xml
+++ b/adapters/pom.xml
@@ -23,7 +23,7 @@
<version>2.5.0.Final-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
- <name>Keycloak Integration</name>
+ <name>Keycloak Adapters</name>
<description/>
<modelVersion>4.0.0</modelVersion>
diff --git a/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/profile/AbstractSamlAuthenticationHandler.java b/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/profile/AbstractSamlAuthenticationHandler.java
index 429d610..1bbdf6d 100644
--- a/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/profile/AbstractSamlAuthenticationHandler.java
+++ b/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/profile/AbstractSamlAuthenticationHandler.java
@@ -571,12 +571,8 @@ public abstract class AbstractSamlAuthenticationHandler implements SamlAuthentic
key = locator.getKey(keyId);
boolean keyLocated = key != null;
- if (validateRedirectBindingSignatureForKey(sigAlg, rawQueryBytes, decodedSignature, key)) {
- return true;
- }
-
if (keyLocated) {
- return false;
+ return validateRedirectBindingSignatureForKey(sigAlg, rawQueryBytes, decodedSignature, key);
}
} catch (KeyManagementException ex) {
}
diff --git a/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/SamlUtil.java b/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/SamlUtil.java
index e375e8c..b551707 100755
--- a/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/SamlUtil.java
+++ b/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/SamlUtil.java
@@ -40,7 +40,7 @@ public class SamlUtil {
httpFacade.getResponse().setHeader("Content-Type", "text/html");
httpFacade.getResponse().setHeader("Pragma", "no-cache");
httpFacade.getResponse().setHeader("Cache-Control", "no-cache, no-store");
- httpFacade.getResponse().getOutputStream().write(html.getBytes());
+ httpFacade.getResponse().getOutputStream().write(html.getBytes(GeneralConstants.SAML_CHARSET));
httpFacade.getResponse().end();
} else {
String uri = asRequest ? binding.redirectBinding(document).requestURI(actionUrl).toString() : binding.redirectBinding(document).responseURI(actionUrl).toString();
diff --git a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/aggregated/AggregatePolicyProvider.java b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/aggregated/AggregatePolicyProvider.java
index b1e668a..b0b9847 100644
--- a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/aggregated/AggregatePolicyProvider.java
+++ b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/aggregated/AggregatePolicyProvider.java
@@ -17,6 +17,8 @@
*/
package org.keycloak.authorization.policy.provider.aggregated;
+import java.util.List;
+
import org.keycloak.authorization.AuthorizationProvider;
import org.keycloak.authorization.model.Policy;
import org.keycloak.authorization.policy.evaluation.DecisionResultCollector;
@@ -26,21 +28,11 @@ import org.keycloak.authorization.policy.evaluation.Result;
import org.keycloak.authorization.policy.provider.PolicyProvider;
import org.keycloak.authorization.policy.provider.PolicyProviderFactory;
-import java.util.List;
-
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
public class AggregatePolicyProvider implements PolicyProvider {
- private final Policy policy;
- private final AuthorizationProvider authorization;
-
- public AggregatePolicyProvider(Policy policy, AuthorizationProvider authorization) {
- this.policy = policy;
- this.authorization = authorization;
- }
-
@Override
public void evaluate(Evaluation evaluation) {
//TODO: need to detect deep recursions
@@ -59,10 +51,12 @@ public class AggregatePolicyProvider implements PolicyProvider {
}
};
- this.policy.getAssociatedPolicies().forEach(associatedPolicy -> {
- PolicyProviderFactory providerFactory = authorization.getProviderFactory(associatedPolicy.getType());
- PolicyProvider policyProvider = providerFactory.create(associatedPolicy, authorization);
- policyProvider.evaluate(new DefaultEvaluation(evaluation.getPermission(), evaluation.getContext(), policy, associatedPolicy, decision));
+ Policy policy = evaluation.getPolicy();
+ AuthorizationProvider authorization = evaluation.getAuthorizationProvider();
+
+ policy.getAssociatedPolicies().forEach(associatedPolicy -> {
+ PolicyProvider policyProvider = authorization.getProvider(associatedPolicy.getType());
+ policyProvider.evaluate(new DefaultEvaluation(evaluation.getPermission(), evaluation.getContext(), policy, associatedPolicy, decision, authorization));
});
decision.onComplete();
diff --git a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/aggregated/AggregatePolicyProviderFactory.java b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/aggregated/AggregatePolicyProviderFactory.java
index 9c7faad..3e86973 100644
--- a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/aggregated/AggregatePolicyProviderFactory.java
+++ b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/aggregated/AggregatePolicyProviderFactory.java
@@ -19,7 +19,6 @@ package org.keycloak.authorization.policy.provider.aggregated;
import org.keycloak.Config;
import org.keycloak.authorization.AuthorizationProvider;
-import org.keycloak.authorization.model.Policy;
import org.keycloak.authorization.model.ResourceServer;
import org.keycloak.authorization.policy.provider.PolicyProvider;
import org.keycloak.authorization.policy.provider.PolicyProviderAdminService;
@@ -32,6 +31,8 @@ import org.keycloak.models.KeycloakSessionFactory;
*/
public class AggregatePolicyProviderFactory implements PolicyProviderFactory {
+ private AggregatePolicyProvider provider = new AggregatePolicyProvider();
+
@Override
public String getName() {
return "Aggregated";
@@ -43,8 +44,8 @@ public class AggregatePolicyProviderFactory implements PolicyProviderFactory {
}
@Override
- public PolicyProvider create(Policy policy, AuthorizationProvider authorization) {
- return new AggregatePolicyProvider(policy, authorization);
+ public PolicyProvider create(AuthorizationProvider authorization) {
+ return provider;
}
@Override
diff --git a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/client/ClientPolicyProvider.java b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/client/ClientPolicyProvider.java
index ec84bbc..5c778d8 100644
--- a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/client/ClientPolicyProvider.java
+++ b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/client/ClientPolicyProvider.java
@@ -1,5 +1,7 @@
package org.keycloak.authorization.policy.provider.client;
+import static org.keycloak.authorization.policy.provider.client.ClientPolicyProviderFactory.getClients;
+
import org.keycloak.authorization.AuthorizationProvider;
import org.keycloak.authorization.model.Policy;
import org.keycloak.authorization.policy.evaluation.Evaluation;
@@ -8,26 +10,19 @@ import org.keycloak.authorization.policy.provider.PolicyProvider;
import org.keycloak.models.ClientModel;
import org.keycloak.models.RealmModel;
-import static org.keycloak.authorization.policy.provider.client.ClientPolicyProviderFactory.getClients;
-
public class ClientPolicyProvider implements PolicyProvider {
- private final Policy policy;
- private final AuthorizationProvider authorization;
-
- public ClientPolicyProvider(Policy policy, AuthorizationProvider authorization) {
- this.policy = policy;
- this.authorization = authorization;
- }
-
@Override
public void evaluate(Evaluation evaluation) {
+ Policy policy = evaluation.getPolicy();
EvaluationContext context = evaluation.getContext();
- String[] clients = getClients(this.policy);
+ String[] clients = getClients(policy);
+ AuthorizationProvider authorizationProvider = evaluation.getAuthorizationProvider();
+ RealmModel realm = authorizationProvider.getKeycloakSession().getContext().getRealm();
if (clients.length > 0) {
for (String client : clients) {
- ClientModel clientModel = getCurrentRealm().getClientById(client);
+ ClientModel clientModel = realm.getClientById(client);
if (context.getAttributes().containsValue("kc.client.id", clientModel.getClientId())) {
evaluation.grant();
return;
@@ -40,8 +35,4 @@ public class ClientPolicyProvider implements PolicyProvider {
public void close() {
}
-
- private RealmModel getCurrentRealm() {
- return this.authorization.getKeycloakSession().getContext().getRealm();
- }
}
diff --git a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/client/ClientPolicyProviderFactory.java b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/client/ClientPolicyProviderFactory.java
index e800a5b..8cb0029 100644
--- a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/client/ClientPolicyProviderFactory.java
+++ b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/client/ClientPolicyProviderFactory.java
@@ -1,5 +1,9 @@
package org.keycloak.authorization.policy.provider.client;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
import org.keycloak.Config;
import org.keycloak.authorization.AuthorizationProvider;
import org.keycloak.authorization.model.Policy;
@@ -8,18 +12,18 @@ import org.keycloak.authorization.policy.provider.PolicyProvider;
import org.keycloak.authorization.policy.provider.PolicyProviderAdminService;
import org.keycloak.authorization.policy.provider.PolicyProviderFactory;
import org.keycloak.authorization.store.PolicyStore;
+import org.keycloak.authorization.store.ResourceServerStore;
+import org.keycloak.authorization.store.StoreFactory;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.RealmModel.ClientRemovedEvent;
import org.keycloak.util.JsonSerialization;
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.List;
-
public class ClientPolicyProviderFactory implements PolicyProviderFactory {
+ private ClientPolicyProvider provider = new ClientPolicyProvider();
+
@Override
public String getName() {
return "Client";
@@ -31,8 +35,8 @@ public class ClientPolicyProviderFactory implements PolicyProviderFactory {
}
@Override
- public PolicyProvider create(Policy policy, AuthorizationProvider authorization) {
- return new ClientPolicyProvider(policy, authorization);
+ public PolicyProvider create(AuthorizationProvider authorization) {
+ return provider;
}
@Override
@@ -56,31 +60,36 @@ public class ClientPolicyProviderFactory implements PolicyProviderFactory {
if (event instanceof ClientRemovedEvent) {
KeycloakSession keycloakSession = ((ClientRemovedEvent) event).getKeycloakSession();
AuthorizationProvider provider = keycloakSession.getProvider(AuthorizationProvider.class);
- PolicyStore policyStore = provider.getStoreFactory().getPolicyStore();
+ StoreFactory storeFactory = provider.getStoreFactory();
+ PolicyStore policyStore = storeFactory.getPolicyStore();
ClientModel removedClient = ((ClientRemovedEvent) event).getClient();
+ ResourceServerStore resourceServerStore = storeFactory.getResourceServerStore();
+ ResourceServer resourceServer = resourceServerStore.findByClient(removedClient.getId());
- policyStore.findByType(getId()).forEach(policy -> {
- List<String> clients = new ArrayList<>();
+ if (resourceServer != null) {
+ policyStore.findByType(getId(), resourceServer.getId()).forEach(policy -> {
+ List<String> clients = new ArrayList<>();
- for (String clientId : getClients(policy)) {
- if (!clientId.equals(removedClient.getId())) {
- clients.add(clientId);
+ for (String clientId : getClients(policy)) {
+ if (!clientId.equals(removedClient.getId())) {
+ clients.add(clientId);
+ }
}
- }
-
- try {
- if (clients.isEmpty()) {
- policyStore.findDependentPolicies(policy.getId()).forEach(dependentPolicy -> {
- dependentPolicy.removeAssociatedPolicy(policy);
- });
- policyStore.delete(policy.getId());
- } else {
- policy.getConfig().put("clients", JsonSerialization.writeValueAsString(clients));
+
+ try {
+ if (clients.isEmpty()) {
+ policyStore.findDependentPolicies(policy.getId(), resourceServer.getId()).forEach(dependentPolicy -> {
+ dependentPolicy.removeAssociatedPolicy(policy);
+ });
+ policyStore.delete(policy.getId());
+ } else {
+ policy.getConfig().put("clients", JsonSerialization.writeValueAsString(clients));
+ }
+ } catch (IOException e) {
+ throw new RuntimeException("Error while synchronizing clients with policy [" + policy.getName() + "].", e);
}
- } catch (IOException e) {
- throw new RuntimeException("Error while synchronizing clients with policy [" + policy.getName() + "].", e);
- }
- });
+ });
+ }
}
});
}
diff --git a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/js/JSPolicyProvider.java b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/js/JSPolicyProvider.java
index eeeb3ea..f875731 100644
--- a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/js/JSPolicyProvider.java
+++ b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/js/JSPolicyProvider.java
@@ -17,32 +17,34 @@
*/
package org.keycloak.authorization.policy.provider.js;
-import org.keycloak.authorization.model.Policy;
-import org.keycloak.authorization.policy.evaluation.Evaluation;
-import org.keycloak.authorization.policy.provider.PolicyProvider;
+import java.util.function.Supplier;
import javax.script.ScriptEngine;
-import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
+import org.keycloak.authorization.model.Policy;
+import org.keycloak.authorization.policy.evaluation.Evaluation;
+import org.keycloak.authorization.policy.provider.PolicyProvider;
+
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
public class JSPolicyProvider implements PolicyProvider {
- private final Policy policy;
+ private Supplier<ScriptEngine> engineProvider;
- public JSPolicyProvider(Policy policy) {
- this.policy = policy;
+ public JSPolicyProvider(Supplier<ScriptEngine> engineProvider) {
+ this.engineProvider = engineProvider;
}
@Override
public void evaluate(Evaluation evaluation) {
- ScriptEngineManager manager = new ScriptEngineManager();
- ScriptEngine engine = manager.getEngineByName("nashorn");
+ ScriptEngine engine = engineProvider.get();
engine.put("$evaluation", evaluation);
+ Policy policy = evaluation.getPolicy();
+
try {
engine.eval(policy.getConfig().get("code"));
} catch (ScriptException e) {
diff --git a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/js/JSPolicyProviderFactory.java b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/js/JSPolicyProviderFactory.java
index 8134d95..b4a5099 100644
--- a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/js/JSPolicyProviderFactory.java
+++ b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/js/JSPolicyProviderFactory.java
@@ -1,8 +1,12 @@
package org.keycloak.authorization.policy.provider.js;
+import java.util.function.Supplier;
+
+import javax.script.ScriptEngine;
+import javax.script.ScriptEngineManager;
+
import org.keycloak.Config;
import org.keycloak.authorization.AuthorizationProvider;
-import org.keycloak.authorization.model.Policy;
import org.keycloak.authorization.model.ResourceServer;
import org.keycloak.authorization.policy.provider.PolicyProvider;
import org.keycloak.authorization.policy.provider.PolicyProviderAdminService;
@@ -15,6 +19,13 @@ import org.keycloak.models.KeycloakSessionFactory;
*/
public class JSPolicyProviderFactory implements PolicyProviderFactory {
+ private JSPolicyProvider provider = new JSPolicyProvider(new Supplier<ScriptEngine>() {
+ @Override
+ public ScriptEngine get() {
+ return new ScriptEngineManager().getEngineByName("nashorn");
+ }
+ });
+
@Override
public String getName() {
return "JavaScript";
@@ -26,8 +37,8 @@ public class JSPolicyProviderFactory implements PolicyProviderFactory {
}
@Override
- public PolicyProvider create(Policy policy, AuthorizationProvider authorization) {
- return new JSPolicyProvider(policy);
+ public PolicyProvider create(AuthorizationProvider authorization) {
+ return provider;
}
@Override
diff --git a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/resource/ResourcePolicyProvider.java b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/resource/ResourcePolicyProvider.java
index c76a989..69e93fe 100644
--- a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/resource/ResourcePolicyProvider.java
+++ b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/resource/ResourcePolicyProvider.java
@@ -17,7 +17,6 @@
*/
package org.keycloak.authorization.policy.provider.resource;
-import org.keycloak.authorization.model.Policy;
import org.keycloak.authorization.policy.evaluation.Evaluation;
import org.keycloak.authorization.policy.provider.PolicyProvider;
@@ -26,7 +25,7 @@ import org.keycloak.authorization.policy.provider.PolicyProvider;
*/
public class ResourcePolicyProvider implements PolicyProvider {
- public ResourcePolicyProvider(Policy policy) {
+ public ResourcePolicyProvider() {
}
diff --git a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/resource/ResourcePolicyProviderFactory.java b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/resource/ResourcePolicyProviderFactory.java
index 8ea4800..d7a6b2b 100644
--- a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/resource/ResourcePolicyProviderFactory.java
+++ b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/resource/ResourcePolicyProviderFactory.java
@@ -2,7 +2,6 @@ package org.keycloak.authorization.policy.provider.resource;
import org.keycloak.Config;
import org.keycloak.authorization.AuthorizationProvider;
-import org.keycloak.authorization.model.Policy;
import org.keycloak.authorization.model.ResourceServer;
import org.keycloak.authorization.policy.provider.PolicyProvider;
import org.keycloak.authorization.policy.provider.PolicyProviderAdminService;
@@ -15,6 +14,8 @@ import org.keycloak.models.KeycloakSessionFactory;
*/
public class ResourcePolicyProviderFactory implements PolicyProviderFactory {
+ private ResourcePolicyProvider provider = new ResourcePolicyProvider();
+
@Override
public String getName() {
return "Resource-Based";
@@ -26,8 +27,8 @@ public class ResourcePolicyProviderFactory implements PolicyProviderFactory {
}
@Override
- public PolicyProvider create(Policy policy, AuthorizationProvider authorization) {
- return new ResourcePolicyProvider(policy);
+ public PolicyProvider create(AuthorizationProvider authorization) {
+ return provider;
}
@Override
diff --git a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/role/RolePolicyProvider.java b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/role/RolePolicyProvider.java
index 9fb9787..4aafedd 100644
--- a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/role/RolePolicyProvider.java
+++ b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/role/RolePolicyProvider.java
@@ -17,6 +17,10 @@
*/
package org.keycloak.authorization.policy.provider.role;
+import static org.keycloak.authorization.policy.provider.role.RolePolicyProviderFactory.getRoles;
+
+import java.util.Map;
+
import org.keycloak.authorization.AuthorizationProvider;
import org.keycloak.authorization.identity.Identity;
import org.keycloak.authorization.model.Policy;
@@ -26,39 +30,26 @@ import org.keycloak.models.ClientModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel;
-import java.util.Map;
-
-import static org.keycloak.authorization.policy.provider.role.RolePolicyProviderFactory.getRoles;
-
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
public class RolePolicyProvider implements PolicyProvider {
- private final Policy policy;
- private final AuthorizationProvider authorization;
-
- public RolePolicyProvider(Policy policy, AuthorizationProvider authorization) {
- this.policy = policy;
- this.authorization = authorization;
- }
-
- public RolePolicyProvider() {
- this(null, null);
- }
-
@Override
public void evaluate(Evaluation evaluation) {
- Map<String, Object>[] roleIds = getRoles(this.policy);
+ Policy policy = evaluation.getPolicy();
+ Map<String, Object>[] roleIds = getRoles(policy);
+ AuthorizationProvider authorizationProvider = evaluation.getAuthorizationProvider();
+ RealmModel realm = authorizationProvider.getKeycloakSession().getContext().getRealm();
if (roleIds.length > 0) {
Identity identity = evaluation.getContext().getIdentity();
for (Map<String, Object> current : roleIds) {
- RoleModel role = getCurrentRealm().getRoleById((String) current.get("id"));
+ RoleModel role = realm.getRoleById((String) current.get("id"));
if (role != null) {
- boolean hasRole = hasRole(identity, role);
+ boolean hasRole = hasRole(identity, role, realm);
if (!hasRole && Boolean.valueOf(isRequired(current))) {
evaluation.deny();
@@ -75,19 +66,15 @@ public class RolePolicyProvider implements PolicyProvider {
return (boolean) current.getOrDefault("required", false);
}
- private boolean hasRole(Identity identity, RoleModel role) {
+ private boolean hasRole(Identity identity, RoleModel role, RealmModel realm) {
String roleName = role.getName();
if (role.isClientRole()) {
- ClientModel clientModel = getCurrentRealm().getClientById(role.getContainerId());
+ ClientModel clientModel = realm.getClientById(role.getContainerId());
return identity.hasClientRole(clientModel.getClientId(), roleName);
}
return identity.hasRealmRole(roleName);
}
- private RealmModel getCurrentRealm() {
- return this.authorization.getKeycloakSession().getContext().getRealm();
- }
-
@Override
public void close() {
diff --git a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/role/RolePolicyProviderFactory.java b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/role/RolePolicyProviderFactory.java
index 67de87a..33db2d5 100644
--- a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/role/RolePolicyProviderFactory.java
+++ b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/role/RolePolicyProviderFactory.java
@@ -26,8 +26,13 @@ import org.keycloak.authorization.policy.provider.PolicyProvider;
import org.keycloak.authorization.policy.provider.PolicyProviderAdminService;
import org.keycloak.authorization.policy.provider.PolicyProviderFactory;
import org.keycloak.authorization.store.PolicyStore;
+import org.keycloak.authorization.store.ResourceServerStore;
+import org.keycloak.authorization.store.StoreFactory;
+import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.RoleContainerModel;
import org.keycloak.models.RoleContainerModel.RoleRemovedEvent;
import org.keycloak.models.RoleModel;
import org.keycloak.util.JsonSerialization;
@@ -37,12 +42,15 @@ import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.function.Consumer;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
public class RolePolicyProviderFactory implements PolicyProviderFactory {
+ private RolePolicyProvider provider = new RolePolicyProvider();
+
@Override
public String getName() {
return "Role";
@@ -54,8 +62,8 @@ public class RolePolicyProviderFactory implements PolicyProviderFactory {
}
@Override
- public PolicyProvider create(Policy policy, AuthorizationProvider authorization) {
- return new RolePolicyProvider(policy, authorization);
+ public PolicyProvider create(AuthorizationProvider authorization) {
+ return provider;
}
@Override
@@ -79,41 +87,61 @@ public class RolePolicyProviderFactory implements PolicyProviderFactory {
if (event instanceof RoleRemovedEvent) {
KeycloakSession keycloakSession = ((RoleRemovedEvent) event).getKeycloakSession();
AuthorizationProvider provider = keycloakSession.getProvider(AuthorizationProvider.class);
- PolicyStore policyStore = provider.getStoreFactory().getPolicyStore();
+ StoreFactory storeFactory = provider.getStoreFactory();
+ PolicyStore policyStore = storeFactory.getPolicyStore();
RoleModel removedRole = ((RoleRemovedEvent) event).getRole();
+ RoleContainerModel container = removedRole.getContainer();
+ ResourceServerStore resourceServerStore = storeFactory.getResourceServerStore();
+
+ if (container instanceof RealmModel) {
+ RealmModel realm = (RealmModel) container;
+ realm.getClients().forEach(clientModel -> updateResourceServer(clientModel, removedRole, resourceServerStore, policyStore));
+ } else {
+ ClientModel clientModel = (ClientModel) container;
+ updateResourceServer(clientModel, removedRole, resourceServerStore, policyStore);
+ }
+ }
+ });
+ }
- policyStore.findByType(getId()).forEach(policy -> {
- List<Map> roles = new ArrayList<>();
+ private void updateResourceServer(ClientModel clientModel, RoleModel removedRole, ResourceServerStore resourceServerStore, PolicyStore policyStore) {
+ ResourceServer resourceServer = resourceServerStore.findByClient(clientModel.getId());
- for (Map<String,Object> role : getRoles(policy)) {
- if (!role.get("id").equals(removedRole.getId())) {
- Map updated = new HashMap();
- updated.put("id", role.get("id"));
- Object required = role.get("required");
- if (required != null) {
- updated.put("required", required);
- }
- roles.add(updated);
- }
- }
+ if (resourceServer != null) {
+ policyStore.findByType(getId(), resourceServer.getId()).forEach(policy -> {
+ List<Map> roles = new ArrayList<>();
- try {
- if (roles.isEmpty()) {
- policyStore.findDependentPolicies(policy.getId()).forEach(dependentPolicy -> {
- dependentPolicy.removeAssociatedPolicy(policy);
- });
- policyStore.delete(policy.getId());
- } else {
- Map<String, String> config = policy.getConfig();
- config.put("roles", JsonSerialization.writeValueAsString(roles));
- policy.setConfig(config);
+ for (Map<String,Object> role : getRoles(policy)) {
+ if (!role.get("id").equals(removedRole.getId())) {
+ Map updated = new HashMap();
+ updated.put("id", role.get("id"));
+ Object required = role.get("required");
+ if (required != null) {
+ updated.put("required", required);
}
- } catch (IOException e) {
- throw new RuntimeException("Error while synchronizing roles with policy [" + policy.getName() + "].", e);
+ roles.add(updated);
}
- });
- }
- });
+ }
+
+ try {
+ if (roles.isEmpty()) {
+ policyStore.findDependentPolicies(policy.getId(), resourceServer.getId()).forEach(dependentPolicy -> {
+ dependentPolicy.removeAssociatedPolicy(policy);
+ if (dependentPolicy.getAssociatedPolicies().isEmpty()) {
+ policyStore.delete(dependentPolicy.getId());
+ }
+ });
+ policyStore.delete(policy.getId());
+ } else {
+ Map<String, String> config = policy.getConfig();
+ config.put("roles", JsonSerialization.writeValueAsString(roles));
+ policy.setConfig(config);
+ }
+ } catch (IOException e) {
+ throw new RuntimeException("Error while synchronizing roles with policy [" + policy.getName() + "].", e);
+ }
+ });
+ }
}
@Override
diff --git a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/scope/ScopePolicyProvider.java b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/scope/ScopePolicyProvider.java
index 5c10cc3..0a8cf85 100644
--- a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/scope/ScopePolicyProvider.java
+++ b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/scope/ScopePolicyProvider.java
@@ -17,7 +17,6 @@
*/
package org.keycloak.authorization.policy.provider.scope;
-import org.keycloak.authorization.model.Policy;
import org.keycloak.authorization.policy.evaluation.Evaluation;
import org.keycloak.authorization.policy.provider.PolicyProvider;
@@ -26,12 +25,6 @@ import org.keycloak.authorization.policy.provider.PolicyProvider;
*/
public class ScopePolicyProvider implements PolicyProvider {
- private final Policy policy;
-
- public ScopePolicyProvider(Policy policy) {
- this.policy = policy;
- }
-
@Override
public void evaluate(Evaluation evaluation) {
}
diff --git a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/scope/ScopePolicyProviderFactory.java b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/scope/ScopePolicyProviderFactory.java
index 6ed0cd5..0678eb3 100644
--- a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/scope/ScopePolicyProviderFactory.java
+++ b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/scope/ScopePolicyProviderFactory.java
@@ -2,7 +2,6 @@ package org.keycloak.authorization.policy.provider.scope;
import org.keycloak.Config;
import org.keycloak.authorization.AuthorizationProvider;
-import org.keycloak.authorization.model.Policy;
import org.keycloak.authorization.model.ResourceServer;
import org.keycloak.authorization.policy.provider.PolicyProvider;
import org.keycloak.authorization.policy.provider.PolicyProviderAdminService;
@@ -15,6 +14,8 @@ import org.keycloak.models.KeycloakSessionFactory;
*/
public class ScopePolicyProviderFactory implements PolicyProviderFactory {
+ private ScopePolicyProvider provider = new ScopePolicyProvider();
+
@Override
public String getName() {
return "Scope-Based";
@@ -26,8 +27,8 @@ public class ScopePolicyProviderFactory implements PolicyProviderFactory {
}
@Override
- public PolicyProvider create(Policy policy, AuthorizationProvider authorization) {
- return new ScopePolicyProvider(policy);
+ public PolicyProvider create(AuthorizationProvider authorization) {
+ return provider;
}
@Override
diff --git a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/time/TimePolicyProvider.java b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/time/TimePolicyProvider.java
index 84ba1a2..7ce4c6e 100644
--- a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/time/TimePolicyProvider.java
+++ b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/time/TimePolicyProvider.java
@@ -17,14 +17,14 @@
*/
package org.keycloak.authorization.policy.provider.time;
-import org.keycloak.authorization.model.Policy;
-import org.keycloak.authorization.policy.evaluation.Evaluation;
-import org.keycloak.authorization.policy.provider.PolicyProvider;
-
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
+import org.keycloak.authorization.model.Policy;
+import org.keycloak.authorization.policy.evaluation.Evaluation;
+import org.keycloak.authorization.policy.provider.PolicyProvider;
+
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
@@ -32,51 +32,51 @@ public class TimePolicyProvider implements PolicyProvider {
static String DEFAULT_DATE_PATTERN = "yyyy-MM-dd hh:mm:ss";
- private final Policy policy;
private final SimpleDateFormat dateFormat;
private final Date currentDate;
- public TimePolicyProvider(Policy policy) {
- this.policy = policy;
+ public TimePolicyProvider() {
this.dateFormat = new SimpleDateFormat(DEFAULT_DATE_PATTERN);
this.currentDate = new Date();
}
@Override
public void evaluate(Evaluation evaluation) {
+ Policy policy = evaluation.getPolicy();
+
try {
- String notBefore = this.policy.getConfig().get("nbf");
- if (notBefore != null) {
+ String notBefore = policy.getConfig().get("nbf");
+ if (notBefore != null && !"".equals(notBefore)) {
if (this.currentDate.before(this.dateFormat.parse(format(notBefore)))) {
evaluation.deny();
return;
}
}
- String notOnOrAfter = this.policy.getConfig().get("noa");
- if (notOnOrAfter != null) {
+ String notOnOrAfter = policy.getConfig().get("noa");
+ if (notOnOrAfter != null && !"".equals(notOnOrAfter)) {
if (this.currentDate.after(this.dateFormat.parse(format(notOnOrAfter)))) {
evaluation.deny();
return;
}
}
- if (isInvalid(Calendar.DAY_OF_MONTH, "dayMonth")
- || isInvalid(Calendar.MONTH, "month")
- || isInvalid(Calendar.YEAR, "year")
- || isInvalid(Calendar.HOUR_OF_DAY, "hour")
- || isInvalid(Calendar.MINUTE, "minute")) {
+ if (isInvalid(Calendar.DAY_OF_MONTH, "dayMonth", policy)
+ || isInvalid(Calendar.MONTH, "month", policy)
+ || isInvalid(Calendar.YEAR, "year", policy)
+ || isInvalid(Calendar.HOUR_OF_DAY, "hour", policy)
+ || isInvalid(Calendar.MINUTE, "minute", policy)) {
evaluation.deny();
return;
}
evaluation.grant();
} catch (Exception e) {
- throw new RuntimeException("Could not evaluate time-based policy [" + this.policy.getName() + "].", e);
+ throw new RuntimeException("Could not evaluate time-based policy [" + policy.getName() + "].", e);
}
}
- private boolean isInvalid(int timeConstant, String configName) {
+ private boolean isInvalid(int timeConstant, String configName, Policy policy) {
Calendar calendar = Calendar.getInstance();
calendar.setTime(this.currentDate);
@@ -87,9 +87,9 @@ public class TimePolicyProvider implements PolicyProvider {
dateField++;
}
- String start = this.policy.getConfig().get(configName);
+ String start = policy.getConfig().get(configName);
if (start != null) {
- String end = this.policy.getConfig().get(configName + "End");
+ String end = policy.getConfig().get(configName + "End");
if (end != null) {
if (dateField < Integer.parseInt(start) || dateField > Integer.parseInt(end)) {
return true;
diff --git a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/time/TimePolicyProviderFactory.java b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/time/TimePolicyProviderFactory.java
index efe3cd2..94c5aad 100644
--- a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/time/TimePolicyProviderFactory.java
+++ b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/time/TimePolicyProviderFactory.java
@@ -2,7 +2,6 @@ package org.keycloak.authorization.policy.provider.time;
import org.keycloak.Config;
import org.keycloak.authorization.AuthorizationProvider;
-import org.keycloak.authorization.model.Policy;
import org.keycloak.authorization.model.ResourceServer;
import org.keycloak.authorization.policy.provider.PolicyProvider;
import org.keycloak.authorization.policy.provider.PolicyProviderAdminService;
@@ -15,6 +14,8 @@ import org.keycloak.models.KeycloakSessionFactory;
*/
public class TimePolicyProviderFactory implements PolicyProviderFactory {
+ private TimePolicyProvider provider = new TimePolicyProvider();
+
@Override
public String getName() {
return "Time";
@@ -26,8 +27,8 @@ public class TimePolicyProviderFactory implements PolicyProviderFactory {
}
@Override
- public PolicyProvider create(Policy policy, AuthorizationProvider authorization) {
- return new TimePolicyProvider(policy);
+ public PolicyProvider create(AuthorizationProvider authorization) {
+ return provider;
}
@Override
diff --git a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/user/UserPolicyProvider.java b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/user/UserPolicyProvider.java
index a6fc0a4..2f77106 100644
--- a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/user/UserPolicyProvider.java
+++ b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/user/UserPolicyProvider.java
@@ -29,16 +29,11 @@ import static org.keycloak.authorization.policy.provider.user.UserPolicyProvider
*/
public class UserPolicyProvider implements PolicyProvider {
- private final Policy policy;
-
- public UserPolicyProvider(Policy policy) {
- this.policy = policy;
- }
-
@Override
public void evaluate(Evaluation evaluation) {
+ Policy policy = evaluation.getPolicy();
EvaluationContext context = evaluation.getContext();
- String[] userIds = getUsers(this.policy);
+ String[] userIds = getUsers(policy);
if (userIds.length > 0) {
for (String userId : userIds) {
diff --git a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/user/UserPolicyProviderFactory.java b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/user/UserPolicyProviderFactory.java
index fdeeac0..09345ec 100644
--- a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/user/UserPolicyProviderFactory.java
+++ b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/user/UserPolicyProviderFactory.java
@@ -18,6 +18,10 @@
package org.keycloak.authorization.policy.provider.user;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
import org.keycloak.Config;
import org.keycloak.authorization.AuthorizationProvider;
import org.keycloak.authorization.model.Policy;
@@ -26,21 +30,22 @@ import org.keycloak.authorization.policy.provider.PolicyProvider;
import org.keycloak.authorization.policy.provider.PolicyProviderAdminService;
import org.keycloak.authorization.policy.provider.PolicyProviderFactory;
import org.keycloak.authorization.store.PolicyStore;
+import org.keycloak.authorization.store.ResourceServerStore;
+import org.keycloak.authorization.store.StoreFactory;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
+import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserModel.UserRemovedEvent;
import org.keycloak.util.JsonSerialization;
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.List;
-
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
public class UserPolicyProviderFactory implements PolicyProviderFactory {
+ private UserPolicyProvider provider = new UserPolicyProvider();
+
@Override
public String getName() {
return "User";
@@ -52,8 +57,8 @@ public class UserPolicyProviderFactory implements PolicyProviderFactory {
}
@Override
- public PolicyProvider create(Policy policy, AuthorizationProvider authorization) {
- return new UserPolicyProvider(policy);
+ public PolicyProvider create(AuthorizationProvider authorization) {
+ return provider;
}
@Override
@@ -77,29 +82,40 @@ public class UserPolicyProviderFactory implements PolicyProviderFactory {
if (event instanceof UserRemovedEvent) {
KeycloakSession keycloakSession = ((UserRemovedEvent) event).getKeycloakSession();
AuthorizationProvider provider = keycloakSession.getProvider(AuthorizationProvider.class);
- PolicyStore policyStore = provider.getStoreFactory().getPolicyStore();
+ StoreFactory storeFactory = provider.getStoreFactory();
+ PolicyStore policyStore = storeFactory.getPolicyStore();
UserModel removedUser = ((UserRemovedEvent) event).getUser();
-
- policyStore.findByType(getId()).forEach(policy -> {
- List<String> users = new ArrayList<>();
-
- for (String userId : getUsers(policy)) {
- if (!userId.equals(removedUser.getId())) {
- users.add(userId);
- }
- }
-
- try {
- if (users.isEmpty()) {
- policyStore.findDependentPolicies(policy.getId()).forEach(dependentPolicy -> {
- dependentPolicy.removeAssociatedPolicy(policy);
- });
- policyStore.delete(policy.getId());
- } else {
- policy.getConfig().put("users", JsonSerialization.writeValueAsString(users));
- }
- } catch (IOException e) {
- throw new RuntimeException("Error while synchronizing users with policy [" + policy.getName() + "].", e);
+ RealmModel realm = ((UserRemovedEvent) event).getRealm();
+ ResourceServerStore resourceServerStore = storeFactory.getResourceServerStore();
+ realm.getClients().forEach(clientModel -> {
+ ResourceServer resourceServer = resourceServerStore.findByClient(clientModel.getId());
+
+ if (resourceServer != null) {
+ policyStore.findByType(getId(), resourceServer.getId()).forEach(policy -> {
+ List<String> users = new ArrayList<>();
+
+ for (String userId : getUsers(policy)) {
+ if (!userId.equals(removedUser.getId())) {
+ users.add(userId);
+ }
+ }
+
+ try {
+ if (users.isEmpty()) {
+ policyStore.findDependentPolicies(policy.getId(), resourceServer.getId()).forEach(dependentPolicy -> {
+ dependentPolicy.removeAssociatedPolicy(policy);
+ if (dependentPolicy.getAssociatedPolicies().isEmpty()) {
+ policyStore.delete(dependentPolicy.getId());
+ }
+ });
+ policyStore.delete(policy.getId());
+ } else {
+ policy.getConfig().put("users", JsonSerialization.writeValueAsString(users));
+ }
+ } catch (IOException e) {
+ throw new RuntimeException("Error while synchronizing users with policy [" + policy.getName() + "].", e);
+ }
+ });
}
});
}
diff --git a/authz/policy/drools/src/main/java/org/keycloak/authorization/policy/provider/drools/DroolsPolicyProvider.java b/authz/policy/drools/src/main/java/org/keycloak/authorization/policy/provider/drools/DroolsPolicyProvider.java
index c53e361..77023f5 100644
--- a/authz/policy/drools/src/main/java/org/keycloak/authorization/policy/provider/drools/DroolsPolicyProvider.java
+++ b/authz/policy/drools/src/main/java/org/keycloak/authorization/policy/provider/drools/DroolsPolicyProvider.java
@@ -17,6 +17,9 @@
*/
package org.keycloak.authorization.policy.provider.drools;
+import java.util.function.Function;
+
+import org.keycloak.authorization.model.Policy;
import org.keycloak.authorization.policy.evaluation.Evaluation;
import org.keycloak.authorization.policy.provider.PolicyProvider;
@@ -25,15 +28,15 @@ import org.keycloak.authorization.policy.provider.PolicyProvider;
*/
public class DroolsPolicyProvider implements PolicyProvider {
- private final DroolsPolicy policy;
+ private final Function<Policy, DroolsPolicy> policy;
- public DroolsPolicyProvider(DroolsPolicy policy) {
- this.policy = policy;
+ public DroolsPolicyProvider(Function<Policy, DroolsPolicy> policyProvider) {
+ this.policy = policyProvider;
}
@Override
- public void evaluate(Evaluation evaluationt) {
- this.policy.evaluate(evaluationt);
+ public void evaluate(Evaluation evaluation) {
+ policy.apply(evaluation.getPolicy()).evaluate(evaluation);
}
@Override
diff --git a/authz/policy/drools/src/main/java/org/keycloak/authorization/policy/provider/drools/DroolsPolicyProviderFactory.java b/authz/policy/drools/src/main/java/org/keycloak/authorization/policy/provider/drools/DroolsPolicyProviderFactory.java
index b3305ae..74ed89d 100644
--- a/authz/policy/drools/src/main/java/org/keycloak/authorization/policy/provider/drools/DroolsPolicyProviderFactory.java
+++ b/authz/policy/drools/src/main/java/org/keycloak/authorization/policy/provider/drools/DroolsPolicyProviderFactory.java
@@ -1,5 +1,9 @@
package org.keycloak.authorization.policy.provider.drools;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
import org.keycloak.Config;
import org.keycloak.authorization.AuthorizationProvider;
import org.keycloak.authorization.model.Policy;
@@ -9,24 +13,25 @@ import org.keycloak.authorization.policy.provider.PolicyProviderAdminService;
import org.keycloak.authorization.policy.provider.PolicyProviderFactory;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
-import org.keycloak.models.utils.PostMigrationEvent;
-import org.keycloak.provider.ProviderEvent;
-import org.keycloak.provider.ProviderEventListener;
-import org.keycloak.provider.ProviderFactory;
import org.kie.api.KieServices;
import org.kie.api.KieServices.Factory;
import org.kie.api.runtime.KieContainer;
-import java.util.HashMap;
-import java.util.Map;
-
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
public class DroolsPolicyProviderFactory implements PolicyProviderFactory {
private KieServices ks;
- private final Map<String, DroolsPolicy> containers = new HashMap<>();
+ private final Map<String, DroolsPolicy> containers = Collections.synchronizedMap(new HashMap<>());
+ private DroolsPolicyProvider provider = new DroolsPolicyProvider(policy -> {
+ if (!containers.containsKey(policy.getId())) {
+ synchronized (containers) {
+ update(policy);
+ }
+ }
+ return containers.get(policy.getId());
+ });
@Override
public String getName() {
@@ -39,12 +44,8 @@ public class DroolsPolicyProviderFactory implements PolicyProviderFactory {
}
@Override
- public PolicyProvider create(Policy policy, AuthorizationProvider authorization) {
- if (!this.containers.containsKey(policy.getId())) {
- update(policy);
- }
-
- return new DroolsPolicyProvider(this.containers.get(policy.getId()));
+ public PolicyProvider create(AuthorizationProvider authorization) {
+ return provider;
}
@Override
@@ -64,19 +65,6 @@ public class DroolsPolicyProviderFactory implements PolicyProviderFactory {
@Override
public void postInit(KeycloakSessionFactory factory) {
- factory.register(new ProviderEventListener() {
-
- @Override
- public void onEvent(ProviderEvent event) {
- // Ensure the initialization is done after DB upgrade is finished
- if (event instanceof PostMigrationEvent) {
- ProviderFactory<AuthorizationProvider> providerFactory = factory.getProviderFactory(AuthorizationProvider.class);
- AuthorizationProvider authorization = providerFactory.create(factory.create());
- authorization.getStoreFactory().getPolicyStore().findByType(getId()).forEach(DroolsPolicyProviderFactory.this::update);
- }
- }
-
- });
}
@Override
diff --git a/common/src/main/java/org/keycloak/common/Profile.java b/common/src/main/java/org/keycloak/common/Profile.java
index ac16874..91b0a80 100755
--- a/common/src/main/java/org/keycloak/common/Profile.java
+++ b/common/src/main/java/org/keycloak/common/Profile.java
@@ -19,7 +19,14 @@ package org.keycloak.common;
import java.io.File;
import java.io.FileInputStream;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
import java.util.Properties;
+import java.util.Set;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@@ -27,43 +34,87 @@ import java.util.Properties;
*/
public class Profile {
+ public enum Feature {
+ AUTHORIZATION, IMPERSONATION, SCRIPTS
+ }
+
private enum ProfileValue {
- PRODUCT, PREVIEW, COMMUNITY
+ PRODUCT(Feature.AUTHORIZATION, Feature.SCRIPTS),
+ PREVIEW,
+ COMMUNITY;
+
+ private List<Feature> disabled;
+
+ ProfileValue() {
+ this.disabled = Collections.emptyList();
+ }
+
+ ProfileValue(Feature... disabled) {
+ this.disabled = Arrays.asList(disabled);
+ }
}
- private static ProfileValue value = load();
+ private static final Profile CURRENT = new Profile();
- static ProfileValue load() {
- String profile = null;
+ private final ProfileValue profile;
+
+ private final Set<Feature> disabledFeatures = new HashSet<>();
+
+ private Profile() {
try {
- profile = System.getProperty("keycloak.profile");
- if (profile == null) {
- String jbossServerConfigDir = System.getProperty("jboss.server.config.dir");
- if (jbossServerConfigDir != null) {
- File file = new File(jbossServerConfigDir, "profile.properties");
- if (file.isFile()) {
- Properties props = new Properties();
- props.load(new FileInputStream(file));
- profile = props.getProperty("profile");
+ Properties props = new Properties();
+
+ String jbossServerConfigDir = System.getProperty("jboss.server.config.dir");
+ if (jbossServerConfigDir != null) {
+ File file = new File(jbossServerConfigDir, "profile.properties");
+ if (file.isFile()) {
+ props.load(new FileInputStream(file));
+ }
+ }
+
+ if (System.getProperties().containsKey("keycloak.profile")) {
+ props.setProperty("profile", System.getProperty("keycloak.profile"));
+ }
+
+ for (String k : System.getProperties().stringPropertyNames()) {
+ if (k.startsWith("keycloak.profile.feature.")) {
+ props.put(k.replace("keycloak.profile.feature.", "feature."), System.getProperty(k));
+ }
+ }
+
+ if (props.containsKey("profile")) {
+ profile = ProfileValue.valueOf(props.getProperty("profile").toUpperCase());
+ } else {
+ profile = ProfileValue.valueOf(Version.DEFAULT_PROFILE.toUpperCase());
+ }
+
+ disabledFeatures.addAll(profile.disabled);
+
+ for (String k : props.stringPropertyNames()) {
+ if (k.startsWith("feature.")) {
+ Feature f = Feature.valueOf(k.replace("feature.", "").toUpperCase());
+ if (props.get(k).equals("enabled")) {
+ disabledFeatures.remove(f);
+ } else if (props.get(k).equals("disabled")) {
+ disabledFeatures.add(f);
}
}
}
} catch (Exception e) {
- }
-
- if (profile == null) {
- return ProfileValue.valueOf(Version.DEFAULT_PROFILE.toUpperCase());
- } else {
- return ProfileValue.valueOf(profile.toUpperCase());
+ throw new RuntimeException(e);
}
}
public static String getName() {
- return value.name().toLowerCase();
+ return CURRENT.profile.name().toLowerCase();
+ }
+
+ public static Set<Feature> getDisabledFeatures() {
+ return CURRENT.disabledFeatures;
}
- public static boolean isPreviewEnabled() {
- return value.ordinal() >= ProfileValue.PREVIEW.ordinal();
+ public static boolean isFeatureEnabled(Feature feature) {
+ return !CURRENT.disabledFeatures.contains(feature);
}
}
diff --git a/common/src/main/java/org/keycloak/common/util/KeyUtils.java b/common/src/main/java/org/keycloak/common/util/KeyUtils.java
index a49b9ab..4514e4f 100644
--- a/common/src/main/java/org/keycloak/common/util/KeyUtils.java
+++ b/common/src/main/java/org/keycloak/common/util/KeyUtils.java
@@ -17,6 +17,8 @@
package org.keycloak.common.util;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.SecretKeySpec;
import java.security.Key;
import java.security.KeyFactory;
import java.security.KeyPair;
@@ -38,6 +40,10 @@ public class KeyUtils {
private KeyUtils() {
}
+ public static SecretKey loadSecretKey(String secret) {
+ return new SecretKeySpec(secret.getBytes(), "HmacSHA256");
+ }
+
public static KeyPair generateRsaKeyPair(int keysize) {
try {
KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA");
diff --git a/common/src/main/java/org/keycloak/common/util/StreamUtil.java b/common/src/main/java/org/keycloak/common/util/StreamUtil.java
index e1f87b9..72ff6df 100755
--- a/common/src/main/java/org/keycloak/common/util/StreamUtil.java
+++ b/common/src/main/java/org/keycloak/common/util/StreamUtil.java
@@ -21,6 +21,7 @@ import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
+import java.nio.charset.Charset;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@@ -28,18 +29,41 @@ import java.io.InputStreamReader;
*/
public final class StreamUtil {
+ private static final int BUFFER_LENGTH = 4096;
+
private StreamUtil() {
}
+ /**
+ * Reads string from byte input stream.
+ * @param in InputStream to build the String from
+ * @return String representation of the input stream contents decoded using default charset
+ * @throws IOException
+ * @deprecated Use {@link #readString(java.io.InputStream, java.nio.charset.Charset)} variant.
+ */
+ @Deprecated
public static String readString(InputStream in) throws IOException
{
- char[] buffer = new char[1024];
+ return readString(in, Charset.defaultCharset());
+ }
+
+ /**
+ * Reads string from byte input stream.
+ * @param in InputStream to build the String from
+ * @param charset Charset used to decode the input stream
+ * @return String representation of the input stream contents decoded using given charset
+ * @throws IOException
+ * @deprecated Use {@link #readString(java.io.InputStream, java.nio.charset.Charset)} variant.
+ */
+ public static String readString(InputStream in, Charset charset) throws IOException
+ {
+ char[] buffer = new char[BUFFER_LENGTH];
StringBuilder builder = new StringBuilder();
- BufferedReader reader = new BufferedReader(new InputStreamReader(in));
+ BufferedReader reader = new BufferedReader(new InputStreamReader(in, charset));
int wasRead;
do
{
- wasRead = reader.read(buffer, 0, 1024);
+ wasRead = reader.read(buffer, 0, BUFFER_LENGTH);
if (wasRead > 0)
{
builder.append(buffer, 0, wasRead);
diff --git a/core/src/main/java/org/keycloak/AuthorizationContext.java b/core/src/main/java/org/keycloak/AuthorizationContext.java
index a14594b..93f3ff1 100644
--- a/core/src/main/java/org/keycloak/AuthorizationContext.java
+++ b/core/src/main/java/org/keycloak/AuthorizationContext.java
@@ -24,6 +24,7 @@ import org.keycloak.representations.idm.authorization.Permission;
import java.util.Collections;
import java.util.List;
+import java.util.Map;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
@@ -31,10 +32,10 @@ import java.util.List;
public class AuthorizationContext {
private final AccessToken authzToken;
- private final List<PathConfig> paths;
+ private final Map<String, PathConfig> paths;
private boolean granted;
- public AuthorizationContext(AccessToken authzToken, List<PathConfig> paths) {
+ public AuthorizationContext(AccessToken authzToken, Map<String, PathConfig> paths) {
this.authzToken = authzToken;
this.paths = paths;
this.granted = true;
@@ -57,7 +58,7 @@ public class AuthorizationContext {
}
for (Permission permission : authorization.getPermissions()) {
- for (PathConfig pathHolder : this.paths) {
+ for (PathConfig pathHolder : this.paths.values()) {
if (pathHolder.getName().equals(resourceName)) {
if (pathHolder.getId().equals(permission.getResourceSetId())) {
if (permission.getScopes().contains(scopeName)) {
@@ -83,7 +84,7 @@ public class AuthorizationContext {
}
for (Permission permission : authorization.getPermissions()) {
- for (PathConfig pathHolder : this.paths) {
+ for (PathConfig pathHolder : this.paths.values()) {
if (pathHolder.getName().equals(resourceName)) {
if (pathHolder.getId().equals(permission.getResourceSetId())) {
return true;
diff --git a/core/src/main/java/org/keycloak/jose/jws/Algorithm.java b/core/src/main/java/org/keycloak/jose/jws/Algorithm.java
index 2772a18..60aa7ac 100755
--- a/core/src/main/java/org/keycloak/jose/jws/Algorithm.java
+++ b/core/src/main/java/org/keycloak/jose/jws/Algorithm.java
@@ -26,23 +26,30 @@ import org.keycloak.jose.jws.crypto.SignatureProvider;
*/
public enum Algorithm {
- none(null),
- HS256(null),
- HS384(null),
- HS512(null),
- RS256(new RSAProvider()),
- RS384(new RSAProvider()),
- RS512(new RSAProvider()),
- ES256(null),
- ES384(null),
- ES512(null)
+ none(null, null),
+ HS256(AlgorithmType.HMAC, null),
+ HS384(AlgorithmType.HMAC, null),
+ HS512(AlgorithmType.HMAC, null),
+ RS256(AlgorithmType.RSA, new RSAProvider()),
+ RS384(AlgorithmType.RSA, new RSAProvider()),
+ RS512(AlgorithmType.RSA, new RSAProvider()),
+ ES256(AlgorithmType.ECDSA, null),
+ ES384(AlgorithmType.ECDSA, null),
+ ES512(AlgorithmType.ECDSA, null)
;
+
+ private AlgorithmType type;
private SignatureProvider provider;
- Algorithm(SignatureProvider provider) {
+ Algorithm(AlgorithmType type, SignatureProvider provider) {
+ this.type = type;
this.provider = provider;
}
+ public AlgorithmType getType() {
+ return type;
+ }
+
public SignatureProvider getProvider() {
return provider;
}
diff --git a/core/src/main/java/org/keycloak/jose/jws/AlgorithmType.java b/core/src/main/java/org/keycloak/jose/jws/AlgorithmType.java
new file mode 100755
index 0000000..6c8d93f
--- /dev/null
+++ b/core/src/main/java/org/keycloak/jose/jws/AlgorithmType.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.jose.jws;
+
+/**
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public enum AlgorithmType {
+
+ RSA,
+ HMAC,
+ ECDSA
+
+}
diff --git a/core/src/main/java/org/keycloak/representations/adapters/config/AdapterConfig.java b/core/src/main/java/org/keycloak/representations/adapters/config/AdapterConfig.java
index e4065bc..0a107bb 100755
--- a/core/src/main/java/org/keycloak/representations/adapters/config/AdapterConfig.java
+++ b/core/src/main/java/org/keycloak/representations/adapters/config/AdapterConfig.java
@@ -30,7 +30,7 @@ import com.fasterxml.jackson.annotation.JsonPropertyOrder;
"resource", "public-client", "credentials",
"use-resource-role-mappings",
"enable-cors", "cors-max-age", "cors-allowed-methods",
- "expose-token", "bearer-only",
+ "expose-token", "bearer-only", "autodetect-bearer-only",
"connection-pool-size",
"allow-any-hostname", "disable-trust-manager", "truststore", "truststore-password",
"client-keystore", "client-keystore-password", "client-key-password",
diff --git a/core/src/main/java/org/keycloak/representations/adapters/config/BaseAdapterConfig.java b/core/src/main/java/org/keycloak/representations/adapters/config/BaseAdapterConfig.java
index f9138a1..3cef2a0 100755
--- a/core/src/main/java/org/keycloak/representations/adapters/config/BaseAdapterConfig.java
+++ b/core/src/main/java/org/keycloak/representations/adapters/config/BaseAdapterConfig.java
@@ -33,7 +33,7 @@ import java.util.Map;
"resource", "public-client", "credentials",
"use-resource-role-mappings",
"enable-cors", "cors-max-age", "cors-allowed-methods",
- "expose-token", "bearer-only", "enable-basic-auth"})
+ "expose-token", "bearer-only", "autodetect-bearer-only", "enable-basic-auth"})
public class BaseAdapterConfig extends BaseRealmConfig {
@JsonProperty("resource")
protected String resource;
@@ -51,6 +51,8 @@ public class BaseAdapterConfig extends BaseRealmConfig {
protected boolean exposeToken;
@JsonProperty("bearer-only")
protected boolean bearerOnly;
+ @JsonProperty("autodetect-bearer-only")
+ protected boolean autodetectBearerOnly;
@JsonProperty("enable-basic-auth")
protected boolean enableBasicAuth;
@JsonProperty("public-client")
@@ -123,6 +125,14 @@ public class BaseAdapterConfig extends BaseRealmConfig {
this.bearerOnly = bearerOnly;
}
+ public boolean isAutodetectBearerOnly() {
+ return autodetectBearerOnly;
+ }
+
+ public void setAutodetectBearerOnly(boolean autodetectBearerOnly) {
+ this.autodetectBearerOnly = autodetectBearerOnly;
+ }
+
public boolean isEnableBasicAuth() {
return enableBasicAuth;
}
diff --git a/core/src/main/java/org/keycloak/representations/adapters/config/PolicyEnforcerConfig.java b/core/src/main/java/org/keycloak/representations/adapters/config/PolicyEnforcerConfig.java
index db874c0..a1bc179 100644
--- a/core/src/main/java/org/keycloak/representations/adapters/config/PolicyEnforcerConfig.java
+++ b/core/src/main/java/org/keycloak/representations/adapters/config/PolicyEnforcerConfig.java
@@ -112,7 +112,7 @@ public class PolicyEnforcerConfig {
public void setOnDenyRedirectTo(String onDenyRedirectTo) {
this.onDenyRedirectTo = onDenyRedirectTo;
}
-
+
public static class PathConfig {
private String name;
diff --git a/core/src/main/java/org/keycloak/representations/idm/authorization/PolicyRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/authorization/PolicyRepresentation.java
index 9a51031..dc04991 100644
--- a/core/src/main/java/org/keycloak/representations/idm/authorization/PolicyRepresentation.java
+++ b/core/src/main/java/org/keycloak/representations/idm/authorization/PolicyRepresentation.java
@@ -16,11 +16,7 @@
*/
package org.keycloak.representations.idm.authorization;
-import com.fasterxml.jackson.annotation.JsonInclude;
-
-import java.util.ArrayList;
import java.util.HashMap;
-import java.util.List;
import java.util.Map;
import java.util.Objects;
@@ -36,9 +32,6 @@ public class PolicyRepresentation {
private Logic logic = Logic.POSITIVE;
private DecisionStrategy decisionStrategy = DecisionStrategy.UNANIMOUS;
private Map<String, String> config = new HashMap();
- private List<PolicyRepresentation> dependentPolicies;
- @JsonInclude(JsonInclude.Include.NON_EMPTY)
- private List<PolicyRepresentation> associatedPolicies = new ArrayList<>();
public String getId() {
return this.id;
@@ -96,14 +89,6 @@ public class PolicyRepresentation {
this.description = description;
}
- public List<PolicyRepresentation> getAssociatedPolicies() {
- return associatedPolicies;
- }
-
- public void setAssociatedPolicies(List<PolicyRepresentation> associatedPolicies) {
- this.associatedPolicies = associatedPolicies;
- }
-
@Override
public boolean equals(final Object o) {
if (this == o) return true;
@@ -116,12 +101,4 @@ public class PolicyRepresentation {
public int hashCode() {
return Objects.hash(getId());
}
-
- public void setDependentPolicies(List<PolicyRepresentation> dependentPolicies) {
- this.dependentPolicies = dependentPolicies;
- }
-
- public List<PolicyRepresentation> getDependentPolicies() {
- return this.dependentPolicies;
- }
}
\ No newline at end of file
diff --git a/core/src/main/java/org/keycloak/representations/idm/authorization/ResourceRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/authorization/ResourceRepresentation.java
index 8f3c795..86f6f98 100644
--- a/core/src/main/java/org/keycloak/representations/idm/authorization/ResourceRepresentation.java
+++ b/core/src/main/java/org/keycloak/representations/idm/authorization/ResourceRepresentation.java
@@ -16,14 +16,14 @@
*/
package org.keycloak.representations.idm.authorization;
-import com.fasterxml.jackson.annotation.JsonInclude;
-import com.fasterxml.jackson.annotation.JsonProperty;
-
import java.net.URI;
import java.util.Collections;
import java.util.List;
+import java.util.Objects;
import java.util.Set;
-import java.util.function.Predicate;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
/**
* <p>One or more resources that the resource server manages as a set of protected resources.
@@ -159,18 +159,6 @@ public class ResourceRepresentation {
this.owner = owner;
}
- public List<PolicyRepresentation> getPolicies() {
- return this.policies;
- }
-
- public void setPolicies(List<PolicyRepresentation> policies) {
- this.policies = policies;
- }
-
- <T> T test(Predicate<T> t) {
- return null;
- }
-
public void setTypedScopes(List<ScopeRepresentation> typedScopes) {
this.typedScopes = typedScopes;
}
@@ -178,4 +166,15 @@ public class ResourceRepresentation {
public List<ScopeRepresentation> getTypedScopes() {
return typedScopes;
}
+
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ ResourceRepresentation scope = (ResourceRepresentation) o;
+ return Objects.equals(getName(), scope.getName());
+ }
+
+ public int hashCode() {
+ return Objects.hash(getName());
+ }
}
diff --git a/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java
index 11ee08a..e552c58 100755
--- a/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java
+++ b/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java
@@ -54,6 +54,8 @@ public class RealmRepresentation {
protected Boolean registrationEmailAsUsername;
protected Boolean rememberMe;
protected Boolean verifyEmail;
+ protected Boolean loginWithEmailAllowed;
+ protected Boolean duplicateEmailsAllowed;
protected Boolean resetPasswordAllowed;
protected Boolean editUsernameAllowed;
@@ -418,6 +420,22 @@ public class RealmRepresentation {
public void setVerifyEmail(Boolean verifyEmail) {
this.verifyEmail = verifyEmail;
}
+
+ public Boolean isLoginWithEmailAllowed() {
+ return loginWithEmailAllowed;
+ }
+
+ public void setLoginWithEmailAllowed(Boolean loginWithEmailAllowed) {
+ this.loginWithEmailAllowed = loginWithEmailAllowed;
+ }
+
+ public Boolean isDuplicateEmailsAllowed() {
+ return duplicateEmailsAllowed;
+ }
+
+ public void setDuplicateEmailsAllowed(Boolean duplicateEmailsAllowed) {
+ this.duplicateEmailsAllowed = duplicateEmailsAllowed;
+ }
public Boolean isResetPasswordAllowed() {
return resetPasswordAllowed;
diff --git a/core/src/main/java/org/keycloak/representations/info/ProfileInfoRepresentation.java b/core/src/main/java/org/keycloak/representations/info/ProfileInfoRepresentation.java
index 3c474d0..c0cbea8 100644
--- a/core/src/main/java/org/keycloak/representations/info/ProfileInfoRepresentation.java
+++ b/core/src/main/java/org/keycloak/representations/info/ProfileInfoRepresentation.java
@@ -19,18 +19,26 @@ package org.keycloak.representations.info;
import org.keycloak.common.Profile;
+import java.util.LinkedList;
+import java.util.List;
+
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class ProfileInfoRepresentation {
private String name;
- private boolean previewEnabled;
+ private List<String> disabledFeatures;
public static ProfileInfoRepresentation create() {
ProfileInfoRepresentation info = new ProfileInfoRepresentation();
- info.setName(Profile.getName());
- info.setPreviewEnabled(Profile.isPreviewEnabled());
+
+ info.name = Profile.getName();
+ info.disabledFeatures = new LinkedList<>();
+ for (Profile.Feature f : Profile.getDisabledFeatures()) {
+ info.disabledFeatures.add(f.name());
+ }
+
return info;
}
@@ -38,16 +46,8 @@ public class ProfileInfoRepresentation {
return name;
}
- public void setName(String name) {
- this.name = name;
- }
-
- public boolean isPreviewEnabled() {
- return previewEnabled;
- }
-
- public void setPreviewEnabled(boolean previewEnabled) {
- this.previewEnabled = previewEnabled;
+ public List<String> getDisabledFeatures() {
+ return disabledFeatures;
}
}
diff --git a/core/src/main/java/org/keycloak/RSATokenVerifier.java b/core/src/main/java/org/keycloak/RSATokenVerifier.java
index 1cbc2e2..db8fc5a 100755
--- a/core/src/main/java/org/keycloak/RSATokenVerifier.java
+++ b/core/src/main/java/org/keycloak/RSATokenVerifier.java
@@ -19,11 +19,7 @@ package org.keycloak;
import org.keycloak.common.VerificationException;
import org.keycloak.jose.jws.JWSHeader;
-import org.keycloak.jose.jws.JWSInput;
-import org.keycloak.jose.jws.JWSInputException;
-import org.keycloak.jose.jws.crypto.RSAProvider;
import org.keycloak.representations.AccessToken;
-import org.keycloak.util.TokenUtil;
import java.security.PublicKey;
@@ -33,18 +29,10 @@ import java.security.PublicKey;
*/
public class RSATokenVerifier {
- private final String tokenString;
- private PublicKey publicKey;
- private String realmUrl;
- private boolean checkTokenType = true;
- private boolean checkActive = true;
- private boolean checkRealmUrl = true;
-
- private JWSInput jws;
- private AccessToken token;
+ private TokenVerifier tokenVerifier;
private RSATokenVerifier(String tokenString) {
- this.tokenString = tokenString;
+ this.tokenVerifier = TokenVerifier.create(tokenString);
}
public static RSATokenVerifier create(String tokenString) {
@@ -60,94 +48,45 @@ public class RSATokenVerifier {
}
public RSATokenVerifier publicKey(PublicKey publicKey) {
- this.publicKey = publicKey;
+ tokenVerifier.publicKey(publicKey);
return this;
}
public RSATokenVerifier realmUrl(String realmUrl) {
- this.realmUrl = realmUrl;
+ tokenVerifier.realmUrl(realmUrl);
return this;
}
public RSATokenVerifier checkTokenType(boolean checkTokenType) {
- this.checkTokenType = checkTokenType;
+ tokenVerifier.checkTokenType(checkTokenType);
return this;
}
public RSATokenVerifier checkActive(boolean checkActive) {
- this.checkActive = checkActive;
+ tokenVerifier.checkActive(checkActive);
return this;
}
public RSATokenVerifier checkRealmUrl(boolean checkRealmUrl) {
- this.checkRealmUrl = checkRealmUrl;
+ tokenVerifier.checkRealmUrl(checkRealmUrl);
return this;
}
public RSATokenVerifier parse() throws VerificationException {
- if (jws == null) {
- if (tokenString == null) {
- throw new VerificationException("Token not set");
- }
-
- try {
- jws = new JWSInput(tokenString);
- } catch (JWSInputException e) {
- throw new VerificationException("Failed to parse JWT", e);
- }
-
-
- try {
- token = jws.readJsonContent(AccessToken.class);
- } catch (JWSInputException e) {
- throw new VerificationException("Failed to read access token from JWT", e);
- }
- }
+ tokenVerifier.parse();
return this;
}
public AccessToken getToken() throws VerificationException {
- parse();
- return token;
+ return tokenVerifier.getToken();
}
public JWSHeader getHeader() throws VerificationException {
- parse();
- return jws.getHeader();
+ return tokenVerifier.getHeader();
}
public RSATokenVerifier verify() throws VerificationException {
- parse();
-
- if (publicKey == null) {
- throw new VerificationException("Public key not set");
- }
-
- if (checkRealmUrl && realmUrl == null) {
- throw new VerificationException("Realm URL not set");
- }
-
- if (!RSAProvider.verify(jws, publicKey)) {
- throw new VerificationException("Invalid token signature");
- }
-
- String user = token.getSubject();
- if (user == null) {
- throw new VerificationException("Subject missing in token");
- }
-
- if (checkRealmUrl && !realmUrl.equals(token.getIssuer())) {
- throw new VerificationException("Invalid token issuer. Expected '" + realmUrl + "', but was '" + token.getIssuer() + "'");
- }
-
- if (checkTokenType && !TokenUtil.TOKEN_TYPE_BEARER.equalsIgnoreCase(token.getType())) {
- throw new VerificationException("Token type is incorrect. Expected '" + TokenUtil.TOKEN_TYPE_BEARER + "' but was '" + token.getType() + "'");
- }
-
- if (checkActive && !token.isActive()) {
- throw new VerificationException("Token is not active");
- }
-
+ tokenVerifier.verify();
return this;
}
core/src/main/java/org/keycloak/TokenVerifier.java 170(+170 -0)
diff --git a/core/src/main/java/org/keycloak/TokenVerifier.java b/core/src/main/java/org/keycloak/TokenVerifier.java
new file mode 100755
index 0000000..9c30bfd
--- /dev/null
+++ b/core/src/main/java/org/keycloak/TokenVerifier.java
@@ -0,0 +1,170 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak;
+
+import org.keycloak.common.VerificationException;
+import org.keycloak.jose.jws.Algorithm;
+import org.keycloak.jose.jws.AlgorithmType;
+import org.keycloak.jose.jws.JWSHeader;
+import org.keycloak.jose.jws.JWSInput;
+import org.keycloak.jose.jws.JWSInputException;
+import org.keycloak.jose.jws.crypto.HMACProvider;
+import org.keycloak.jose.jws.crypto.RSAProvider;
+import org.keycloak.representations.AccessToken;
+import org.keycloak.util.TokenUtil;
+
+import javax.crypto.SecretKey;
+import java.security.PublicKey;
+
+/**
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public class TokenVerifier {
+
+ private final String tokenString;
+ private PublicKey publicKey;
+ private SecretKey secretKey;
+ private String realmUrl;
+ private boolean checkTokenType = true;
+ private boolean checkActive = true;
+ private boolean checkRealmUrl = true;
+
+ private JWSInput jws;
+ private AccessToken token;
+
+ protected TokenVerifier(String tokenString) {
+ this.tokenString = tokenString;
+ }
+
+ public static TokenVerifier create(String tokenString) {
+ return new TokenVerifier(tokenString);
+ }
+
+ public TokenVerifier publicKey(PublicKey publicKey) {
+ this.publicKey = publicKey;
+ return this;
+ }
+
+ public TokenVerifier secretKey(SecretKey secretKey) {
+ this.secretKey = secretKey;
+ return this;
+ }
+
+ public TokenVerifier realmUrl(String realmUrl) {
+ this.realmUrl = realmUrl;
+ return this;
+ }
+
+ public TokenVerifier checkTokenType(boolean checkTokenType) {
+ this.checkTokenType = checkTokenType;
+ return this;
+ }
+
+ public TokenVerifier checkActive(boolean checkActive) {
+ this.checkActive = checkActive;
+ return this;
+ }
+
+ public TokenVerifier checkRealmUrl(boolean checkRealmUrl) {
+ this.checkRealmUrl = checkRealmUrl;
+ return this;
+ }
+
+ public TokenVerifier parse() throws VerificationException {
+ if (jws == null) {
+ if (tokenString == null) {
+ throw new VerificationException("Token not set");
+ }
+
+ try {
+ jws = new JWSInput(tokenString);
+ } catch (JWSInputException e) {
+ throw new VerificationException("Failed to parse JWT", e);
+ }
+
+
+ try {
+ token = jws.readJsonContent(AccessToken.class);
+ } catch (JWSInputException e) {
+ throw new VerificationException("Failed to read access token from JWT", e);
+ }
+ }
+ return this;
+ }
+
+ public AccessToken getToken() throws VerificationException {
+ parse();
+ return token;
+ }
+
+ public JWSHeader getHeader() throws VerificationException {
+ parse();
+ return jws.getHeader();
+ }
+
+ public TokenVerifier verify() throws VerificationException {
+ parse();
+
+ if (checkRealmUrl && realmUrl == null) {
+ throw new VerificationException("Realm URL not set");
+ }
+
+ AlgorithmType algorithmType = getHeader().getAlgorithm().getType();
+
+ if (AlgorithmType.RSA.equals(algorithmType)) {
+ if (publicKey == null) {
+ throw new VerificationException("Public key not set");
+ }
+
+ if (!RSAProvider.verify(jws, publicKey)) {
+ throw new VerificationException("Invalid token signature");
+ }
+ } else if (AlgorithmType.HMAC.equals(algorithmType)) {
+ if (secretKey == null) {
+ throw new VerificationException("Secret key not set");
+ }
+
+ if (!HMACProvider.verify(jws, secretKey)) {
+ throw new VerificationException("Invalid token signature");
+ }
+ } else {
+ throw new VerificationException("Unknown or unsupported token algorith");
+ }
+
+ String user = token.getSubject();
+ if (user == null) {
+ throw new VerificationException("Subject missing in token");
+ }
+
+ if (checkRealmUrl && !realmUrl.equals(token.getIssuer())) {
+ throw new VerificationException("Invalid token issuer. Expected '" + realmUrl + "', but was '" + token.getIssuer() + "'");
+ }
+
+ if (checkTokenType && !TokenUtil.TOKEN_TYPE_BEARER.equalsIgnoreCase(token.getType())) {
+ throw new VerificationException("Token type is incorrect. Expected '" + TokenUtil.TOKEN_TYPE_BEARER + "' but was '" + token.getType() + "'");
+ }
+
+ if (checkActive && !token.isActive()) {
+ throw new VerificationException("Token is not active");
+ }
+
+ return this;
+ }
+
+}
diff --git a/examples/authz/servlet-authz/src/main/webapp/WEB-INF/web.xml b/examples/authz/servlet-authz/src/main/webapp/WEB-INF/web.xml
index 14d0615..e4ce0f1 100644
--- a/examples/authz/servlet-authz/src/main/webapp/WEB-INF/web.xml
+++ b/examples/authz/servlet-authz/src/main/webapp/WEB-INF/web.xml
@@ -13,16 +13,8 @@
</web-resource-collection>
<auth-constraint>
<role-name>user</role-name>
- </auth-constraint>
- </security-constraint>
-
- <security-constraint>
- <web-resource-collection>
- <web-resource-name>All Resources</web-resource-name>
- <url-pattern>/*</url-pattern>
- </web-resource-collection>
- <auth-constraint>
<role-name>admin</role-name>
+ <role-name>user_premium</role-name>
</auth-constraint>
</security-constraint>
@@ -39,9 +31,13 @@
<role-name>user</role-name>
</security-role>
+ <security-role>
+ <role-name>user_premium</role-name>
+ </security-role>
+
<error-page>
<error-code>403</error-code>
<location>/accessDenied.jsp</location>
</error-page>
-</web-app>
+</web-app>
\ No newline at end of file
examples/demo-template/pom.xml 2(+1 -1)
diff --git a/examples/demo-template/pom.xml b/examples/demo-template/pom.xml
index d08aedb..d71dc35 100755
--- a/examples/demo-template/pom.xml
+++ b/examples/demo-template/pom.xml
@@ -23,7 +23,7 @@
<version>2.5.0.Final-SNAPSHOT</version>
</parent>
- <name>Examples</name>
+ <name>Demo Examples</name>
<description/>
<modelVersion>4.0.0</modelVersion>
examples/fuse/pom.xml 2(+1 -1)
diff --git a/examples/fuse/pom.xml b/examples/fuse/pom.xml
index cc09f87..4495efb 100755
--- a/examples/fuse/pom.xml
+++ b/examples/fuse/pom.xml
@@ -23,7 +23,7 @@
<version>2.5.0.Final-SNAPSHOT</version>
</parent>
- <name>Fuse examples</name>
+ <name>Fuse Examples</name>
<description/>
<modelVersion>4.0.0</modelVersion>
diff --git a/examples/ldap/ldap-example-users.ldif b/examples/ldap/ldap-example-users.ldif
index d503255..082ee53 100644
--- a/examples/ldap/ldap-example-users.ldif
+++ b/examples/ldap/ldap-example-users.ldif
@@ -45,6 +45,7 @@ postalCode: 77332
postalCode: 66221
street: Elm 5
userPassword: password
+jpegPhoto:: /9j/4AAQSkZJRgABAQAAAQABAAD/2wCEAAMDAwMDAwQEBAQFBQUFBQcHBgYHBwsICQgJCAsRCwwLCwwLEQ8SDw4PEg8bFRMTFRsfGhkaHyYiIiYwLTA+PlQBAwMDAwMDBAQEBAUFBQUFBwcGBgcHCwgJCAkICxELDAsLDAsRDxIPDg8SDxsVExMVGx8aGRofJiIiJjAtMD4+VP/CABEIAFIAWAMBIgACEQEDEQH/xAAdAAACAgMBAQEAAAAAAAAAAAAGCAUHAwQJAgAB/9oACAEBAAAAAElmzK1aOaraUmpiktrD10DayAIMkKunPQdk+hrZwkUNkMrM88VDtt7r7C1KCJtprbSBP2J6K+VDqUlwErkDnOm/HF9IPDaWQ+cP85sXS7OCj1Iybjj2zVvJA0b91BqJ1JuQDYfkuSWrGdFzsMsWkaIUGHh4IuY1glqtWrK8G89YeJk+ldfT1pHz9//EABoBAAIDAQEAAAAAAAAAAAAAAAUHAwYIBAD/2gAIAQIQAAAApKICefJOj5haleI6vyhC1+FEa9UydaFaD7hDLtU9jttBsCJni6f/xAAaAQADAQEBAQAAAAAAAAAAAAAFBgcDBAII/9oACAEDEAAAAF5+Mdc4X6LUuz2kyGquiYYI/PzvUctR8d5289ghjS48fuOK/wD/xAA5EAABAwIEBAMGAwcFAAAAAAABAgMEABEFBhIhEzFBURQiMgdCYXGBwSOhsRUkM3KCkdFSU2J04f/aAAgBAQABPwGHLbkIVbmjmPhSF60BSL2VUPDhLYLjL4KuotyP0NQPZq5iWBmUiWPGFarIV/DIHS/MGpeC4lhrnh5MdxLgJBuna/wPWsFyW2/kVaH2/wB7dK5LSyPMDbb6KtWD+zqBKypKTIZQJ89q4dPNG10f+1GydizuAPSSgstwlHiawQVnVby/LrTrfBOkqF6gyWmEJQSoKvfVYUiawVbrubc6bQEr4qSkKpDj/iEMKSdLoDYSm/XkRWSco+Bg4dORxo8lCyJcd1NgqxI68tjSG2Wm/wAFKUi97DYVi2GRcZh8F+9goLSR0IpC02TakqA2rE4acSgPw9WgPJ0qPYE71mL2fQJLanYiOGmJh5DKEep125IualtvxJDrDo0OtLKFjsQd6U8tJ2686deDa/Ii1/STvWU8KwDOOVoaZ8VPioV2VLQdC0WNwQRSU2QEk3sNyetPExzq9zr8KEwpulXTrTWJx0OLQp5IOskC/Sm5iFpSpCr3A/S9IeF9zQJPI2r2g5Tw3AWFS0uPOPzpay3q9KE8zc9Tvzpxv6HrWUcCh5hnmC/iQiv6bskp18XvbcVlXJE3K0xbrOJJfadFnGi2U/UG5peocqxPEmYrCtak3I2FTcckBwp1q9PIck87Hbn3oLllYWTvzvUbFpkE7XIv1+FYFizOIM60W18inqmmeIrnXtTaxmW82p2I61CYFkL2Ui6upI5XrhKS0nWL6VadQ95J7VkzKcZzEor4lsHgkL4KwSrWO1ulJdWbeUHbcjaluqAPkX+VZjxMLkur07Nk7cjYdjyvTSzJcceWLFe4+9Mt6x5eQNOBCiR8enSsqylxcSLSdR1g7X273pmVYedJT9Kx7L+G5mjBp911sp9KkqOx+KTsazDl+Rl3EBHcktvNKuptSFbEfEdCKyEGHNi+OKhZUGFNg/1JJ3BoSU99/lTuzZUs2FutZhaSZT5SggJVcG29r9bU1p8OhQuRalyHgBa4TyriWTfqayjEck4kp1CiA0j63VyplTqPKtQV89qnfsl5jhzyzw1/7igP7XrPOF4fgmMNrg4ozIjSFH8BCwtbI7E9u1ZCwvCpjrcyRKQHW3PLG1WJ+N/8U65o3sBSWCs8R3c9B2rN2BGWy7ISQDpsRb86SuRC1IlNLQb9ep+B60ZUbYnaokOXijwaYQdJ5q90bd6wDBF4JESlJC7+Zdhvc00W3htWbZmHYVhi3J8PxUVR0rb06t+l+3zrFXMLU6VQUvJJcWVBQ0pSOiUi6jt3Jpryea52r2c+KxKap9995TEVvyjWbFajtfvWsL+VKW1iBJQoKZbV03uof4qThEWWyriNJWee++9R8uYTH06YrYIH+kU9BaCNKU2t6aw93iJ0KO6axfFIOFSW1OSEsFwn1GwJHOsdbYzhlybEgy21ugC4QoG+nfSfnU6MAtY9JR7vfferqPQmsOzTOw9bDMdTLZTfhtcklZFrnufnWLe0HM+JLMbi+FSfIpDW3zuedZXQzGyxh/IDw4Wf6/Mo1h8pqbCaktnyOi6T8L1PxlqFj0OE5sJLK7fzDkKOlbYVWM4yzl7FY63dmJKTqI90o62r2pQXnDExpg8aK4gNmytkHoR8DTMx6MVLYdW2u1uo/NNL1JSt3UFq08qCVN+pR2pS9SiR32oyPElp824iLIc/5dlUjOstvLyMKCAAEFBdJ9y/Kms2QMNyvBjofC3ww2opQb8/MAfvWPY/IxzFvFqUW9B/BsfSByFSfaGpzAm+E4WZrTyF2725/Q9qx7NT+ZHY7i20t8JrTpBvc33NSJkzgmKH3THJ1cLUdN/lQTtvUSO2664FXA0LWr6C9KJJNConrP8AIaRzXTPo/t+tO/f71J9X0H6U1T1L+1QNncQ/6n3Ff//EACQQAQEAAgICAQQDAQAAAAAAAAERACExQVGBYXGRobEQwfDR/9oACAEBAAE/EFxCJHa+QcvGUwhESkN9fiYHv0VpOm0o9OFKc/OP0nyMqyNS7O0x2FTnAH6HHF+oMkQP6cPbVCpqdHusdzBsTjjKJKrAZwN411hOmhN3Q+OdYFicQWN6QTJzrwEHC7aJwbcdwysS/AW7GNhVpxF5hwesFHQhHh77IyY3UAIHQHgwuHXP36x4apQUBQcWcXJmcwJakbelduaVYQKsBS3eAVJyB+cQnvn1w6o9ZDI6pnUIiPfdxG9iCBQRXreNy18f2PjFCeB0fH3MMVGwtGz1hMB2JKPunWGBO9cLh1omlDTJZj6GlAPSm3RMCgLAgSfWedY3okyFyEYQWneraZLkQPZNfjHeyDpVdBP7dYuNwjIibANNDHmgaDAeXRptkBMDpGVE6Hqjvltx4aCVkJq9o4gOA4y6pRz7bXiMrUKoBArI5ZdacH0tDvKBFDsjg3oRL8G/tceEiW1Y6UAXW0jxK47AXAL0gOg3HkF1MeStC2JfnHWoCStw/wDe804AlIhCOmmMAu7jZ+Fw0B2U6H2hTGAuDJHQLuYV1xK4W0jhBExMKEcCV+5m01yqkMRn8VrZVUN24oNBOeYF1eI6cEGRpvfJrB+GQLVd9ZIRJROhHod4eCYNA8PTGhag8kNPJivYOADUtVxNVsC44dbHxjhJe2AYOXuc16+cQODu1ej3TF3gwCyreivI5JUksX4fesSG1rUxOwRWcCuJY2pBRrL0AQyrY62UfZjjgyEKuhQpIYLK9dtHzfVC4V2Gib2vOsI+EqqQI4gFwY1Py185FplUDmj8LU84jKWQGhYb+GYoKQdBVa8fLgkXPHI8s+uWSNh+pzh6/EJ6CF0MduPBMS2w88OjhgEW0VUC+Ht6xbrYBeevOWtwmrpdV6RSGDx83G9WqMDRe3oFCj2uUoN4FoGLoFWobz7guMGnj2W4jCzVWD1RwXq8VTsWxxafUEV2R2WOBoHSEnGyczAFEcmv43hrndLdMA4GT4cJ8xi4mEFtpKDrTMB0WxjaWEKwJpfaeX95xkQjKxHJ5FgWWx4WjvAwAztJmx6tsUyaoEOUrgVl3jsSC8V1lgV0fwbzc/pzg9R17zkOp+jGh6Z5/wDGuf2M4nvOf+9ZxOos/wD/xAAvEQABAwMCBQMDAwUAAAAAAAACAQMEAAUREiEGBxMxQSJRcSMzYRQygRUXQpGy/9oACAECAQE/AHIDLhl0i0+VQvHvXF3MCTaZ7kW0voatv+o1FP8ABcKBfhavPM+dcbZESOCsSQki49pzj0dk+Fqy81r3FkSBkfUbkzAdQjVS6I9lQUqw8SWzi6GRRjVSHTrBdiHPaisAEuUNa5mXmBDgLFC4lBubGHmCRDFT8YQsYwtQmHpkpBFOorpepPfPmoPAIq0RPvCCGKYRKv3B71pZV8fqte6ePmuVEmaxxMLDUkWmDTL45RNenslA5lNiT/aVzPmzJt7EHzJ1GQwKKONOd65cxYz9wdUw3ANqfaOQaiC6UHbHxVxYaGzTlfX6aNL37ZqG49CurT0UnWzE8obKay/hPNWtXpdqhSFdV7qsoqmrStqq+cj4WuYPB5SVfu7twJWmAXDKhnT7COKslxnW6cMtvIg0qIW22Pav7i2hwOq7EdF3VujZJiuI+MnrzDVuO30owEmtvuq/lVrh+2G1dbZLfbkDBceDW4iEmlM99SVFVhY7e6l6e5KhLTtmjXAFafQTbwuyjsua454JC4223QLXHajNhJQnhBNO2MZ/NWLl0/Luk39Q2rcQeu2Cki6s9gJK4e5ZXNqUSzxT9KXWbcHOC2T0qlcPWz+k2dmC+YPi1kQXTjLefShJ7pTUiO2pAaIqpjG2MJQft/irl+9Ka+4XwtJ9qmu4/NF99z4D/mv/xAAsEQACAQMCBAUDBQAAAAAAAAABAgMABBEFEgYhMUETFCIycVFSYRUzNGJy/9oACAEDAQE/AE09bmQ+F8kNWhcIsrs9wAYmHp+vMVYcIx291cO+JInQqoPXJ61c8MWzW6Rx5HhoQP7HHKtZ0W5tMJMMhjkN8V5B/uFcM2EodZDAjxMNpY9qtoRyIxtHUVLNawYyBQEM6+nGcVxVFB5F96bmHsI7ZokZ61wdEP0pJcDdJzPqzSnZESprWricTAktWgtNtBfJJ7GtRtYZonWRAQQeXatdsXtr90VNoxyAbIrhDXbaJYrKO1IZurA5yfqajYk7GwAwqWxikOSVNJFHbx5BBP4q9uikEojdC/hnCEg1LNHM5Zwuc9xWnRywSq8UrJjmDitP4ha2neS6kYnwiFB+7NXvFFtawxEOrTPtJUdgetapxVBDZFrdwZPSy5/PUGtS1iO7n8xEGQuPUuT1qEhgx25yerVb9Kv/AHU/8l/8VcftrSe5fmovYK//2Q==
dn: cn=ldap-user,ou=RealmRoles,dc=keycloak,dc=org
objectclass: top
examples/ldap/ldaprealm.json 28(+28 -0)
diff --git a/examples/ldap/ldaprealm.json b/examples/ldap/ldaprealm.json
index ef1e2ae..1630924 100644
--- a/examples/ldap/ldaprealm.json
+++ b/examples/ldap/ldaprealm.json
@@ -119,6 +119,21 @@
"id.token.claim" : "true",
"access.token.claim" : "true"
}
+ },
+ {
+ "protocolMapper" : "oidc-usermodel-attribute-mapper",
+ "protocol" : "openid-connect",
+ "name" : "picture",
+ "consentText" : "Picture",
+ "consentRequired" : true,
+ "config" : {
+ "Claim JSON Type" : "String",
+ "user.attribute" : "picture",
+ "claim.name" : "picture",
+ "multivalued": "false",
+ "id.token.claim" : "true",
+ "access.token.claim" : "true"
+ }
}
]
}
@@ -248,6 +263,19 @@
}
},
{
+ "name" : "picture",
+ "federationMapperType" : "user-attribute-ldap-mapper",
+ "federationProviderDisplayName" : "ldap-apacheds",
+ "config" : {
+ "ldap.attribute" : "jpegPhoto",
+ "user.model.attribute" : "picture",
+ "is.mandatory.in.ldap" : "false",
+ "read.only" : "false",
+ "always.read.value.from.ldap" : "true",
+ "is.binary.attribute" : "true"
+ }
+ },
+ {
"name" : "realm roles",
"federationMapperType" : "role-ldap-mapper",
"federationProviderDisplayName" : "ldap-apacheds",
diff --git a/examples/ldap/src/main/java/org/keycloak/example/ldap/LDAPPictureServlet.java b/examples/ldap/src/main/java/org/keycloak/example/ldap/LDAPPictureServlet.java
new file mode 100644
index 0000000..fe5a983
--- /dev/null
+++ b/examples/ldap/src/main/java/org/keycloak/example/ldap/LDAPPictureServlet.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.example.ldap;
+
+import java.io.IOException;
+import java.util.List;
+
+import javax.servlet.ServletException;
+import javax.servlet.ServletOutputStream;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.keycloak.KeycloakSecurityContext;
+import org.keycloak.common.util.Base64;
+import org.keycloak.representations.IDToken;
+
+/**
+ * Tests binary LDAP attribute
+ *
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class LDAPPictureServlet extends HttpServlet {
+
+ @Override
+ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
+ resp.setContentType("image/jpeg");
+ ServletOutputStream outputStream = resp.getOutputStream();
+
+ KeycloakSecurityContext securityContext = (KeycloakSecurityContext) req.getAttribute(KeycloakSecurityContext.class.getName());
+ IDToken idToken = securityContext.getIdToken();
+
+ String profilePicture = idToken.getPicture();
+
+ if (profilePicture != null) {
+ byte[] decodedPicture = Base64.decode(profilePicture);
+ outputStream.write(decodedPicture);
+ }
+
+ outputStream.flush();
+ }
+
+}
diff --git a/examples/ldap/src/main/webapp/index.jsp b/examples/ldap/src/main/webapp/index.jsp
index a40c382..683e5b1 100644
--- a/examples/ldap/src/main/webapp/index.jsp
+++ b/examples/ldap/src/main/webapp/index.jsp
@@ -37,6 +37,9 @@
<p><b>Full Name: </b><%=idToken.getName()%></p>
<p><b>First: </b><%=idToken.getGivenName()%></p>
<p><b>Last: </b><%=idToken.getFamilyName()%></p>
+ <% if (idToken.getPicture() != null) { %>
+ <p><b>Profile picture: </b><img src='/ldap-portal/picture' /></p>
+ <% } %>
<hr />
diff --git a/examples/ldap/src/main/webapp/WEB-INF/web.xml b/examples/ldap/src/main/webapp/WEB-INF/web.xml
index 23d8896..64f703a 100644
--- a/examples/ldap/src/main/webapp/WEB-INF/web.xml
+++ b/examples/ldap/src/main/webapp/WEB-INF/web.xml
@@ -23,6 +23,16 @@
<module-name>ldap-portal</module-name>
+ <servlet>
+ <servlet-name>Picture</servlet-name>
+ <servlet-class>org.keycloak.example.ldap.LDAPPictureServlet</servlet-class>
+ </servlet>
+
+ <servlet-mapping>
+ <servlet-name>Picture</servlet-name>
+ <url-pattern>/picture/*</url-pattern>
+ </servlet-mapping>
+
<security-constraint>
<web-resource-collection>
<web-resource-name>LDAPApp</web-resource-name>
examples/pom.xml 2(+1 -1)
diff --git a/examples/pom.xml b/examples/pom.xml
index f2507b1..2922fd5 100755
--- a/examples/pom.xml
+++ b/examples/pom.xml
@@ -23,7 +23,7 @@
<version>2.5.0.Final-SNAPSHOT</version>
</parent>
- <name>Examples</name>
+ <name>Keycloak Examples</name>
<description/>
<modelVersion>4.0.0</modelVersion>
examples/providers/rest/pom.xml 2(+1 -1)
diff --git a/examples/providers/rest/pom.xml b/examples/providers/rest/pom.xml
index 1f78550..9182fcc 100755
--- a/examples/providers/rest/pom.xml
+++ b/examples/providers/rest/pom.xml
@@ -23,7 +23,7 @@
<version>2.5.0.Final-SNAPSHOT</version>
</parent>
- <name>Authenticator Example</name>
+ <name>REST Example</name>
<description/>
<modelVersion>4.0.0</modelVersion>
examples/saml/pom.xml 2(+1 -1)
diff --git a/examples/saml/pom.xml b/examples/saml/pom.xml
index c5ffee9..f7a179e 100755
--- a/examples/saml/pom.xml
+++ b/examples/saml/pom.xml
@@ -23,7 +23,7 @@
<version>2.5.0.Final-SNAPSHOT</version>
</parent>
- <name>Provider Examples</name>
+ <name>SAML Examples</name>
<description/>
<modelVersion>4.0.0</modelVersion>
diff --git a/federation/ldap/src/main/java/org/keycloak/storage/ldap/idm/store/ldap/LDAPIdentityStore.java b/federation/ldap/src/main/java/org/keycloak/storage/ldap/idm/store/ldap/LDAPIdentityStore.java
index b36c17c..fec84b2 100644
--- a/federation/ldap/src/main/java/org/keycloak/storage/ldap/idm/store/ldap/LDAPIdentityStore.java
+++ b/federation/ldap/src/main/java/org/keycloak/storage/ldap/idm/store/ldap/LDAPIdentityStore.java
@@ -18,6 +18,7 @@
package org.keycloak.storage.ldap.idm.store.ldap;
import org.jboss.logging.Logger;
+import org.keycloak.common.util.Base64;
import org.keycloak.models.LDAPConstants;
import org.keycloak.models.ModelException;
import org.keycloak.storage.ldap.LDAPConfig;
@@ -40,6 +41,8 @@ import javax.naming.directory.DirContext;
import javax.naming.directory.ModificationItem;
import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;
+
+import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
@@ -323,8 +326,15 @@ public class LDAPIdentityStore implements IdentityStore {
Set<String> attrValues = new LinkedHashSet<>();
NamingEnumeration<?> enumm = ldapAttribute.getAll();
while (enumm.hasMoreElements()) {
- String attrVal = enumm.next().toString().trim();
- attrValues.add(attrVal);
+ Object val = enumm.next();
+
+ if (val instanceof byte[]) { // byte[]
+ String attrVal = Base64.encodeBytes((byte[]) val);
+ attrValues.add(attrVal);
+ } else { // String
+ String attrVal = val.toString().trim();
+ attrValues.add(attrVal);
+ }
}
if (ldapAttributeName.equalsIgnoreCase(LDAPConstants.OBJECT_CLASS)) {
@@ -377,7 +387,18 @@ public class LDAPIdentityStore implements IdentityStore {
if (val == null || val.toString().trim().length() == 0) {
val = LDAPConstants.EMPTY_ATTRIBUTE_VALUE;
}
- attr.add(val);
+
+ if (getConfig().getBinaryAttributeNames().contains(attrName)) {
+ // Binary attribute
+ try {
+ byte[] bytes = Base64.decode(val);
+ attr.add(bytes);
+ } catch (IOException ioe) {
+ logger.warnf("Wasn't able to Base64 decode the attribute value. Ignoring attribute update. LDAP DN: %s, Attribute: %s, Attribute value: %s" + ldapObject.getDn(), attrName, attrValue);
+ }
+ } else {
+ attr.add(val);
+ }
}
entryAttributes.put(attr);
diff --git a/federation/ldap/src/main/java/org/keycloak/storage/ldap/idm/store/ldap/LDAPOperationManager.java b/federation/ldap/src/main/java/org/keycloak/storage/ldap/idm/store/ldap/LDAPOperationManager.java
index cb58a1e..cf421e7 100644
--- a/federation/ldap/src/main/java/org/keycloak/storage/ldap/idm/store/ldap/LDAPOperationManager.java
+++ b/federation/ldap/src/main/java/org/keycloak/storage/ldap/idm/store/ldap/LDAPOperationManager.java
@@ -515,8 +515,17 @@ public class LDAPOperationManager {
}
}
+ StringBuilder binaryAttrsBuilder = new StringBuilder();
if (this.config.isObjectGUID()) {
- env.put("java.naming.ldap.attributes.binary", LDAPConstants.OBJECT_GUID);
+ binaryAttrsBuilder.append(LDAPConstants.OBJECT_GUID).append(" ");
+ }
+ for (String attrName : config.getBinaryAttributeNames()) {
+ binaryAttrsBuilder.append(attrName).append(" ");
+ }
+
+ String binaryAttrs = binaryAttrsBuilder.toString().trim();
+ if (!binaryAttrs.isEmpty()) {
+ env.put("java.naming.ldap.attributes.binary", binaryAttrs);
}
if (logger.isDebugEnabled()) {
diff --git a/federation/ldap/src/main/java/org/keycloak/storage/ldap/LDAPConfig.java b/federation/ldap/src/main/java/org/keycloak/storage/ldap/LDAPConfig.java
index 930837d..ee2a89f 100644
--- a/federation/ldap/src/main/java/org/keycloak/storage/ldap/LDAPConfig.java
+++ b/federation/ldap/src/main/java/org/keycloak/storage/ldap/LDAPConfig.java
@@ -24,6 +24,7 @@ import org.keycloak.storage.UserStorageProvider;
import javax.naming.directory.SearchControls;
import java.util.Collection;
import java.util.HashSet;
+import java.util.List;
import java.util.Properties;
import java.util.Set;
@@ -34,6 +35,7 @@ import java.util.Set;
public class LDAPConfig {
private final MultivaluedHashMap<String, String> config;
+ private final Set<String> binaryAttributeNames = new HashSet<>();
public LDAPConfig(MultivaluedHashMap<String, String> config) {
this.config = config;
@@ -184,4 +186,39 @@ public class LDAPConfig {
return UserStorageProvider.EditMode.valueOf(editModeString);
}
}
+
+ public void addBinaryAttribute(String attrName) {
+ binaryAttributeNames.add(attrName);
+ }
+
+ public Set<String> getBinaryAttributeNames() {
+ return binaryAttributeNames;
+ }
+
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == this) return true;
+ if (!(obj instanceof LDAPConfig)) return false;
+
+ LDAPConfig that = (LDAPConfig) obj;
+
+ if (!config.equals(that.config)) return false;
+ if (!binaryAttributeNames.equals(that.binaryAttributeNames)) return false;
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ return config.hashCode() * 13 + binaryAttributeNames.hashCode();
+ }
+
+ @Override
+ public String toString() {
+ MultivaluedHashMap<String, String> copy = new MultivaluedHashMap<String, String>(config);
+ copy.remove(LDAPConstants.BIND_CREDENTIAL);
+ return new StringBuilder(copy.toString())
+ .append(", binaryAttributes: ").append(binaryAttributeNames)
+ .toString();
+ }
}
diff --git a/federation/ldap/src/main/java/org/keycloak/storage/ldap/LDAPIdentityStoreRegistry.java b/federation/ldap/src/main/java/org/keycloak/storage/ldap/LDAPIdentityStoreRegistry.java
index 7dc3086..67d03f2 100644
--- a/federation/ldap/src/main/java/org/keycloak/storage/ldap/LDAPIdentityStoreRegistry.java
+++ b/federation/ldap/src/main/java/org/keycloak/storage/ldap/LDAPIdentityStoreRegistry.java
@@ -20,8 +20,8 @@ package org.keycloak.storage.ldap;
import org.jboss.logging.Logger;
import org.keycloak.common.util.MultivaluedHashMap;
import org.keycloak.component.ComponentModel;
-import org.keycloak.models.LDAPConstants;
import org.keycloak.storage.ldap.idm.store.ldap.LDAPIdentityStore;
+import org.keycloak.storage.ldap.mappers.LDAPConfigDecorator;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@@ -33,37 +33,40 @@ public class LDAPIdentityStoreRegistry {
private static final Logger logger = Logger.getLogger(LDAPIdentityStoreRegistry.class);
- private Map<String, LDAPIdentityStoreContext> ldapStores = new ConcurrentHashMap<String, LDAPIdentityStoreContext>();
+ private Map<String, LDAPIdentityStoreContext> ldapStores = new ConcurrentHashMap<>();
- public LDAPIdentityStore getLdapStore(ComponentModel model) {
- LDAPIdentityStoreContext context = ldapStores.get(model.getId());
+ public LDAPIdentityStore getLdapStore(ComponentModel ldapModel, Map<ComponentModel, LDAPConfigDecorator> configDecorators) {
+ LDAPIdentityStoreContext context = ldapStores.get(ldapModel.getId());
// Ldap config might have changed for the realm. In this case, we must re-initialize
- MultivaluedHashMap<String, String> config = model.getConfig();
- if (context == null || !config.equals(context.config)) {
- logLDAPConfig(model.getName(), config);
+ MultivaluedHashMap<String, String> configModel = ldapModel.getConfig();
+ LDAPConfig ldapConfig = new LDAPConfig(configModel);
+ for (Map.Entry<ComponentModel, LDAPConfigDecorator> entry : configDecorators.entrySet()) {
+ ComponentModel mapperModel = entry.getKey();
+ LDAPConfigDecorator decorator = entry.getValue();
- LDAPIdentityStore store = createLdapIdentityStore(config);
- context = new LDAPIdentityStoreContext(config, store);
- ldapStores.put(model.getId(), context);
+ decorator.updateLDAPConfig(ldapConfig, mapperModel);
+ }
+
+ if (context == null || !ldapConfig.equals(context.config)) {
+ logLDAPConfig(ldapModel.getName(), ldapConfig);
+
+ LDAPIdentityStore store = createLdapIdentityStore(ldapConfig);
+ context = new LDAPIdentityStoreContext(ldapConfig, store);
+ ldapStores.put(ldapModel.getId(), context);
}
return context.store;
}
// Don't log LDAP password
- private void logLDAPConfig(String fedProviderDisplayName, MultivaluedHashMap<String, String> ldapConfig) {
- MultivaluedHashMap<String, String> copy = new MultivaluedHashMap<String, String>(ldapConfig);
- copy.remove(LDAPConstants.BIND_CREDENTIAL);
- logger.infof("Creating new LDAP based partition manager for the Federation provider: " + fedProviderDisplayName + ", LDAP Configuration: " + copy);
+ private void logLDAPConfig(String fedProviderDisplayName, LDAPConfig ldapConfig) {
+ logger.infof("Creating new LDAP Store for the LDAP storage provider: '%s', LDAP Configuration: %s", fedProviderDisplayName, ldapConfig.toString());
}
/**
- * @param ldapConfig from realm
- * @return PartitionManager instance based on LDAP store
+ * Create LDAPIdentityStore to be cached in the local registry
*/
- public static LDAPIdentityStore createLdapIdentityStore(MultivaluedHashMap<String, String> ldapConfig) {
- LDAPConfig cfg = new LDAPConfig(ldapConfig);
-
+ public static LDAPIdentityStore createLdapIdentityStore(LDAPConfig cfg) {
checkSystemProperty("com.sun.jndi.ldap.connect.pool.authentication", "none simple");
checkSystemProperty("com.sun.jndi.ldap.connect.pool.initsize", "1");
checkSystemProperty("com.sun.jndi.ldap.connect.pool.maxsize", "1000");
@@ -84,12 +87,12 @@ public class LDAPIdentityStoreRegistry {
private class LDAPIdentityStoreContext {
- private LDAPIdentityStoreContext(MultivaluedHashMap<String, String> config, LDAPIdentityStore store) {
+ private LDAPIdentityStoreContext(LDAPConfig config, LDAPIdentityStore store) {
this.config = config;
this.store = store;
}
- private MultivaluedHashMap<String, String> config;
+ private LDAPConfig config;
private LDAPIdentityStore store;
}
}
diff --git a/federation/ldap/src/main/java/org/keycloak/storage/ldap/LDAPStorageProvider.java b/federation/ldap/src/main/java/org/keycloak/storage/ldap/LDAPStorageProvider.java
index d1fc068..afdf668 100755
--- a/federation/ldap/src/main/java/org/keycloak/storage/ldap/LDAPStorageProvider.java
+++ b/federation/ldap/src/main/java/org/keycloak/storage/ldap/LDAPStorageProvider.java
@@ -39,6 +39,7 @@ import org.keycloak.models.RoleModel;
import org.keycloak.models.UserCredentialModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserManager;
+import org.keycloak.models.cache.UserCache;
import org.keycloak.storage.StorageId;
import org.keycloak.storage.UserStorageProvider;
import org.keycloak.storage.ldap.idm.model.LDAPObject;
@@ -138,6 +139,9 @@ public class LDAPStorageProvider implements UserStorageProvider,
protected UserModel proxy(RealmModel realm, UserModel local, LDAPObject ldapObject) {
UserModel proxied = local;
+
+ checkDNChanged(realm, local, ldapObject);
+
switch (editMode) {
case READ_ONLY:
proxied = new ReadonlyLDAPUserModelDelegate(local, this);
@@ -159,6 +163,20 @@ public class LDAPStorageProvider implements UserStorageProvider,
return proxied;
}
+ private void checkDNChanged(RealmModel realm, UserModel local, LDAPObject ldapObject) {
+ String dnFromDB = local.getFirstAttribute(LDAPConstants.LDAP_ENTRY_DN);
+ String ldapDn = ldapObject.getDn().toString();
+ if (!ldapDn.equals(dnFromDB)) {
+ logger.debugf("Updated LDAP DN of user '%s' to '%s'", local.getUsername(), ldapDn);
+ local.setSingleAttribute(LDAPConstants.LDAP_ENTRY_DN, ldapDn);
+
+ UserCache userCache = session.userCache();
+ if (userCache != null) {
+ userCache.evict(realm, local);
+ }
+ }
+ }
+
@Override
public boolean supportsCredentialAuthenticationFor(String type) {
return type.equals(CredentialModel.KERBEROS) && kerberosConfig.isAllowKerberosAuthentication();
diff --git a/federation/ldap/src/main/java/org/keycloak/storage/ldap/LDAPStorageProviderFactory.java b/federation/ldap/src/main/java/org/keycloak/storage/ldap/LDAPStorageProviderFactory.java
index 051ed55..d8d0497 100755
--- a/federation/ldap/src/main/java/org/keycloak/storage/ldap/LDAPStorageProviderFactory.java
+++ b/federation/ldap/src/main/java/org/keycloak/storage/ldap/LDAPStorageProviderFactory.java
@@ -48,7 +48,9 @@ import org.keycloak.storage.ldap.idm.query.internal.LDAPQueryConditionsBuilder;
import org.keycloak.storage.ldap.idm.store.ldap.LDAPIdentityStore;
import org.keycloak.storage.ldap.mappers.FullNameLDAPStorageMapper;
import org.keycloak.storage.ldap.mappers.FullNameLDAPStorageMapperFactory;
+import org.keycloak.storage.ldap.mappers.LDAPConfigDecorator;
import org.keycloak.storage.ldap.mappers.LDAPStorageMapper;
+import org.keycloak.storage.ldap.mappers.LDAPStorageMapperFactory;
import org.keycloak.storage.ldap.mappers.UserAttributeLDAPStorageMapper;
import org.keycloak.storage.ldap.mappers.UserAttributeLDAPStorageMapperFactory;
import org.keycloak.storage.ldap.mappers.msad.MSADUserAccountControlStorageMapperFactory;
@@ -57,7 +59,9 @@ import org.keycloak.storage.user.SynchronizationResult;
import org.keycloak.utils.CredentialHelper;
import java.util.Date;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
@@ -179,10 +183,30 @@ public class LDAPStorageProviderFactory implements UserStorageProviderFactory<LD
@Override
public LDAPStorageProvider create(KeycloakSession session, ComponentModel model) {
- LDAPIdentityStore ldapIdentityStore = this.ldapStoreRegistry.getLdapStore(model);
+ Map<ComponentModel, LDAPConfigDecorator> configDecorators = getLDAPConfigDecorators(session, model);
+
+ LDAPIdentityStore ldapIdentityStore = this.ldapStoreRegistry.getLdapStore(model, configDecorators);
return new LDAPStorageProvider(this, session, model, ldapIdentityStore);
}
+
+ // Check if it's some performance overhead to create this map in every request. But probably not...
+ protected Map<ComponentModel, LDAPConfigDecorator> getLDAPConfigDecorators(KeycloakSession session, ComponentModel ldapModel) {
+ RealmModel realm = session.realms().getRealm(ldapModel.getParentId());
+ List<ComponentModel> mapperComponents = realm.getComponents(ldapModel.getId(), LDAPStorageMapper.class.getName());
+
+ Map<ComponentModel, LDAPConfigDecorator> result = new HashMap<>();
+ for (ComponentModel mapperModel : mapperComponents) {
+ LDAPStorageMapperFactory mapperFactory = (LDAPStorageMapperFactory) session.getKeycloakSessionFactory().getProviderFactory(LDAPStorageMapper.class, mapperModel.getProviderId());
+ if (mapperFactory instanceof LDAPConfigDecorator) {
+ result.put(mapperModel, (LDAPConfigDecorator) mapperFactory);
+ }
+ }
+
+ return result;
+ }
+
+
@Override
public void validateConfiguration(KeycloakSession session, RealmModel realm, ComponentModel config) throws ComponentValidationException {
LDAPConfig cfg = new LDAPConfig(config.getConfig());
diff --git a/federation/ldap/src/main/java/org/keycloak/storage/ldap/LDAPUtils.java b/federation/ldap/src/main/java/org/keycloak/storage/ldap/LDAPUtils.java
index 3a391d3..87754f5 100755
--- a/federation/ldap/src/main/java/org/keycloak/storage/ldap/LDAPUtils.java
+++ b/federation/ldap/src/main/java/org/keycloak/storage/ldap/LDAPUtils.java
@@ -153,11 +153,12 @@ public class LDAPUtils {
* @param ldapProvider
* @param membershipType how is 'member' attribute saved (full DN or just uid)
* @param memberAttrName usually 'member'
+ * @param memberChildAttrName used just if membershipType is UID. Usually 'uid'
* @param ldapParent role or group
* @param ldapChild usually user (or child group or child role)
* @param sendLDAPUpdateRequest if true, the method will send LDAP update request too. Otherwise it will skip it
*/
- public static void addMember(LDAPStorageProvider ldapProvider, MembershipType membershipType, String memberAttrName, LDAPObject ldapParent, LDAPObject ldapChild, boolean sendLDAPUpdateRequest) {
+ public static void addMember(LDAPStorageProvider ldapProvider, MembershipType membershipType, String memberAttrName, String memberChildAttrName, LDAPObject ldapParent, LDAPObject ldapChild, boolean sendLDAPUpdateRequest) {
Set<String> memberships = getExistingMemberships(memberAttrName, ldapParent);
@@ -171,7 +172,7 @@ public class LDAPUtils {
}
}
- String membership = getMemberValueOfChildObject(ldapChild, membershipType);
+ String membership = getMemberValueOfChildObject(ldapChild, membershipType, memberChildAttrName);
memberships.add(membership);
ldapParent.setAttribute(memberAttrName, memberships);
@@ -187,13 +188,14 @@ public class LDAPUtils {
* @param ldapProvider
* @param membershipType how is 'member' attribute saved (full DN or just uid)
* @param memberAttrName usually 'member'
+ * @param memberChildAttrName used just if membershipType is UID. Usually 'uid'
* @param ldapParent role or group
* @param ldapChild usually user (or child group or child role)
*/
- public static void deleteMember(LDAPStorageProvider ldapProvider, MembershipType membershipType, String memberAttrName, LDAPObject ldapParent, LDAPObject ldapChild) {
+ public static void deleteMember(LDAPStorageProvider ldapProvider, MembershipType membershipType, String memberAttrName, String memberChildAttrName, LDAPObject ldapParent, LDAPObject ldapChild) {
Set<String> memberships = getExistingMemberships(memberAttrName, ldapParent);
- String userMembership = getMemberValueOfChildObject(ldapChild, membershipType);
+ String userMembership = getMemberValueOfChildObject(ldapChild, membershipType, memberChildAttrName);
memberships.remove(userMembership);
@@ -222,10 +224,14 @@ public class LDAPUtils {
}
/**
- * Get value to be used as attribute 'member' in some parent ldapObject
+ * Get value to be used as attribute 'member' or 'memberUid' in some parent ldapObject
*/
- public static String getMemberValueOfChildObject(LDAPObject ldapUser, MembershipType membershipType) {
- return membershipType == MembershipType.DN ? ldapUser.getDn().toString() : ldapUser.getAttributeAsString(ldapUser.getRdnAttributeName());
+ public static String getMemberValueOfChildObject(LDAPObject ldapUser, MembershipType membershipType, String memberChildAttrName) {
+ if (membershipType == MembershipType.DN) {
+ return ldapUser.getDn().toString();
+ } else {
+ return ldapUser.getAttributeAsString(memberChildAttrName);
+ }
}
diff --git a/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/AbstractLDAPStorageMapper.java b/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/AbstractLDAPStorageMapper.java
index fb09d1d..dedf8d8 100644
--- a/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/AbstractLDAPStorageMapper.java
+++ b/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/AbstractLDAPStorageMapper.java
@@ -22,6 +22,7 @@ import org.keycloak.models.GroupModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
+import org.keycloak.storage.ldap.LDAPConfig;
import org.keycloak.storage.ldap.LDAPStorageProvider;
import org.keycloak.storage.ldap.idm.model.LDAPObject;
import org.keycloak.storage.ldap.idm.query.internal.LDAPQuery;
diff --git a/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/LDAPConfigDecorator.java b/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/LDAPConfigDecorator.java
new file mode 100644
index 0000000..0da6df1
--- /dev/null
+++ b/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/LDAPConfigDecorator.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.storage.ldap.mappers;
+
+import org.keycloak.component.ComponentModel;
+import org.keycloak.storage.ldap.LDAPConfig;
+
+/**
+ *
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public interface LDAPConfigDecorator {
+
+ void updateLDAPConfig(LDAPConfig ldapConfig, ComponentModel mapperModel);
+
+}
diff --git a/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/membership/CommonLDAPGroupMapperConfig.java b/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/membership/CommonLDAPGroupMapperConfig.java
index a4ff175..01bcbb1 100644
--- a/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/membership/CommonLDAPGroupMapperConfig.java
+++ b/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/membership/CommonLDAPGroupMapperConfig.java
@@ -20,6 +20,7 @@ package org.keycloak.storage.ldap.mappers.membership;
import org.keycloak.component.ComponentModel;
import org.keycloak.models.LDAPConstants;
import org.keycloak.models.ModelException;
+import org.keycloak.storage.ldap.LDAPConfig;
import java.util.HashSet;
import java.util.Set;
@@ -35,6 +36,9 @@ public abstract class CommonLDAPGroupMapperConfig {
// See docs for MembershipType enum
public static final String MEMBERSHIP_ATTRIBUTE_TYPE = "membership.attribute.type";
+ // Used just for membershipType=UID. Name of LDAP attribute on user, which is used for membership mappings. Usually it will be "uid"
+ public static final String MEMBERSHIP_USER_LDAP_ATTRIBUTE = "membership.user.ldap.attribute";
+
// See docs for Mode enum
public static final String MODE = "mode";
@@ -58,6 +62,11 @@ public abstract class CommonLDAPGroupMapperConfig {
return (membershipType!=null && !membershipType.isEmpty()) ? Enum.valueOf(MembershipType.class, membershipType) : MembershipType.DN;
}
+ public String getMembershipUserLdapAttribute(LDAPConfig ldapConfig) {
+ String membershipUserAttrName = mapperModel.getConfig().getFirst(MEMBERSHIP_USER_LDAP_ATTRIBUTE);
+ return membershipUserAttrName!=null ? membershipUserAttrName : ldapConfig.getUsernameLdapAttribute();
+ }
+
public LDAPGroupMapperMode getMode() {
String modeString = mapperModel.getConfig().getFirst(MODE);
if (modeString == null || modeString.isEmpty()) {
diff --git a/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/membership/group/GroupLDAPStorageMapper.java b/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/membership/group/GroupLDAPStorageMapper.java
index fe4930c..fcdae3c 100644
--- a/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/membership/group/GroupLDAPStorageMapper.java
+++ b/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/membership/group/GroupLDAPStorageMapper.java
@@ -27,6 +27,7 @@ import org.keycloak.models.UserModel;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.models.utils.RoleUtils;
import org.keycloak.models.utils.UserModelDelegate;
+import org.keycloak.storage.ldap.LDAPConfig;
import org.keycloak.storage.ldap.LDAPStorageProvider;
import org.keycloak.storage.ldap.LDAPUtils;
import org.keycloak.storage.ldap.idm.model.LDAPDn;
@@ -450,11 +451,13 @@ public class GroupLDAPStorageMapper extends AbstractLDAPStorageMapper implements
LDAPObject ldapGroup = ldapGroupsMap.get(kcGroup.getName());
Set<LDAPDn> toRemoveSubgroupsDNs = getLDAPSubgroups(ldapGroup);
+ String membershipUserLdapAttrName = getMembershipUserLdapAttribute(); // Not applicable for groups, but needs to be here
+
// Add LDAP subgroups, which are KC subgroups
Set<GroupModel> kcSubgroups = kcGroup.getSubGroups();
for (GroupModel kcSubgroup : kcSubgroups) {
LDAPObject ldapSubgroup = ldapGroupsMap.get(kcSubgroup.getName());
- LDAPUtils.addMember(ldapProvider, MembershipType.DN, config.getMembershipLdapAttribute(), ldapGroup, ldapSubgroup, false);
+ LDAPUtils.addMember(ldapProvider, MembershipType.DN, config.getMembershipLdapAttribute(), membershipUserLdapAttrName, ldapGroup, ldapSubgroup, false);
toRemoveSubgroupsDNs.remove(ldapSubgroup.getDn());
}
@@ -462,7 +465,7 @@ public class GroupLDAPStorageMapper extends AbstractLDAPStorageMapper implements
for (LDAPDn toRemoveDN : toRemoveSubgroupsDNs) {
LDAPObject fakeGroup = new LDAPObject();
fakeGroup.setDn(toRemoveDN);
- LDAPUtils.deleteMember(ldapProvider, MembershipType.DN, config.getMembershipLdapAttribute(), ldapGroup, fakeGroup);
+ LDAPUtils.deleteMember(ldapProvider, MembershipType.DN, config.getMembershipLdapAttribute(), membershipUserLdapAttrName, ldapGroup, fakeGroup);
}
// Update group to LDAP
@@ -497,17 +500,22 @@ public class GroupLDAPStorageMapper extends AbstractLDAPStorageMapper implements
ldapGroup = loadLDAPGroupByName(groupName);
}
- LDAPUtils.addMember(ldapProvider, config.getMembershipTypeLdapAttribute(), config.getMembershipLdapAttribute(), ldapGroup, ldapUser, true);
+ String membershipUserLdapAttrName = getMembershipUserLdapAttribute();
+
+ LDAPUtils.addMember(ldapProvider, config.getMembershipTypeLdapAttribute(), config.getMembershipLdapAttribute(), membershipUserLdapAttrName, ldapGroup, ldapUser, true);
}
public void deleteGroupMappingInLDAP(LDAPObject ldapUser, LDAPObject ldapGroup) {
- LDAPUtils.deleteMember(ldapProvider, config.getMembershipTypeLdapAttribute(), config.getMembershipLdapAttribute(), ldapGroup, ldapUser);
+ String membershipUserLdapAttrName = getMembershipUserLdapAttribute();
+ LDAPUtils.deleteMember(ldapProvider, config.getMembershipTypeLdapAttribute(), config.getMembershipLdapAttribute(), membershipUserLdapAttrName, ldapGroup, ldapUser);
}
protected List<LDAPObject> getLDAPGroupMappings(LDAPObject ldapUser) {
String strategyKey = config.getUserGroupsRetrieveStrategy();
UserRolesRetrieveStrategy strategy = factory.getUserGroupsRetrieveStrategy(strategyKey);
- return strategy.getLDAPRoleMappings(this, ldapUser);
+
+ LDAPConfig ldapConfig = ldapProvider.getLdapIdentityStore().getConfig();
+ return strategy.getLDAPRoleMappings(this, ldapUser, ldapConfig);
}
@Override
@@ -555,6 +563,12 @@ public class GroupLDAPStorageMapper extends AbstractLDAPStorageMapper implements
}
+ protected String getMembershipUserLdapAttribute() {
+ LDAPConfig ldapConfig = ldapProvider.getLdapIdentityStore().getConfig();
+ return config.getMembershipUserLdapAttribute(ldapConfig);
+ }
+
+
public class LDAPGroupMappingsUserDelegate extends UserModelDelegate {
private final RealmModel realm;
@@ -604,8 +618,11 @@ public class GroupLDAPStorageMapper extends AbstractLDAPStorageMapper implements
LDAPQuery ldapQuery = createGroupQuery();
LDAPQueryConditionsBuilder conditionsBuilder = new LDAPQueryConditionsBuilder();
Condition roleNameCondition = conditionsBuilder.equal(config.getGroupNameLdapAttribute(), group.getName());
- String membershipUserAttr = LDAPUtils.getMemberValueOfChildObject(ldapUser, config.getMembershipTypeLdapAttribute());
+
+ String membershipUserLdapAttrName = getMembershipUserLdapAttribute();
+ String membershipUserAttr = LDAPUtils.getMemberValueOfChildObject(ldapUser, config.getMembershipTypeLdapAttribute(), membershipUserLdapAttrName);
Condition membershipCondition = conditionsBuilder.equal(config.getMembershipLdapAttribute(), membershipUserAttr);
+
ldapQuery.addWhereCondition(roleNameCondition).addWhereCondition(membershipCondition);
LDAPObject ldapGroup = ldapQuery.getFirstResult();
diff --git a/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/membership/group/GroupLDAPStorageMapperFactory.java b/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/membership/group/GroupLDAPStorageMapperFactory.java
index 3f018b7..3473372 100644
--- a/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/membership/group/GroupLDAPStorageMapperFactory.java
+++ b/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/membership/group/GroupLDAPStorageMapperFactory.java
@@ -34,6 +34,7 @@ import org.keycloak.storage.ldap.mappers.membership.CommonLDAPGroupMapperConfig;
import org.keycloak.storage.ldap.mappers.membership.LDAPGroupMapperMode;
import org.keycloak.storage.ldap.mappers.membership.MembershipType;
import org.keycloak.storage.ldap.mappers.membership.UserRolesRetrieveStrategy;
+import org.keycloak.storage.ldap.mappers.membership.role.RoleMapperConfig;
import java.util.HashMap;
import java.util.LinkedHashMap;
@@ -74,11 +75,14 @@ public class GroupLDAPStorageMapperFactory extends AbstractLDAPStorageMapperFact
private static List<ProviderConfigProperty> getProps(ComponentModel parent) {
String roleObjectClasses = LDAPConstants.GROUP_OF_NAMES;
String mode = LDAPGroupMapperMode.LDAP_ONLY.toString();
+ String membershipUserAttribute = LDAPConstants.UID;
if (parent != null) {
LDAPConfig config = new LDAPConfig(parent.getConfig());
roleObjectClasses = config.isActiveDirectory() ? LDAPConstants.GROUP : LDAPConstants.GROUP_OF_NAMES;
mode = config.getEditMode() == UserStorageProvider.EditMode.WRITABLE ? LDAPGroupMapperMode.LDAP_ONLY.toString() : LDAPGroupMapperMode.READ_ONLY.toString();
+ membershipUserAttribute = config.getUsernameLdapAttribute();
}
+
return ProviderConfigurationBuilder.create()
.property().name(GroupMapperConfig.GROUPS_DN)
.label("LDAP Groups DN")
@@ -106,7 +110,8 @@ public class GroupLDAPStorageMapperFactory extends AbstractLDAPStorageMapperFact
.add()
.property().name(GroupMapperConfig.MEMBERSHIP_LDAP_ATTRIBUTE)
.label("Membership LDAP Attribute")
- .helpText("Name of LDAP attribute on group, which is used for membership mappings. Usually it will be 'member' ")
+ .helpText("Name of LDAP attribute on group, which is used for membership mappings. Usually it will be 'member' ." +
+ "However when 'Membership Attribute Type' is 'UID' then 'Membership LDAP Attribute' could be typically 'memberUid' .")
.type(ProviderConfigProperty.STRING_TYPE)
.defaultValue(LDAPConstants.MEMBER)
.add()
@@ -118,6 +123,14 @@ public class GroupLDAPStorageMapperFactory extends AbstractLDAPStorageMapperFact
.options(MEMBERSHIP_TYPES)
.defaultValue(MembershipType.DN.toString())
.add()
+ .property().name(RoleMapperConfig.MEMBERSHIP_USER_LDAP_ATTRIBUTE)
+ .label("Membership User LDAP Attribute")
+ .helpText("Used just if Membership Attribute Type is UID. It is name of LDAP attribute on user, which is used for membership mappings. Usually it will be 'uid' . For example if value of " +
+ "'Membership User LDAP Attribute' is 'uid' and " +
+ " LDAP group has 'memberUid: john', then it is expected that particular LDAP user will have attribute 'uid: john' .")
+ .type(ProviderConfigProperty.STRING_TYPE)
+ .defaultValue(membershipUserAttribute)
+ .add()
.property().name(GroupMapperConfig.GROUPS_LDAP_FILTER)
.label("LDAP Filter")
.helpText("LDAP Filter adds additional custom filter to the whole query for retrieve LDAP groups. Leave this empty if no additional filtering is needed and you want to retrieve all groups from LDAP. Otherwise make sure that filter starts with '(' and ends with ')'")
diff --git a/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/membership/group/GroupMapperConfig.java b/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/membership/group/GroupMapperConfig.java
index 99cde4d..b305cb4 100644
--- a/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/membership/group/GroupMapperConfig.java
+++ b/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/membership/group/GroupMapperConfig.java
@@ -90,11 +90,6 @@ public class GroupMapperConfig extends CommonLDAPGroupMapperConfig {
return AbstractLDAPStorageMapper.parseBooleanParameter(mapperModel, PRESERVE_GROUP_INHERITANCE);
}
- public String getMembershipLdapAttribute() {
- String membershipAttrName = mapperModel.getConfig().getFirst(MEMBERSHIP_LDAP_ATTRIBUTE);
- return membershipAttrName!=null ? membershipAttrName : LDAPConstants.MEMBER;
- }
-
public Collection<String> getGroupObjectClasses(LDAPStorageProvider ldapProvider) {
String objectClasses = mapperModel.getConfig().getFirst(GROUP_OBJECT_CLASSES);
if (objectClasses == null) {
diff --git a/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/membership/MembershipType.java b/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/membership/MembershipType.java
index f2df5cd..894b2b4 100644
--- a/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/membership/MembershipType.java
+++ b/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/membership/MembershipType.java
@@ -57,7 +57,7 @@ public enum MembershipType {
protected Set<LDAPDn> getLDAPMembersWithParent(LDAPObject ldapGroup, String membershipLdapAttribute, LDAPDn requiredParentDn) {
Set<String> allMemberships = LDAPUtils.getExistingMemberships(membershipLdapAttribute, ldapGroup);
- // Filter and keep just groups
+ // Filter and keep just descendants of requiredParentDn
Set<LDAPDn> result = new HashSet<>();
for (String membership : allMemberships) {
LDAPDn childDn = LDAPDn.fromString(membership);
@@ -135,6 +135,9 @@ public enum MembershipType {
@Override
public List<UserModel> getGroupMembers(RealmModel realm, GroupLDAPStorageMapper groupMapper, LDAPObject ldapGroup, int firstResult, int maxResults) {
+ LDAPStorageProvider ldapProvider = groupMapper.getLdapProvider();
+ LDAPConfig ldapConfig = ldapProvider.getLdapIdentityStore().getConfig();
+
String memberAttrName = groupMapper.getConfig().getMembershipLdapAttribute();
Set<String> memberUids = LDAPUtils.getExistingMemberships(memberAttrName, ldapGroup);
@@ -146,7 +149,34 @@ public enum MembershipType {
int max = Math.min(memberUids.size(), firstResult + maxResults);
uids = uids.subList(firstResult, max);
- return groupMapper.getLdapProvider().loadUsersByUsernames(uids, realm);
+ String membershipUserAttrName = groupMapper.getConfig().getMembershipUserLdapAttribute(ldapConfig);
+
+ List<String> usernames;
+ if (membershipUserAttrName.equals(ldapConfig.getUsernameLdapAttribute())) {
+ usernames = uids; // Optimized version. No need to
+ } else {
+ usernames = new LinkedList<>();
+
+ LDAPQuery query = LDAPUtils.createQueryForUserSearch(ldapProvider, realm);
+ LDAPQueryConditionsBuilder conditionsBuilder = new LDAPQueryConditionsBuilder();
+
+ Condition[] orSubconditions = new Condition[uids.size()];
+ int index = 0;
+ for (String memberUid : uids) {
+ Condition condition = conditionsBuilder.equal(membershipUserAttrName, memberUid, EscapeStrategy.DEFAULT);
+ orSubconditions[index] = condition;
+ index++;
+ }
+ Condition orCondition = conditionsBuilder.orCondition(orSubconditions);
+ query.addWhereCondition(orCondition);
+ List<LDAPObject> ldapUsers = query.getResultList();
+ for (LDAPObject ldapUser : ldapUsers) {
+ String username = LDAPUtils.getUsername(ldapUser, ldapConfig);
+ usernames.add(username);
+ }
+ }
+
+ return groupMapper.getLdapProvider().loadUsersByUsernames(usernames, realm);
}
};
diff --git a/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/membership/role/RoleLDAPStorageMapper.java b/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/membership/role/RoleLDAPStorageMapper.java
index fd78877..1a4a5f9 100644
--- a/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/membership/role/RoleLDAPStorageMapper.java
+++ b/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/membership/role/RoleLDAPStorageMapper.java
@@ -27,6 +27,7 @@ import org.keycloak.models.RoleModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.utils.RoleUtils;
import org.keycloak.models.utils.UserModelDelegate;
+import org.keycloak.storage.ldap.LDAPConfig;
import org.keycloak.storage.ldap.LDAPStorageProvider;
import org.keycloak.storage.ldap.LDAPUtils;
import org.keycloak.storage.ldap.idm.model.LDAPObject;
@@ -252,11 +253,14 @@ public class RoleLDAPStorageMapper extends AbstractLDAPStorageMapper implements
ldapRole = createLDAPRole(roleName);
}
- LDAPUtils.addMember(ldapProvider, config.getMembershipTypeLdapAttribute(), config.getMembershipLdapAttribute(), ldapRole, ldapUser, true);
+ String membershipUserAttrName = getMembershipUserLdapAttribute();
+
+ LDAPUtils.addMember(ldapProvider, config.getMembershipTypeLdapAttribute(), config.getMembershipLdapAttribute(), membershipUserAttrName, ldapRole, ldapUser, true);
}
public void deleteRoleMappingInLDAP(LDAPObject ldapUser, LDAPObject ldapRole) {
- LDAPUtils.deleteMember(ldapProvider, config.getMembershipTypeLdapAttribute(), config.getMembershipLdapAttribute(), ldapRole, ldapUser);
+ String membershipUserAttrName = getMembershipUserLdapAttribute();
+ LDAPUtils.deleteMember(ldapProvider, config.getMembershipTypeLdapAttribute(), config.getMembershipLdapAttribute(), membershipUserAttrName, ldapRole, ldapUser);
}
public LDAPObject loadLDAPRoleByName(String roleName) {
@@ -269,7 +273,9 @@ public class RoleLDAPStorageMapper extends AbstractLDAPStorageMapper implements
protected List<LDAPObject> getLDAPRoleMappings(LDAPObject ldapUser) {
String strategyKey = config.getUserRolesRetrieveStrategy();
UserRolesRetrieveStrategy strategy = factory.getUserRolesRetrieveStrategy(strategyKey);
- return strategy.getLDAPRoleMappings(this, ldapUser);
+
+ LDAPConfig ldapConfig = ldapProvider.getLdapIdentityStore().getConfig();
+ return strategy.getLDAPRoleMappings(this, ldapUser, ldapConfig);
}
@Override
@@ -292,6 +298,11 @@ public class RoleLDAPStorageMapper extends AbstractLDAPStorageMapper implements
}
+ protected String getMembershipUserLdapAttribute() {
+ LDAPConfig ldapConfig = ldapProvider.getLdapIdentityStore().getConfig();
+ return config.getMembershipUserLdapAttribute(ldapConfig);
+ }
+
public class LDAPRoleMappingsUserDelegate extends UserModelDelegate {
@@ -422,8 +433,12 @@ public class RoleLDAPStorageMapper extends AbstractLDAPStorageMapper implements
LDAPQuery ldapQuery = createRoleQuery();
LDAPQueryConditionsBuilder conditionsBuilder = new LDAPQueryConditionsBuilder();
Condition roleNameCondition = conditionsBuilder.equal(config.getRoleNameLdapAttribute(), role.getName());
- String membershipUserAttr = LDAPUtils.getMemberValueOfChildObject(ldapUser, config.getMembershipTypeLdapAttribute());
+
+ String membershipUserAttrName = getMembershipUserLdapAttribute();
+ String membershipUserAttr = LDAPUtils.getMemberValueOfChildObject(ldapUser, config.getMembershipTypeLdapAttribute(), membershipUserAttrName);
+
Condition membershipCondition = conditionsBuilder.equal(config.getMembershipLdapAttribute(), membershipUserAttr);
+
ldapQuery.addWhereCondition(roleNameCondition).addWhereCondition(membershipCondition);
LDAPObject ldapRole = ldapQuery.getFirstResult();
diff --git a/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/membership/role/RoleLDAPStorageMapperFactory.java b/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/membership/role/RoleLDAPStorageMapperFactory.java
index f5c8dfe..2af7116 100644
--- a/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/membership/role/RoleLDAPStorageMapperFactory.java
+++ b/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/membership/role/RoleLDAPStorageMapperFactory.java
@@ -74,11 +74,14 @@ public class RoleLDAPStorageMapperFactory extends AbstractLDAPStorageMapperFacto
private static List<ProviderConfigProperty> getProps(ComponentModel parent) {
String roleObjectClasses = LDAPConstants.GROUP_OF_NAMES;
String mode = LDAPGroupMapperMode.LDAP_ONLY.toString();
+ String membershipUserAttribute = LDAPConstants.UID;
if (parent != null) {
LDAPConfig config = new LDAPConfig(parent.getConfig());
roleObjectClasses = config.isActiveDirectory() ? LDAPConstants.GROUP : LDAPConstants.GROUP_OF_NAMES;
mode = config.getEditMode() == UserStorageProvider.EditMode.WRITABLE ? LDAPGroupMapperMode.LDAP_ONLY.toString() : LDAPGroupMapperMode.READ_ONLY.toString();
+ membershipUserAttribute = config.getUsernameLdapAttribute();
}
+
return ProviderConfigurationBuilder.create()
.property().name(RoleMapperConfig.ROLES_DN)
.label("LDAP Roles DN")
@@ -99,7 +102,8 @@ public class RoleLDAPStorageMapperFactory extends AbstractLDAPStorageMapperFacto
.add()
.property().name(RoleMapperConfig.MEMBERSHIP_LDAP_ATTRIBUTE)
.label("Membership LDAP Attribute")
- .helpText("Name of LDAP attribute on role, which is used for membership mappings. Usually it will be 'member' ")
+ .helpText("Name of LDAP attribute on role, which is used for membership mappings. Usually it will be 'member' ." +
+ "However when 'Membership Attribute Type' is 'UID' then 'Membership LDAP Attribute' could be typically 'memberUid' .")
.type(ProviderConfigProperty.STRING_TYPE)
.defaultValue(LDAPConstants.MEMBER)
.add()
@@ -111,6 +115,14 @@ public class RoleLDAPStorageMapperFactory extends AbstractLDAPStorageMapperFacto
.options(MEMBERSHIP_TYPES)
.defaultValue(MembershipType.DN.toString())
.add()
+ .property().name(RoleMapperConfig.MEMBERSHIP_USER_LDAP_ATTRIBUTE)
+ .label("Membership User LDAP Attribute")
+ .helpText("Used just if Membership Attribute Type is UID. It is name of LDAP attribute on user, which is used for membership mappings. Usually it will be 'uid' . For example if value of " +
+ "'Membership User LDAP Attribute' is 'uid' and " +
+ " LDAP group has 'memberUid: john', then it is expected that particular LDAP user will have attribute 'uid: john' .")
+ .type(ProviderConfigProperty.STRING_TYPE)
+ .defaultValue(membershipUserAttribute)
+ .add()
.property().name(RoleMapperConfig.ROLES_LDAP_FILTER)
.label("LDAP Filter")
.helpText("LDAP Filter adds additional custom filter to the whole query for retrieve LDAP roles. Leave this empty if no additional filtering is needed and you want to retrieve all roles from LDAP. Otherwise make sure that filter starts with '(' and ends with ')'")
diff --git a/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/membership/UserRolesRetrieveStrategy.java b/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/membership/UserRolesRetrieveStrategy.java
index 29dd1d4..3ce4a42 100644
--- a/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/membership/UserRolesRetrieveStrategy.java
+++ b/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/membership/UserRolesRetrieveStrategy.java
@@ -19,6 +19,7 @@ package org.keycloak.storage.ldap.mappers.membership;
import org.keycloak.models.LDAPConstants;
+import org.keycloak.storage.ldap.LDAPConfig;
import org.keycloak.storage.ldap.LDAPUtils;
import org.keycloak.storage.ldap.idm.model.LDAPDn;
import org.keycloak.storage.ldap.idm.model.LDAPObject;
@@ -39,7 +40,7 @@ import java.util.Set;
public interface UserRolesRetrieveStrategy {
- List<LDAPObject> getLDAPRoleMappings(CommonLDAPGroupMapper roleOrGroupMapper, LDAPObject ldapUser);
+ List<LDAPObject> getLDAPRoleMappings(CommonLDAPGroupMapper roleOrGroupMapper, LDAPObject ldapUser, LDAPConfig ldapConfig);
void beforeUserLDAPQuery(LDAPQuery query);
@@ -52,11 +53,12 @@ public interface UserRolesRetrieveStrategy {
class LoadRolesByMember implements UserRolesRetrieveStrategy {
@Override
- public List<LDAPObject> getLDAPRoleMappings(CommonLDAPGroupMapper roleOrGroupMapper, LDAPObject ldapUser) {
+ public List<LDAPObject> getLDAPRoleMappings(CommonLDAPGroupMapper roleOrGroupMapper, LDAPObject ldapUser, LDAPConfig ldapConfig) {
LDAPQuery ldapQuery = roleOrGroupMapper.createLDAPGroupQuery();
String membershipAttr = roleOrGroupMapper.getConfig().getMembershipLdapAttribute();
- String userMembership = LDAPUtils.getMemberValueOfChildObject(ldapUser, roleOrGroupMapper.getConfig().getMembershipTypeLdapAttribute());
+ String membershipUserAttrName = roleOrGroupMapper.getConfig().getMembershipUserLdapAttribute(ldapConfig);
+ String userMembership = LDAPUtils.getMemberValueOfChildObject(ldapUser, roleOrGroupMapper.getConfig().getMembershipTypeLdapAttribute(), membershipUserAttrName);
Condition membershipCondition = getMembershipCondition(membershipAttr, userMembership);
ldapQuery.addWhereCondition(membershipCondition);
@@ -79,7 +81,7 @@ public interface UserRolesRetrieveStrategy {
class GetRolesFromUserMemberOfAttribute implements UserRolesRetrieveStrategy {
@Override
- public List<LDAPObject> getLDAPRoleMappings(CommonLDAPGroupMapper roleOrGroupMapper, LDAPObject ldapUser) {
+ public List<LDAPObject> getLDAPRoleMappings(CommonLDAPGroupMapper roleOrGroupMapper, LDAPObject ldapUser, LDAPConfig ldapConfig) {
Set<String> memberOfValues = ldapUser.getAttributeAsSet(LDAPConstants.MEMBER_OF);
if (memberOfValues == null) {
return Collections.emptyList();
diff --git a/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/UserAttributeLDAPStorageMapper.java b/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/UserAttributeLDAPStorageMapper.java
index 3b29091..8a9cf7d 100644
--- a/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/UserAttributeLDAPStorageMapper.java
+++ b/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/UserAttributeLDAPStorageMapper.java
@@ -80,6 +80,7 @@ public class UserAttributeLDAPStorageMapper extends AbstractLDAPStorageMapper {
public static final String READ_ONLY = "read.only";
public static final String ALWAYS_READ_VALUE_FROM_LDAP = "always.read.value.from.ldap";
public static final String IS_MANDATORY_IN_LDAP = "is.mandatory.in.ldap";
+ public static final String IS_BINARY_ATTRIBUTE = "is.binary.attribute";
public UserAttributeLDAPStorageMapper(ComponentModel mapperModel, LDAPStorageProvider ldapProvider) {
super(mapperModel, ldapProvider);
@@ -90,6 +91,12 @@ public class UserAttributeLDAPStorageMapper extends AbstractLDAPStorageMapper {
String userModelAttrName = mapperModel.getConfig().getFirst(USER_MODEL_ATTRIBUTE);
String ldapAttrName = mapperModel.getConfig().getFirst(LDAP_ATTRIBUTE);
+ // We won't update binary attributes to Keycloak DB. They might be too big
+ boolean isBinaryAttribute = mapperModel.get(IS_BINARY_ATTRIBUTE, false);
+ if (isBinaryAttribute) {
+ return;
+ }
+
Property<Object> userModelProperty = userModelProperties.get(userModelAttrName.toLowerCase());
if (userModelProperty != null) {
@@ -157,7 +164,7 @@ public class UserAttributeLDAPStorageMapper extends AbstractLDAPStorageMapper {
// throw ModelDuplicateException if there is different user in model with same email
protected void checkDuplicateEmail(String userModelAttrName, String email, RealmModel realm, KeycloakSession session, UserModel user) {
- if (email == null) return;
+ if (email == null || realm.isDuplicateEmailsAllowed()) return;
if (UserModel.EMAIL.equalsIgnoreCase(userModelAttrName)) {
// lowercase before search
email = KeycloakModelUtils.toLowerCaseSafe(email);
@@ -177,6 +184,7 @@ public class UserAttributeLDAPStorageMapper extends AbstractLDAPStorageMapper {
final String ldapAttrName = mapperModel.getConfig().getFirst(LDAP_ATTRIBUTE);
boolean isAlwaysReadValueFromLDAP = parseBooleanParameter(mapperModel, ALWAYS_READ_VALUE_FROM_LDAP);
final boolean isMandatoryInLdap = parseBooleanParameter(mapperModel, IS_MANDATORY_IN_LDAP);
+ final boolean isBinaryAttribute = parseBooleanParameter(mapperModel, IS_BINARY_ATTRIBUTE);
// For writable mode, we want to propagate writing of attribute to LDAP as well
if (ldapProvider.getEditMode() == UserStorageProvider.EditMode.WRITABLE && !isReadOnly()) {
@@ -185,20 +193,23 @@ public class UserAttributeLDAPStorageMapper extends AbstractLDAPStorageMapper {
@Override
public void setSingleAttribute(String name, String value) {
- setLDAPAttribute(name, value);
- super.setSingleAttribute(name, value);
+ if (setLDAPAttribute(name, value)) {
+ super.setSingleAttribute(name, value);
+ }
}
@Override
public void setAttribute(String name, List<String> values) {
- setLDAPAttribute(name, values);
- super.setAttribute(name, values);
+ if (setLDAPAttribute(name, values)) {
+ super.setAttribute(name, values);
+ }
}
@Override
public void removeAttribute(String name) {
- setLDAPAttribute(name, null);
- super.removeAttribute(name);
+ if ( setLDAPAttribute(name, null)) {
+ super.removeAttribute(name);
+ }
}
@Override
@@ -221,10 +232,10 @@ public class UserAttributeLDAPStorageMapper extends AbstractLDAPStorageMapper {
super.setFirstName(firstName);
}
- protected void setLDAPAttribute(String modelAttrName, Object value) {
+ protected boolean setLDAPAttribute(String modelAttrName, Object value) {
if (modelAttrName.equalsIgnoreCase(userModelAttrName)) {
- if (logger.isTraceEnabled()) {
- logger.tracef("Pushing user attribute to LDAP. username: %s, Model attribute name: %s, LDAP attribute name: %s, Attribute value: %s", getUsername(), modelAttrName, ldapAttrName, value);
+ if (UserAttributeLDAPStorageMapper.logger.isTraceEnabled()) {
+ UserAttributeLDAPStorageMapper.logger.tracef("Pushing user attribute to LDAP. username: %s, Model attribute name: %s, LDAP attribute name: %s, Attribute value: %s", getUsername(), modelAttrName, ldapAttrName, value);
}
ensureTransactionStarted();
@@ -245,7 +256,53 @@ public class UserAttributeLDAPStorageMapper extends AbstractLDAPStorageMapper {
ldapUser.setAttribute(ldapAttrName, new LinkedHashSet<>(asList));
}
}
+
+ if (isBinaryAttribute) {
+ UserAttributeLDAPStorageMapper.logger.debugf("Skip writing model attribute '%s' to DB for user '%s' as it is mapped to binary LDAP attribute.", userModelAttrName, getUsername());
+ return false;
+ } else {
+ return true;
+ }
}
+
+ return true;
+ }
+
+ };
+
+ } else if (isBinaryAttribute) {
+
+ delegate = new UserModelDelegate(delegate) {
+
+ @Override
+ public void setSingleAttribute(String name, String value) {
+ if (name.equalsIgnoreCase(userModelAttrName)) {
+ logSkipDBWrite();
+ } else {
+ super.setSingleAttribute(name, value);
+ }
+ }
+
+ @Override
+ public void setAttribute(String name, List<String> values) {
+ if (name.equalsIgnoreCase(userModelAttrName)) {
+ logSkipDBWrite();
+ } else {
+ super.setAttribute(name, values);
+ }
+ }
+
+ @Override
+ public void removeAttribute(String name) {
+ if (name.equalsIgnoreCase(userModelAttrName)) {
+ logSkipDBWrite();
+ } else {
+ super.removeAttribute(name);
+ }
+ }
+
+ private void logSkipDBWrite() {
+ logger.debugf("Skip writing model attribute '%s' to DB for user '%s' as it is mapped to binary LDAP attribute", userModelAttrName, getUsername());
}
};
diff --git a/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/UserAttributeLDAPStorageMapperFactory.java b/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/UserAttributeLDAPStorageMapperFactory.java
index fcc5525..eece55b 100755
--- a/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/UserAttributeLDAPStorageMapperFactory.java
+++ b/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/UserAttributeLDAPStorageMapperFactory.java
@@ -32,7 +32,7 @@ import java.util.List;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
-public class UserAttributeLDAPStorageMapperFactory extends AbstractLDAPStorageMapperFactory {
+public class UserAttributeLDAPStorageMapperFactory extends AbstractLDAPStorageMapperFactory implements LDAPConfigDecorator {
public static final String PROVIDER_ID = "user-attribute-ldap-mapper";
protected static final List<ProviderConfigProperty> configProperties;
@@ -69,6 +69,10 @@ public class UserAttributeLDAPStorageMapperFactory extends AbstractLDAPStorageMa
.helpText("If true, attribute is mandatory in LDAP. Hence if there is no value in Keycloak DB, the empty value will be set to be propagated to LDAP")
.type(ProviderConfigProperty.BOOLEAN_TYPE)
.defaultValue("false").add()
+ .property().name(UserAttributeLDAPStorageMapper.IS_BINARY_ATTRIBUTE).label("Is Binary Attribute")
+ .helpText("Should be true for binary LDAP attributes")
+ .type(ProviderConfigProperty.BOOLEAN_TYPE)
+ .defaultValue("false").add()
.build();
}
@@ -92,6 +96,12 @@ public class UserAttributeLDAPStorageMapperFactory extends AbstractLDAPStorageMa
checkMandatoryConfigAttribute(UserAttributeLDAPStorageMapper.USER_MODEL_ATTRIBUTE, "User Model Attribute", config);
checkMandatoryConfigAttribute(UserAttributeLDAPStorageMapper.LDAP_ATTRIBUTE, "LDAP Attribute", config);
+ boolean isBinaryAttribute = config.get(UserAttributeLDAPStorageMapper.IS_BINARY_ATTRIBUTE, false);
+ boolean alwaysReadValueFromLDAP = config.get(UserAttributeLDAPStorageMapper.ALWAYS_READ_VALUE_FROM_LDAP, false);
+ if (isBinaryAttribute && !alwaysReadValueFromLDAP) {
+ throw new ComponentValidationException("With Binary attribute enabled, the ''Always read value from LDAP'' must be enabled too");
+ }
+
}
@Override
@@ -103,4 +113,14 @@ public class UserAttributeLDAPStorageMapperFactory extends AbstractLDAPStorageMa
public List<ProviderConfigProperty> getConfigProperties(RealmModel realm, ComponentModel parent) {
return getConfigProps(parent);
}
+
+
+ @Override
+ public void updateLDAPConfig(LDAPConfig ldapConfig, ComponentModel mapperModel) {
+ boolean isBinaryAttribute = mapperModel.get(UserAttributeLDAPStorageMapper.IS_BINARY_ATTRIBUTE, false);
+ if (isBinaryAttribute) {
+ String ldapAttrName = mapperModel.getConfig().getFirst(UserAttributeLDAPStorageMapper.LDAP_ATTRIBUTE);
+ ldapConfig.addBinaryAttribute(ldapAttrName);
+ }
+ }
}
diff --git a/federation/sssd/src/main/java/cx/ath/matthew/LibraryLoader.java b/federation/sssd/src/main/java/cx/ath/matthew/LibraryLoader.java
index 4088d46..7279a36 100644
--- a/federation/sssd/src/main/java/cx/ath/matthew/LibraryLoader.java
+++ b/federation/sssd/src/main/java/cx/ath/matthew/LibraryLoader.java
@@ -21,7 +21,14 @@ package cx.ath.matthew;
*/
public class LibraryLoader {
- private static final String[] PATHS = {"/usr/lib/", "/usr/lib64/", "/usr/local/lib/", "/opt/local/lib/"};
+ private static final String[] PATHS = {
+ "/opt/rh/rh-sso7/root/lib/",
+ "/opt/rh/rh-sso7/root/lib64/",
+ "/usr/lib/",
+ "/usr/lib64/",
+ "/usr/local/lib/",
+ "/opt/local/lib/"
+ };
private static final String LIBRARY_NAME = "libunix_dbus_java";
private static final String VERSION = "0.0.8";
private static boolean loadSucceeded;
diff --git a/federation/sssd/src/main/java/cx/ath/matthew/unix/UnixSocket.java b/federation/sssd/src/main/java/cx/ath/matthew/unix/UnixSocket.java
index 8851637..1c59dd7 100644
--- a/federation/sssd/src/main/java/cx/ath/matthew/unix/UnixSocket.java
+++ b/federation/sssd/src/main/java/cx/ath/matthew/unix/UnixSocket.java
@@ -26,7 +26,6 @@
*/
package cx.ath.matthew.unix;
-import cx.ath.matthew.LibraryLoader;
import cx.ath.matthew.debug.Debug;
import java.io.IOException;
@@ -37,9 +36,6 @@ import java.io.OutputStream;
* Represents a UnixSocket.
*/
public class UnixSocket {
- static {
- LibraryLoader.load();
- }
private native void native_set_pass_cred(int sock, boolean passcred) throws IOException;
diff --git a/federation/sssd/src/main/java/org/freedesktop/sssd/infopipe/InfoPipe.java b/federation/sssd/src/main/java/org/freedesktop/sssd/infopipe/InfoPipe.java
index 6152d26..9ef979c 100644
--- a/federation/sssd/src/main/java/org/freedesktop/sssd/infopipe/InfoPipe.java
+++ b/federation/sssd/src/main/java/org/freedesktop/sssd/infopipe/InfoPipe.java
@@ -34,11 +34,13 @@ public interface InfoPipe extends DBusInterface {
String OBJECTPATH = "/org/freedesktop/sssd/infopipe";
String BUSNAME = "org.freedesktop.sssd.infopipe";
-
@DBusMemberName("GetUserAttr")
Map<String, Variant> getUserAttributes(String user, List<String> attr);
@DBusMemberName("GetUserGroups")
List<String> getUserGroups(String user);
+ @DBusMemberName("Ping")
+ String ping(String ping);
+
}
\ No newline at end of file
diff --git a/federation/sssd/src/main/java/org/keycloak/federation/sssd/api/Sssd.java b/federation/sssd/src/main/java/org/keycloak/federation/sssd/api/Sssd.java
index 9551579..308d596 100644
--- a/federation/sssd/src/main/java/org/keycloak/federation/sssd/api/Sssd.java
+++ b/federation/sssd/src/main/java/org/keycloak/federation/sssd/api/Sssd.java
@@ -17,15 +17,13 @@
package org.keycloak.federation.sssd.api;
+import cx.ath.matthew.LibraryLoader;
import org.freedesktop.dbus.DBusConnection;
import org.freedesktop.dbus.Variant;
import org.freedesktop.dbus.exceptions.DBusException;
import org.freedesktop.sssd.infopipe.InfoPipe;
import org.jboss.logging.Logger;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.Paths;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
@@ -52,7 +50,8 @@ public class Sssd {
public Sssd(String username) {
this.username = username;
try {
- dBusConnection = DBusConnection.getConnection(DBusConnection.SYSTEM);
+ if (LibraryLoader.load().succeed())
+ dBusConnection = DBusConnection.getConnection(DBusConnection.SYSTEM);
} catch (DBusException e) {
e.printStackTrace();
}
@@ -96,14 +95,20 @@ public class Sssd {
public static boolean isAvailable() {
boolean sssdAvailable = false;
try {
- Path path = Paths.get("/etc/sssd");
- if (!Files.exists(path)) {
- logger.debugv("SSSD is not available in your system. Federation provider will be disabled.");
+ if (LibraryLoader.load().succeed()) {
+ DBusConnection connection = DBusConnection.getConnection(DBusConnection.SYSTEM);
+ InfoPipe infoPipe = connection.getRemoteObject(InfoPipe.BUSNAME, InfoPipe.OBJECTPATH, InfoPipe.class);
+
+ if (infoPipe.ping("PING") == null || infoPipe.ping("PING").isEmpty()) {
+ logger.debugv("SSSD is not available in your system. Federation provider will be disabled.");
+ } else {
+ sssdAvailable = true;
+ }
} else {
- sssdAvailable = true;
+ logger.debugv("The RPM libunix-dbus-java is not installed. SSSD Federation provider will be disabled.");
}
} catch (Exception e) {
- logger.error("SSSD check failed", e);
+ logger.debugv("SSSD is not available in your system. Federation provider will be disabled.", e);
}
return sssdAvailable;
}
diff --git a/integration/admin-client/src/main/java/org/keycloak/admin/client/Config.java b/integration/admin-client/src/main/java/org/keycloak/admin/client/Config.java
index 4e628e5..183f486 100644
--- a/integration/admin-client/src/main/java/org/keycloak/admin/client/Config.java
+++ b/integration/admin-client/src/main/java/org/keycloak/admin/client/Config.java
@@ -110,7 +110,7 @@ public class Config {
}
public static void checkGrantType(String grantType) {
- if (!PASSWORD.equals(grantType) && !CLIENT_CREDENTIALS.equals(grantType)) {
+ if (grantType != null && !PASSWORD.equals(grantType) && !CLIENT_CREDENTIALS.equals(grantType)) {
throw new IllegalArgumentException("Unsupported grantType: " + grantType +
" (only " + PASSWORD + " and " + CLIENT_CREDENTIALS + " are supported)");
}
diff --git a/integration/admin-client/src/main/java/org/keycloak/admin/client/Keycloak.java b/integration/admin-client/src/main/java/org/keycloak/admin/client/Keycloak.java
index d267d17..8c4235c 100755
--- a/integration/admin-client/src/main/java/org/keycloak/admin/client/Keycloak.java
+++ b/integration/admin-client/src/main/java/org/keycloak/admin/client/Keycloak.java
@@ -43,18 +43,22 @@ import static org.keycloak.OAuth2Constants.PASSWORD;
public class Keycloak {
private final Config config;
private final TokenManager tokenManager;
+ private String authToken;
private final ResteasyWebTarget target;
private final ResteasyClient client;
- Keycloak(String serverUrl, String realm, String username, String password, String clientId, String clientSecret, String grantType, ResteasyClient resteasyClient) {
+ Keycloak(String serverUrl, String realm, String username, String password, String clientId, String clientSecret, String grantType, ResteasyClient resteasyClient, String authtoken) {
config = new Config(serverUrl, realm, username, password, clientId, clientSecret, grantType);
client = resteasyClient != null ? resteasyClient : new ResteasyClientBuilder().connectionPoolSize(10).build();
-
- tokenManager = new TokenManager(config, client);
+ authToken = authtoken;
+ tokenManager = authtoken == null ? new TokenManager(config, client) : null;
target = client.target(config.getServerUrl());
+ target.register(newAuthFilter());
+ }
- target.register(new BearerAuthFilter(tokenManager));
+ private BearerAuthFilter newAuthFilter() {
+ return authToken != null ? new BearerAuthFilter(authToken) : new BearerAuthFilter(tokenManager);
}
public static Keycloak getInstance(String serverUrl, String realm, String username, String password, String clientId, String clientSecret, SSLContext sslContext) {
@@ -63,15 +67,19 @@ public class Keycloak {
.hostnameVerification(ResteasyClientBuilder.HostnameVerificationPolicy.WILDCARD)
.connectionPoolSize(10).build();
- return new Keycloak(serverUrl, realm, username, password, clientId, clientSecret, PASSWORD, client);
+ return new Keycloak(serverUrl, realm, username, password, clientId, clientSecret, PASSWORD, client, null);
}
public static Keycloak getInstance(String serverUrl, String realm, String username, String password, String clientId, String clientSecret) {
- return new Keycloak(serverUrl, realm, username, password, clientId, clientSecret, PASSWORD, null);
+ return new Keycloak(serverUrl, realm, username, password, clientId, clientSecret, PASSWORD, null, null);
}
public static Keycloak getInstance(String serverUrl, String realm, String username, String password, String clientId) {
- return new Keycloak(serverUrl, realm, username, password, clientId, null, PASSWORD, null);
+ return new Keycloak(serverUrl, realm, username, password, clientId, null, PASSWORD, null, null);
+ }
+
+ public static Keycloak getInstance(String serverUrl, String realm, String clientId, String authtoken) {
+ return new Keycloak(serverUrl, realm, null, null, clientId, null, PASSWORD, null, null);
}
public RealmsResource realms() {
@@ -100,7 +108,7 @@ public class Keycloak {
* @return
*/
public <T> T proxy(Class<T> proxyClass, URI absoluteURI) {
- return client.target(absoluteURI).register(new BearerAuthFilter(tokenManager)).proxy(proxyClass);
+ return client.target(absoluteURI).register(newAuthFilter()).proxy(proxyClass);
}
/**
diff --git a/integration/admin-client/src/main/java/org/keycloak/admin/client/KeycloakBuilder.java b/integration/admin-client/src/main/java/org/keycloak/admin/client/KeycloakBuilder.java
index e192d9a..0c003d3 100644
--- a/integration/admin-client/src/main/java/org/keycloak/admin/client/KeycloakBuilder.java
+++ b/integration/admin-client/src/main/java/org/keycloak/admin/client/KeycloakBuilder.java
@@ -60,8 +60,9 @@ public class KeycloakBuilder {
private String password;
private String clientId;
private String clientSecret;
- private String grantType = PASSWORD;
+ private String grantType;
private ResteasyClient resteasyClient;
+ private String authorization;
public KeycloakBuilder serverUrl(String serverUrl) {
this.serverUrl = serverUrl;
@@ -104,6 +105,11 @@ public class KeycloakBuilder {
return this;
}
+ public KeycloakBuilder authorization(String auth) {
+ this.authorization = auth;
+ return this;
+ }
+
/**
* Builds a new Keycloak client from this builder.
*/
@@ -116,6 +122,10 @@ public class KeycloakBuilder {
throw new IllegalStateException("realm required");
}
+ if (authorization == null && grantType == null) {
+ grantType = PASSWORD;
+ }
+
if (PASSWORD.equals(grantType)) {
if (username == null) {
throw new IllegalStateException("username required");
@@ -130,11 +140,11 @@ public class KeycloakBuilder {
}
}
- if (clientId == null) {
+ if (authorization == null && clientId == null) {
throw new IllegalStateException("clientId required");
}
- return new Keycloak(serverUrl, realm, username, password, clientId, clientSecret, grantType, resteasyClient);
+ return new Keycloak(serverUrl, realm, username, password, clientId, clientSecret, grantType, resteasyClient, authorization);
}
private KeycloakBuilder() {
diff --git a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/BearerAuthFilter.java b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/BearerAuthFilter.java
index 514aeea..0d4992f 100644
--- a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/BearerAuthFilter.java
+++ b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/BearerAuthFilter.java
@@ -49,8 +49,10 @@ public class BearerAuthFilter implements ClientRequestFilter, ClientResponseFilt
@Override
public void filter(ClientRequestContext requestContext) throws IOException {
- String authHeader = AUTH_HEADER_PREFIX + (tokenManager != null ? tokenManager.getAccessTokenString() : tokenString);
-
+ String authHeader = (tokenManager != null ? tokenManager.getAccessTokenString() : tokenString);
+ if (!authHeader.startsWith(AUTH_HEADER_PREFIX)) {
+ authHeader = AUTH_HEADER_PREFIX + authHeader;
+ }
requestContext.getHeaders().add(HttpHeaders.AUTHORIZATION, authHeader);
}
diff --git a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/PolicyResource.java b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/PolicyResource.java
index 9a45045..28202f5 100644
--- a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/PolicyResource.java
+++ b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/PolicyResource.java
@@ -16,16 +16,21 @@
*/
package org.keycloak.admin.client.resource;
-import org.jboss.resteasy.annotations.cache.NoCache;
-import org.keycloak.representations.idm.authorization.PolicyRepresentation;
+import java.util.List;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.PUT;
+import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
+import org.jboss.resteasy.annotations.cache.NoCache;
+import org.keycloak.representations.idm.authorization.PolicyRepresentation;
+import org.keycloak.representations.idm.authorization.ResourceRepresentation;
+import org.keycloak.representations.idm.authorization.ScopeRepresentation;
+
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
@@ -42,4 +47,28 @@ public interface PolicyResource {
@DELETE
void remove();
+
+ @Path("/associatedPolicies")
+ @GET
+ @Produces(MediaType.APPLICATION_JSON)
+ @NoCache
+ List<PolicyRepresentation> associatedPolicies();
+
+ @Path("/dependentPolicies")
+ @GET
+ @Produces(MediaType.APPLICATION_JSON)
+ @NoCache
+ List<PolicyRepresentation> dependentPolicies();
+
+ @Path("/scopes")
+ @GET
+ @Produces("application/json")
+ @NoCache
+ List<ScopeRepresentation> scopes();
+
+ @Path("/resources")
+ @GET
+ @Produces("application/json")
+ @NoCache
+ List<ResourceRepresentation> resources();
}
diff --git a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/ResourceResource.java b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/ResourceResource.java
index 834cb06..28e57a7 100644
--- a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/ResourceResource.java
+++ b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/ResourceResource.java
@@ -16,13 +16,17 @@
*/
package org.keycloak.admin.client.resource;
+import java.util.List;
+
import org.jboss.resteasy.annotations.cache.NoCache;
+import org.keycloak.representations.idm.authorization.PolicyRepresentation;
import org.keycloak.representations.idm.authorization.ResourceRepresentation;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.PUT;
+import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
@@ -42,4 +46,10 @@ public interface ResourceResource {
@DELETE
void remove();
+
+ @Path("permissions")
+ @GET
+ @NoCache
+ @Produces(MediaType.APPLICATION_JSON)
+ List<PolicyRepresentation> permissions();
}
diff --git a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/ResourceScopeResource.java b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/ResourceScopeResource.java
index 6975574..87b285b 100644
--- a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/ResourceScopeResource.java
+++ b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/ResourceScopeResource.java
@@ -16,13 +16,17 @@
*/
package org.keycloak.admin.client.resource;
+import java.util.List;
+
import org.jboss.resteasy.annotations.cache.NoCache;
+import org.keycloak.representations.idm.authorization.PolicyRepresentation;
import org.keycloak.representations.idm.authorization.ScopeRepresentation;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.PUT;
+import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
@@ -42,4 +46,9 @@ public interface ResourceScopeResource {
@DELETE
void remove();
+
+ @Path("/permissions")
+ @GET
+ @Produces(MediaType.APPLICATION_JSON)
+ List<PolicyRepresentation> permissions();
}
integration/client-cli/admin-cli/pom.xml 177(+177 -0)
diff --git a/integration/client-cli/admin-cli/pom.xml b/integration/client-cli/admin-cli/pom.xml
new file mode 100755
index 0000000..90676bf
--- /dev/null
+++ b/integration/client-cli/admin-cli/pom.xml
@@ -0,0 +1,177 @@
+<?xml version="1.0"?>
+<!--
+ ~ Copyright 2016 Red Hat, Inc. and/or its affiliates
+ ~ and other contributors as indicated by the @author tags.
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+ <parent>
+ <artifactId>keycloak-client-cli-parent</artifactId>
+ <groupId>org.keycloak</groupId>
+ <version>2.5.0.Final-SNAPSHOT</version>
+ </parent>
+ <modelVersion>4.0.0</modelVersion>
+
+ <artifactId>keycloak-admin-cli</artifactId>
+ <name>Keycloak Admin CLI</name>
+ <description/>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.jboss.aesh</groupId>
+ <artifactId>aesh</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-core</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.httpcomponents</groupId>
+ <artifactId>httpclient</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>junit</groupId>
+ <artifactId>junit</artifactId>
+ </dependency>
+ </dependencies>
+
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-shade-plugin</artifactId>
+ <!--version>2.4.3</version-->
+ <executions>
+ <execution>
+ <phase>package</phase>
+ <goals>
+ <goal>shade</goal>
+ </goals>
+ <configuration>
+ <filters>
+ <filter>
+ <artifact>org.keycloak:keycloak-core</artifact>
+ <includes>
+ <include>org/keycloak/util/**</include>
+ <include>org/keycloak/json/**</include>
+ <include>org/keycloak/jose/jws/**</include>
+ <include>org/keycloak/jose/jwk/**</include>
+ <include>org/keycloak/representations/adapters/config/**</include>
+ <include>org/keycloak/representations/adapters/action/**</include>
+ <include>org/keycloak/representations/AccessTokenResponse.class</include>
+ <!--
+ <include>org/keycloak/representations/idm/ClientRepresentation.class</include>
+ <include>org/keycloak/representations/idm/RealmRepresentation.class</include>
+ <include>org/keycloak/representations/idm/UserRepresentation.class</include>
+ <include>org/keycloak/representations/idm/RoleRepresentation.class</include>
+ <include>org/keycloak/representations/idm/RoleRepresentation.class</include>
+ <include>org/keycloak/representations/idm/RolesRepresentation.class</include>
+ <include>org/keycloak/representations/idm/ScopeMappingRepresentation.class</include>
+ <include>org/keycloak/representations/idm/UserFederationMapperRepresentation.class</include>
+ <include>org/keycloak/representations/idm/ProtocolMapperRepresentation.class</include>
+ <include>org/keycloak/representations/idm/IdentityProviderRepresentation.class</include>
+ <include>org/keycloak/representations/idm/authorization/**</include>
+ -->
+ <include>org/keycloak/representations/idm/**</include>
+ <include>org/keycloak/representations/JsonWebToken.class</include>
+ </includes>
+ </filter>
+ <filter>
+ <artifact>org.keycloak:keycloak-common</artifact>
+ <includes>
+ <include>org/keycloak/common/util/**</include>
+ </includes>
+ </filter>
+ <filter>
+ <artifact>org.bouncycastle:bcprov-jdk15on</artifact>
+ <includes>
+ <include>**/**</include>
+ </includes>
+ </filter>
+ <filter>
+ <artifact>org.bouncycastle:bcpkix-jdk15on</artifact>
+ <excludes>
+ <exclude>**/**</exclude>
+ </excludes>
+ </filter>
+ <filter>
+ <artifact>com.fasterxml.jackson.core:jackson-core</artifact>
+ <includes>
+ <include>**/**</include>
+ </includes>
+ </filter>
+ <filter>
+ <artifact>com.fasterxml.jackson.core:jackson-databind</artifact>
+ <includes>
+ <include>**/**</include>
+ </includes>
+ </filter>
+ <filter>
+ <artifact>com.fasterxml.jackson.core:jackson-annotations</artifact>
+ <includes>
+ <include>com/fasterxml/jackson/annotation/**</include>
+ </includes>
+ </filter>
+ <filter>
+ <artifact>org.jboss.resteasy:resteasy-client</artifact>
+ <includes>
+ <include>**/**</include>
+ </includes>
+ </filter>
+ <filter>
+ <artifact>org.jboss.resteasy:resteasy-jaxrs</artifact>
+ <includes>
+ <include>**/**</include>
+ </includes>
+ </filter>
+ <filter>
+ <artifact>org.jboss.resteasy:resteasy-jackson2-provider</artifact>
+ <includes>
+ <include>**/**</include>
+ </includes>
+ </filter>
+ <filter>
+ <artifact>org.jboss.spec.javax.ws.rs:jboss-jaxrs-api_2.0_spec</artifact>
+ <includes>
+ <include>**/**</include>
+ </includes>
+ </filter>
+ <filter>
+ <artifact>*:*</artifact>
+ <excludes>
+ <exclude>META-INF/*.SF</exclude>
+ <exclude>META-INF/*.DSA</exclude>
+ <exclude>META-INF/*.RSA</exclude>
+ </excludes>
+ </filter>
+ </filters>
+ </configuration>
+ </execution>
+ </executions>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-compiler-plugin</artifactId>
+ <configuration>
+ <source>1.8</source>
+ <target>1.8</target>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+
+</project>
diff --git a/integration/client-cli/admin-cli/src/main/bin/kcadm.bat b/integration/client-cli/admin-cli/src/main/bin/kcadm.bat
new file mode 100644
index 0000000..46dc256
--- /dev/null
+++ b/integration/client-cli/admin-cli/src/main/bin/kcadm.bat
@@ -0,0 +1,8 @@
+@echo off
+
+if "%OS%" == "Windows_NT" (
+ set "DIRNAME=%~dp0%"
+) else (
+ set DIRNAME=.\
+)
+java %KC_OPTS% -cp %DIRNAME%\client\keycloak-admin-cli-${project.version}.jar org.keycloak.client.admin.cli.KcAdmMain %*
diff --git a/integration/client-cli/admin-cli/src/main/bin/kcadm.sh b/integration/client-cli/admin-cli/src/main/bin/kcadm.sh
new file mode 100755
index 0000000..26df7e1
--- /dev/null
+++ b/integration/client-cli/admin-cli/src/main/bin/kcadm.sh
@@ -0,0 +1,23 @@
+#!/bin/sh
+case "`uname`" in
+ CYGWIN*)
+ CFILE = `cygpath "$0"`
+ RESOLVED_NAME=`readlink -f "$CFILE"`
+ ;;
+ Darwin*)
+ RESOLVED_NAME=`readlink "$0"`
+ ;;
+ FreeBSD)
+ RESOLVED_NAME=`readlink -f "$0"`
+ ;;
+ Linux)
+ RESOLVED_NAME=`readlink -f "$0"`
+ ;;
+esac
+
+if [ "x$RESOLVED_NAME" = "x" ]; then
+ RESOLVED_NAME="$0"
+fi
+
+DIRNAME=`dirname "$RESOLVED_NAME"`
+java $KC_OPTS -cp $DIRNAME/client/keycloak-admin-cli-${project.version}.jar org.keycloak.client.admin.cli.KcAdmMain "$@"
\ No newline at end of file
diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/aesh/AeshConsoleCallbackImpl.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/aesh/AeshConsoleCallbackImpl.java
new file mode 100644
index 0000000..fedaaa0
--- /dev/null
+++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/aesh/AeshConsoleCallbackImpl.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.client.admin.cli.aesh;
+
+import org.jboss.aesh.cl.parser.OptionParserException;
+import org.jboss.aesh.cl.result.ResultHandler;
+import org.jboss.aesh.console.AeshConsoleCallback;
+import org.jboss.aesh.console.AeshConsoleImpl;
+import org.jboss.aesh.console.ConsoleOperation;
+import org.jboss.aesh.console.command.CommandNotFoundException;
+import org.jboss.aesh.console.command.CommandResult;
+import org.jboss.aesh.console.command.container.CommandContainer;
+import org.jboss.aesh.console.command.container.CommandContainerResult;
+import org.jboss.aesh.console.command.invocation.AeshCommandInvocation;
+import org.jboss.aesh.console.command.invocation.AeshCommandInvocationProvider;
+import org.jboss.aesh.parser.AeshLine;
+import org.jboss.aesh.parser.ParserStatus;
+
+import java.lang.reflect.Method;
+
+class AeshConsoleCallbackImpl extends AeshConsoleCallback {
+
+ private final AeshConsoleImpl console;
+ private CommandResult result;
+
+ AeshConsoleCallbackImpl(AeshConsoleImpl aeshConsole) {
+ this.console = aeshConsole;
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public int execute(ConsoleOperation output) throws InterruptedException {
+ if (output != null && output.getBuffer().trim().length() > 0) {
+ ResultHandler resultHandler = null;
+ //AeshLine aeshLine = Parser.findAllWords(output.getBuffer());
+ AeshLine aeshLine = new AeshLine(output.getBuffer(), Globals.args, ParserStatus.OK, "");
+ try (CommandContainer commandContainer = getCommand(output, aeshLine)) {
+ resultHandler = commandContainer.getParser().getProcessedCommand().getResultHandler();
+ CommandContainerResult ccResult =
+ commandContainer.executeCommand(aeshLine, console.getInvocationProviders(), console.getAeshContext(),
+ new AeshCommandInvocationProvider().enhanceCommandInvocation(
+ new AeshCommandInvocation(console,
+ output.getControlOperator(), output.getPid(), this)));
+
+ result = ccResult.getCommandResult();
+
+ if(result == CommandResult.SUCCESS && resultHandler != null)
+ resultHandler.onSuccess();
+ else if(resultHandler != null)
+ resultHandler.onFailure(result);
+
+ if (result == CommandResult.FAILURE) {
+ // we assume the command has already output any error messages
+ System.exit(1);
+ }
+ } catch (Exception e) {
+ console.stop();
+
+ if (e instanceof OptionParserException) {
+ System.err.println("Unknown command: " + aeshLine.getWords().get(0));
+ } else {
+ System.err.println(e.getMessage());
+ }
+ if (Globals.dumpTrace) {
+ e.printStackTrace();
+ }
+
+ System.exit(1);
+ }
+ }
+ // empty line
+ else if (output != null) {
+ result = CommandResult.FAILURE;
+ }
+ else {
+ //stop();
+ result = CommandResult.FAILURE;
+ }
+
+ if (result == CommandResult.SUCCESS) {
+ return 0;
+ } else {
+ return 1;
+ }
+ }
+
+ private CommandContainer getCommand(ConsoleOperation output, AeshLine aeshLine) throws CommandNotFoundException {
+ Method m;
+ try {
+ m = console.getClass().getDeclaredMethod("getCommand", AeshLine.class, String.class);
+ } catch (NoSuchMethodException e) {
+ throw new RuntimeException("Unexpected error: ", e);
+ }
+
+ m.setAccessible(true);
+
+ try {
+ return (CommandContainer) m.invoke(console, aeshLine, output.getBuffer());
+ } catch (Exception e) {
+ throw new RuntimeException("Unexpected error: ", e);
+ }
+ }
+}
diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/aesh/AeshEnhancer.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/aesh/AeshEnhancer.java
new file mode 100644
index 0000000..9e21b18
--- /dev/null
+++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/aesh/AeshEnhancer.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.client.admin.cli.aesh;
+
+import org.jboss.aesh.console.AeshConsoleImpl;
+import org.jboss.aesh.console.Console;
+
+import java.lang.reflect.Field;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public class AeshEnhancer {
+
+ public static void enhance(AeshConsoleImpl console) {
+ try {
+ Globals.stdin.setConsole(console);
+
+ Field field = AeshConsoleImpl.class.getDeclaredField("console");
+ field.setAccessible(true);
+ Console internalConsole = (Console) field.get(console);
+ internalConsole.setConsoleCallback(new AeshConsoleCallbackImpl(console));
+ } catch (Exception e) {
+ throw new RuntimeException("Failed to install Aesh enhancement", e);
+ }
+ }
+}
diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/aesh/Globals.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/aesh/Globals.java
new file mode 100644
index 0000000..e16a43c
--- /dev/null
+++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/aesh/Globals.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.client.admin.cli.aesh;
+
+import java.util.List;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public class Globals {
+
+ public static boolean dumpTrace = false;
+
+ public static ValveInputStream stdin;
+
+ public static List<String> args;
+}
diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/aesh/ValveInputStream.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/aesh/ValveInputStream.java
new file mode 100644
index 0000000..ec2cf4f
--- /dev/null
+++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/aesh/ValveInputStream.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.client.admin.cli.aesh;
+
+import org.jboss.aesh.console.AeshConsoleImpl;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InterruptedIOException;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.LinkedBlockingQueue;
+
+/**
+ * This stream blocks and waits, until there is a stream in the queue.
+ * It reads the stream to the end, then stops Aesh console.
+ *
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public class ValveInputStream extends InputStream {
+
+ private BlockingQueue<InputStream> queue = new LinkedBlockingQueue<>(10);
+
+ private InputStream current;
+
+ private AeshConsoleImpl console;
+
+ @Override
+ public int read() throws IOException {
+ if (current == null) {
+ try {
+ current = queue.take();
+ } catch (InterruptedException e) {
+ throw new InterruptedIOException("Signalled to exit");
+ }
+ }
+ int c = current.read();
+ if (c == -1) {
+ //current = null;
+ if (console != null) {
+ console.stop();
+ }
+ }
+
+ return c;
+ }
+
+ /**
+ * For some reason AeshInputStream wants to do blocking read of whole buffers, which for stdin
+ * results in blocked input.
+ */
+ @Override
+ public int read(byte b[], int off, int len) throws IOException {
+ int c = read();
+ if (c == -1) {
+ return c;
+ }
+ b[off] = (byte) c;
+ return 1;
+ }
+
+ public void setInputStream(InputStream is) {
+ if (queue.contains(is)) {
+ return;
+ }
+ queue.add(is);
+ }
+
+ public void setConsole(AeshConsoleImpl console) {
+ this.console = console;
+ }
+
+ public boolean isStdinAvailable() {
+ return console.isRunning();
+ }
+}
diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/AbstractAuthOptionsCmd.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/AbstractAuthOptionsCmd.java
new file mode 100644
index 0000000..1473d7f
--- /dev/null
+++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/AbstractAuthOptionsCmd.java
@@ -0,0 +1,267 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.client.admin.cli.commands;
+
+import org.jboss.aesh.cl.Option;
+import org.jboss.aesh.console.command.invocation.CommandInvocation;
+import org.keycloak.client.admin.cli.config.ConfigData;
+import org.keycloak.client.admin.cli.config.ConfigHandler;
+import org.keycloak.client.admin.cli.config.FileConfigHandler;
+import org.keycloak.client.admin.cli.config.InMemoryConfigHandler;
+import org.keycloak.client.admin.cli.config.RealmConfigData;
+import org.keycloak.client.admin.cli.util.ConfigUtil;
+import org.keycloak.client.admin.cli.util.HttpUtil;
+import org.keycloak.client.admin.cli.util.IoUtil;
+
+import java.io.File;
+
+import static org.keycloak.client.admin.cli.config.FileConfigHandler.setConfigFile;
+import static org.keycloak.client.admin.cli.util.ConfigUtil.DEFAULT_CLIENT;
+import static org.keycloak.client.admin.cli.util.ConfigUtil.checkAuthInfo;
+import static org.keycloak.client.admin.cli.util.ConfigUtil.checkServerInfo;
+import static org.keycloak.client.admin.cli.util.ConfigUtil.loadConfig;
+
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public abstract class AbstractAuthOptionsCmd extends AbstractGlobalOptionsCmd {
+
+ @Option(shortName = 'a', name = "admin-root", description = "URL of Admin REST endpoint root if not default - e.g. http://localhost:8080/auth/admin")
+ String adminRestRoot;
+
+ @Option(name = "config", description = "Path to the config file (~/.keycloak/kcadm.config by default)")
+ String config;
+
+ @Option(name = "no-config", description = "No configuration file should be used, no authentication info should be saved", hasValue = false)
+ boolean noconfig;
+
+ @Option(name = "server", description = "Server endpoint url (e.g. 'http://localhost:8080/auth')")
+ String server;
+
+ @Option(shortName = 'r', name = "target-realm", description = "Realm to target - when it's different than the realm we authenticate against")
+ String targetRealm;
+
+ @Option(name = "realm", description = "Realm name to authenticate against")
+ String realm;
+
+ @Option(name = "client", description = "Realm name to authenticate against")
+ String clientId;
+
+ @Option(name = "user", description = "Username to login with")
+ String user;
+
+ @Option(name = "password", description = "Password to login with (prompted for if not specified and --user is used)")
+ String password;
+
+ @Option(name = "secret", description = "Secret to authenticate the client (prompted for if no --user or --keystore is specified)")
+ String secret;
+
+ @Option(name = "keystore", description = "Path to a keystore containing private key")
+ String keystore;
+
+ @Option(name = "storepass", description = "Keystore password (prompted for if not specified and --keystore is used)")
+ String storePass;
+
+ @Option(name = "keypass", description = "Key password (prompted for if not specified and --keystore is used without --storepass, \n otherwise defaults to keystore password)")
+ String keyPass;
+
+ @Option(name = "alias", description = "Alias of the key inside a keystore (defaults to the value of ClientId)")
+ String alias;
+
+ @Option(name = "truststore", description = "Path to a truststore")
+ String trustStore;
+
+ @Option(name = "trustpass", description = "Truststore password (prompted for if not specified and --truststore is used)")
+ String trustPass;
+
+
+ protected void initFromParent(AbstractAuthOptionsCmd parent) {
+
+ super.initFromParent(parent);
+
+ noconfig = parent.noconfig;
+ config = parent.config;
+ server = parent.server;
+ realm = parent.realm;
+ clientId = parent.clientId;
+ user = parent.user;
+ password = parent.password;
+ secret = parent.secret;
+ keystore = parent.keystore;
+ storePass = parent.storePass;
+ keyPass = parent.keyPass;
+ alias = parent.alias;
+ trustStore = parent.trustStore;
+ trustPass = parent.trustPass;
+ }
+
+ protected void applyDefaultOptionValues() {
+ if (clientId == null) {
+ clientId = DEFAULT_CLIENT;
+ }
+ }
+
+ protected boolean noOptions() {
+ return server == null && realm == null && clientId == null && secret == null &&
+ user == null && password == null &&
+ keystore == null && storePass == null && keyPass == null && alias == null &&
+ trustStore == null && trustPass == null && config == null && (args == null || args.size() == 0);
+ }
+
+
+ protected String getTargetRealm(ConfigData config) {
+ return targetRealm != null ? targetRealm : config.getRealm();
+ }
+
+ protected void processGlobalOptions() {
+
+ super.processGlobalOptions();
+
+ if (config != null && noconfig) {
+ throw new RuntimeException("Options --config and --no-config are mutually exclusive");
+ }
+
+ if (!noconfig) {
+ setConfigFile(config != null ? config : ConfigUtil.DEFAULT_CONFIG_FILE_PATH);
+ ConfigUtil.setHandler(new FileConfigHandler());
+ } else {
+ InMemoryConfigHandler handler = new InMemoryConfigHandler();
+ ConfigData data = new ConfigData();
+ initConfigData(data);
+ handler.setConfigData(data);
+ ConfigUtil.setHandler(handler);
+ }
+ }
+
+ protected void setupTruststore(ConfigData configData, CommandInvocation invocation ) {
+
+ if (!configData.getServerUrl().startsWith("https:")) {
+ return;
+ }
+
+ String truststore = trustStore;
+ if (truststore == null) {
+ truststore = configData.getTruststore();
+ }
+
+ if (truststore != null) {
+ String pass = trustPass;
+ if (pass == null) {
+ pass = configData.getTrustpass();
+ }
+ if (pass == null) {
+ pass = IoUtil.readSecret("Enter truststore password: ", invocation);
+ }
+
+ try {
+ HttpUtil.setTruststore(new File(truststore), pass);
+ } catch (Exception e) {
+ throw new RuntimeException("Failed to load truststore: " + truststore, e);
+ }
+ }
+ }
+
+ protected ConfigData ensureAuthInfo(ConfigData config, CommandInvocation commandInvocation) {
+
+ if (requiresLogin()) {
+ // make sure current handler is in-memory handler
+ // restore it at the end
+ ConfigHandler old = ConfigUtil.getHandler();
+ try {
+ // make sure all defaults are initialized after this point
+ applyDefaultOptionValues();
+
+ initConfigData(config);
+ ConfigUtil.setupInMemoryHandler(config);
+
+ ConfigCredentialsCmd login = new ConfigCredentialsCmd();
+ login.initFromParent(this);
+ login.init(config);
+ login.process(commandInvocation);
+
+ // this must be executed before finally block which restores config handler
+ return loadConfig();
+
+ } catch (RuntimeException e) {
+ throw e;
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ } finally {
+ ConfigUtil.setHandler(old);
+ }
+
+ } else {
+ checkAuthInfo(config);
+
+ // make sure all defaults are initialized after this point
+ applyDefaultOptionValues();
+ return loadConfig();
+ }
+ }
+
+ protected boolean requiresLogin() {
+ return user != null || password != null || secret != null || keystore != null
+ || keyPass != null || storePass != null || alias != null;
+ }
+
+ protected ConfigData copyWithServerInfo(ConfigData config) {
+
+ ConfigData result = config.deepcopy();
+
+ if (server != null) {
+ result.setServerUrl(server);
+ }
+ if (realm != null) {
+ result.setRealm(realm);
+ }
+
+ checkServerInfo(result);
+ return result;
+ }
+
+ private void initConfigData(ConfigData data) {
+ if (server != null)
+ data.setServerUrl(server);
+ if (realm != null)
+ data.setRealm(realm);
+ if (trustStore != null)
+ data.setTruststore(trustStore);
+
+ RealmConfigData rdata = data.sessionRealmConfigData();
+ if (clientId != null)
+ rdata.setClientId(clientId);
+ if (secret != null)
+ rdata.setSecret(secret);
+ }
+
+ protected void checkUnsupportedOptions(String ... options) {
+ if (options.length % 2 != 0) {
+ throw new IllegalArgumentException("Even number of argument required");
+ }
+
+ for (int i = 0; i < options.length; i++) {
+ String name = options[i];
+ String value = options[++i];
+
+ if (value != null) {
+ throw new RuntimeException("Unsupported option: " + name);
+ }
+ }
+ }
+
+}
diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/AbstractGlobalOptionsCmd.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/AbstractGlobalOptionsCmd.java
new file mode 100644
index 0000000..c49c83c
--- /dev/null
+++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/AbstractGlobalOptionsCmd.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.client.admin.cli.commands;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.jboss.aesh.cl.Arguments;
+import org.jboss.aesh.cl.Option;
+import org.jboss.aesh.console.command.Command;
+import org.keycloak.client.admin.cli.aesh.Globals;
+import org.keycloak.client.admin.cli.util.FilterUtil;
+import org.keycloak.client.admin.cli.util.ReturnFields;
+
+import java.io.IOException;
+import java.util.Iterator;
+import java.util.List;
+
+import static org.keycloak.client.admin.cli.util.HttpUtil.normalize;
+import static org.keycloak.client.admin.cli.util.IoUtil.printOut;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public abstract class AbstractGlobalOptionsCmd implements Command {
+
+ @Option(shortName = 'x', description = "Print full stack trace when exiting with error", hasValue = false)
+ boolean dumpTrace;
+
+ @Option(name = "help", description = "Print command specific help", hasValue = false)
+ boolean help;
+
+
+ // we don't want Aesh to handle illegal options
+ @Arguments
+ List<String> args;
+
+
+ protected void initFromParent(AbstractGlobalOptionsCmd parent) {
+ dumpTrace = parent.dumpTrace;
+ help = parent.help;
+ args = parent.args;
+ }
+
+ protected void processGlobalOptions() {
+ Globals.dumpTrace = dumpTrace;
+ }
+
+ protected boolean printHelp() {
+ if (help || nothingToDo()) {
+ printOut(help());
+ return true;
+ }
+
+ return false;
+ }
+
+ protected boolean nothingToDo() {
+ return false;
+ }
+
+ protected String help() {
+ return KcAdmCmd.usage();
+ }
+
+ protected String composeAdminRoot(String server) {
+ return normalize(server) + "admin";
+ }
+
+
+ protected void requireValue(Iterator<String> it, String option) {
+ if (!it.hasNext()) {
+ throw new IllegalArgumentException("Option " + option + " requires a value");
+ }
+ }
+
+ protected String extractTypeNameFromUri(String resourceUrl) {
+ String type = extractLastComponentOfUri(resourceUrl);
+ if (type.endsWith("s")) {
+ type = type.substring(0, type.length()-1);
+ }
+ return type;
+ }
+
+ protected String extractLastComponentOfUri(String resourceUrl) {
+ int endPos = resourceUrl.endsWith("/") ? resourceUrl.length()-2 : resourceUrl.length()-1;
+ int pos = resourceUrl.lastIndexOf("/", endPos);
+ pos = pos == -1 ? 0 : pos;
+ return resourceUrl.substring(pos+1, endPos+1);
+ }
+
+ protected JsonNode applyFieldFilter(ObjectMapper mapper, JsonNode rootNode, ReturnFields returnFields) {
+ // construct new JsonNode that satisfies filtering specified by returnFields
+ try {
+ return FilterUtil.copyFilteredObject(rootNode, returnFields);
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to apply fields filter", e);
+ }
+ }
+}
diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/AbstractRequestCmd.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/AbstractRequestCmd.java
new file mode 100644
index 0000000..84d9579
--- /dev/null
+++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/AbstractRequestCmd.java
@@ -0,0 +1,435 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.client.admin.cli.commands;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import org.jboss.aesh.console.command.CommandException;
+import org.jboss.aesh.console.command.CommandResult;
+import org.jboss.aesh.console.command.invocation.CommandInvocation;
+import org.keycloak.client.admin.cli.common.AttributeOperation;
+import org.keycloak.client.admin.cli.common.CmdStdinContext;
+import org.keycloak.client.admin.cli.config.ConfigData;
+import org.keycloak.client.admin.cli.util.AccessibleBufferOutputStream;
+import org.keycloak.client.admin.cli.util.Header;
+import org.keycloak.client.admin.cli.util.Headers;
+import org.keycloak.client.admin.cli.util.HeadersBody;
+import org.keycloak.client.admin.cli.util.HeadersBodyStatus;
+import org.keycloak.client.admin.cli.util.HttpUtil;
+import org.keycloak.client.admin.cli.util.OutputFormat;
+import org.keycloak.client.admin.cli.util.ReflectionUtil;
+import org.keycloak.client.admin.cli.util.ReturnFields;
+
+import java.io.BufferedInputStream;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.Charset;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+import static org.keycloak.client.admin.cli.common.AttributeOperation.Type.DELETE;
+import static org.keycloak.client.admin.cli.common.AttributeOperation.Type.SET;
+import static org.keycloak.client.admin.cli.util.AuthUtil.ensureToken;
+import static org.keycloak.client.admin.cli.util.ConfigUtil.credentialsAvailable;
+import static org.keycloak.client.admin.cli.util.ConfigUtil.loadConfig;
+import static org.keycloak.client.admin.cli.util.HttpUtil.checkSuccess;
+import static org.keycloak.client.admin.cli.util.HttpUtil.composeResourceUrl;
+import static org.keycloak.client.admin.cli.util.HttpUtil.doGet;
+import static org.keycloak.client.admin.cli.util.IoUtil.copyStream;
+import static org.keycloak.client.admin.cli.util.IoUtil.printErr;
+import static org.keycloak.client.admin.cli.util.IoUtil.printOut;
+import static org.keycloak.client.admin.cli.util.OutputUtil.MAPPER;
+import static org.keycloak.client.admin.cli.util.OutputUtil.printAsCsv;
+import static org.keycloak.client.admin.cli.util.ParseUtil.mergeAttributes;
+import static org.keycloak.client.admin.cli.util.ParseUtil.parseFileOrStdin;
+import static org.keycloak.client.admin.cli.util.ParseUtil.parseKeyVal;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public abstract class AbstractRequestCmd extends AbstractAuthOptionsCmd {
+
+ String file;
+
+ String fields;
+
+ boolean printHeaders;
+
+ boolean returnId;
+
+ boolean outputResult;
+
+ boolean compressed;
+
+ boolean unquoted;
+
+ boolean mergeMode;
+
+ boolean noMerge;
+
+ Integer offset;
+
+ Integer limit;
+
+ String format = "json";
+
+ OutputFormat outputFormat;
+
+ String httpVerb;
+
+ Headers headers = new Headers();
+
+ List<AttributeOperation> attrs = new LinkedList<>();
+
+ Map<String, String> filter = new HashMap<>();
+
+ String url = null;
+
+
+ @Override
+ public CommandResult execute(CommandInvocation commandInvocation) throws CommandException, InterruptedException {
+ try {
+ initOptions();
+
+ if (printHelp()) {
+ return help ? CommandResult.SUCCESS : CommandResult.FAILURE;
+ }
+
+ processGlobalOptions();
+
+ processOptions(commandInvocation);
+
+ return process(commandInvocation);
+ } catch (IllegalArgumentException e) {
+ throw new IllegalArgumentException(e.getMessage() + suggestHelp(), e);
+ } finally {
+ commandInvocation.stop();
+ }
+ }
+
+ abstract void initOptions();
+
+ abstract String suggestHelp();
+
+
+ void processOptions(CommandInvocation commandInvocation) {
+
+ if (args == null || args.isEmpty()) {
+ throw new IllegalArgumentException("URI not specified");
+ }
+
+ Iterator<String> it = args.iterator();
+
+ while (it.hasNext()) {
+ String option = it.next();
+ switch (option) {
+ case "-s":
+ case "--set": {
+ if (!it.hasNext()) {
+ throw new IllegalArgumentException("Option " + option + " requires a value");
+ }
+ String[] keyVal = parseKeyVal(it.next());
+ attrs.add(new AttributeOperation(SET, keyVal[0], keyVal[1]));
+ break;
+ }
+ case "-d":
+ case "--delete": {
+ attrs.add(new AttributeOperation(DELETE, it.next()));
+ break;
+ }
+ case "-h":
+ case "--header": {
+ requireValue(it, option);
+ String[] keyVal = parseKeyVal(it.next());
+ headers.add(keyVal[0], keyVal[1]);
+ break;
+ }
+ case "-q":
+ case "--query": {
+ if (!it.hasNext()) {
+ throw new IllegalArgumentException("Option " + option + " requires a value");
+ }
+ String arg = it.next();
+ String[] keyVal;
+ if (arg.indexOf("=") == -1) {
+ keyVal = new String[] {"", arg};
+ } else {
+ keyVal = parseKeyVal(arg);
+ }
+ filter.put(keyVal[0], keyVal[1]);
+ break;
+ }
+ default: {
+ if (url == null) {
+ url = option;
+ } else {
+ throw new IllegalArgumentException("Invalid option: " + option);
+ }
+ }
+ }
+ }
+
+
+ if (url == null) {
+ throw new IllegalArgumentException("Resource URI not specified");
+ }
+
+ if (outputResult && returnId) {
+ throw new IllegalArgumentException("Options -o and -i are mutually exclusive");
+ }
+
+ try {
+ outputFormat = OutputFormat.valueOf(format.toUpperCase());
+ } catch (Exception e) {
+ throw new RuntimeException("Unsupported output format: " + format);
+ }
+
+ if (mergeMode && noMerge) {
+ throw new IllegalArgumentException("Options --merge and --no-merge are mutually exclusive");
+ }
+
+ if (file == null && attrs.size() > 0 && !noMerge) {
+ mergeMode = true;
+ }
+ }
+
+
+
+ public CommandResult process(CommandInvocation commandInvocation) throws CommandException, InterruptedException {
+
+ // see if Content-Type header is explicitly set to non-json value
+ Header ctype = headers.get("content-type");
+
+ InputStream body = null;
+
+ CmdStdinContext<JsonNode> ctx = new CmdStdinContext<>();
+
+ if (file != null) {
+ if (ctype != null && !"application/json".equals(ctype.getValue())) {
+ if ("-".equals(file)) {
+ body = System.in;
+ } else {
+ try {
+ body = new BufferedInputStream(new FileInputStream(file));
+ } catch (FileNotFoundException e) {
+ throw new RuntimeException("File not found: " + file);
+ }
+ }
+ } else {
+ ctx = parseFileOrStdin(file);
+ }
+ }
+
+ ConfigData config = loadConfig();
+ config = copyWithServerInfo(config);
+
+ setupTruststore(config, commandInvocation);
+
+ String auth = null;
+
+ config = ensureAuthInfo(config, commandInvocation);
+ config = copyWithServerInfo(config);
+ if (credentialsAvailable(config)) {
+ auth = ensureToken(config);
+ }
+
+ auth = auth != null ? "Bearer " + auth : null;
+
+ if (auth != null) {
+ headers.addIfMissing("Authorization", auth);
+ }
+
+
+ final String server = config.getServerUrl();
+ final String realm = getTargetRealm(config);
+ final String adminRoot = adminRestRoot != null ? adminRestRoot : composeAdminRoot(server);
+
+
+ String resourceUrl = composeResourceUrl(adminRoot, realm, url);
+ String typeName = extractTypeNameFromUri(resourceUrl);
+
+
+ if (filter.size() > 0) {
+ resourceUrl = HttpUtil.addQueryParamsToUri(resourceUrl, filter);
+ }
+
+ headers.addIfMissing("Accept", "application/json");
+
+ if (isUpdate() && mergeMode) {
+ ObjectNode result;
+ HeadersBodyStatus response;
+ try {
+ response = HttpUtil.doGet(resourceUrl, new HeadersBody(headers));
+ checkSuccess(resourceUrl, response);
+
+ ByteArrayOutputStream buffer = new ByteArrayOutputStream();
+ copyStream(response.getBody(), buffer);
+
+ result = MAPPER.readValue(buffer.toByteArray(), ObjectNode.class);
+
+ } catch (IOException e) {
+ throw new RuntimeException("HTTP request error: " + e.getMessage(), e);
+ }
+
+ CmdStdinContext<JsonNode> ctxremote = new CmdStdinContext<>();
+ ctxremote.setResult(result);
+
+ // merge local representation over remote one
+ if (ctx.getResult() != null) {
+ ReflectionUtil.merge(ctx.getResult(), (ObjectNode) ctxremote.getResult());
+ }
+ ctx = ctxremote;
+ }
+
+ if (attrs.size() > 0) {
+ if (body != null) {
+ throw new RuntimeException("Can't set attributes on content of type other than application/json");
+ }
+
+ ctx = mergeAttributes(ctx, MAPPER.createObjectNode(), attrs);
+ }
+
+ if (body == null && ctx.getContent() != null) {
+ body = new ByteArrayInputStream(ctx.getContent().getBytes(Charset.forName("utf-8")));
+ }
+
+ ReturnFields returnFields = null;
+
+ if (fields != null) {
+ returnFields = new ReturnFields(fields);
+ }
+
+ // make sure content type is set
+ if (body != null) {
+ headers.addIfMissing("Content-Type", "application/json");
+ }
+
+ LinkedHashMap<String, String> queryParams = new LinkedHashMap<>();
+ if (offset != null) {
+ queryParams.put("first", String.valueOf(offset));
+ }
+ if (limit != null) {
+ queryParams.put("max", String.valueOf(limit));
+ }
+ if (queryParams.size() > 0) {
+ resourceUrl = HttpUtil.addQueryParamsToUri(resourceUrl, queryParams);
+ }
+
+ HeadersBodyStatus response;
+ try {
+ response = HttpUtil.doRequest(httpVerb, resourceUrl, new HeadersBody(headers, body));
+ } catch (IOException e) {
+ throw new RuntimeException("HTTP request error: " + e.getMessage(), e);
+ }
+
+ // output response
+ if (printHeaders) {
+ printOut(response.getStatus());
+ for (Header header : response.getHeaders()) {
+ printOut(header.getName() + ": " + header.getValue());
+ }
+ }
+
+ checkSuccess(resourceUrl, response);
+
+ AccessibleBufferOutputStream abos = new AccessibleBufferOutputStream(System.out);
+ if (response.getBody() == null) {
+ throw new RuntimeException("Internal error - response body should never be null");
+ }
+
+ if (printHeaders) {
+ printOut("");
+ }
+
+
+ Header location = response.getHeaders().get("Location");
+ String id = location != null ? extractLastComponentOfUri(location.getValue()) : null;
+ if (id != null) {
+ if (returnId) {
+ printOut(id);
+ } else if (!outputResult) {
+ printErr("Created new " + typeName + " with id '" + id + "'");
+ }
+ }
+
+ if (outputResult) {
+
+ if (isCreateOrUpdate() && (response.getStatusCode() == 204 || id != null)) {
+ // get object for id
+ headers = new Headers();
+ if (auth != null) {
+ headers.add("Authorization", auth);
+ }
+ try {
+ String fetchUrl = id != null ? (resourceUrl + "/" + id) : resourceUrl;
+ response = doGet(fetchUrl, new HeadersBody(headers));
+ } catch (IOException e) {
+ throw new RuntimeException("HTTP request error: " + e.getMessage(), e);
+ }
+ }
+
+ Header contentType = response.getHeaders().get("content-type");
+ boolean canPrettyPrint = contentType != null && contentType.getValue().equals("application/json");
+ boolean pretty = !compressed;
+
+ if (canPrettyPrint && (pretty || returnFields != null)) {
+ ByteArrayOutputStream buffer = new ByteArrayOutputStream();
+ copyStream(response.getBody(), buffer);
+
+ try {
+ JsonNode rootNode = MAPPER.readValue(buffer.toByteArray(), JsonNode.class);
+ if (returnFields != null) {
+ rootNode = applyFieldFilter(MAPPER, rootNode, returnFields);
+ }
+ if (outputFormat == OutputFormat.JSON) {
+ // now pretty print it to output
+ MAPPER.writeValue(abos, rootNode);
+ } else {
+ printAsCsv(rootNode, returnFields, unquoted);
+ }
+ } catch (Exception ignored) {
+ copyStream(new ByteArrayInputStream(buffer.toByteArray()), abos);
+ }
+ } else {
+ copyStream(response.getBody(), abos);
+ }
+ }
+
+ int lastByte = abos.getLastByte();
+ if (lastByte != -1 && lastByte != 13 && lastByte != 10) {
+ printErr("");
+ }
+
+ return CommandResult.SUCCESS;
+ }
+
+ private boolean isUpdate() {
+ return "put".equals(httpVerb);
+ }
+
+ private boolean isCreateOrUpdate() {
+ return "post".equals(httpVerb) || "put".equals(httpVerb);
+ }
+}
diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/AddRolesCmd.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/AddRolesCmd.java
new file mode 100644
index 0000000..fecd042
--- /dev/null
+++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/AddRolesCmd.java
@@ -0,0 +1,334 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.client.admin.cli.commands;
+
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import org.jboss.aesh.cl.CommandDefinition;
+import org.jboss.aesh.cl.Option;
+import org.jboss.aesh.console.command.CommandException;
+import org.jboss.aesh.console.command.CommandResult;
+import org.jboss.aesh.console.command.invocation.CommandInvocation;
+import org.keycloak.client.admin.cli.config.ConfigData;
+import org.keycloak.client.admin.cli.operations.ClientOperations;
+import org.keycloak.client.admin.cli.operations.GroupOperations;
+import org.keycloak.client.admin.cli.operations.RoleOperations;
+import org.keycloak.client.admin.cli.operations.LocalSearch;
+import org.keycloak.client.admin.cli.operations.UserOperations;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Set;
+
+import static org.keycloak.client.admin.cli.util.AuthUtil.ensureToken;
+import static org.keycloak.client.admin.cli.util.ConfigUtil.DEFAULT_CONFIG_FILE_STRING;
+import static org.keycloak.client.admin.cli.util.ConfigUtil.credentialsAvailable;
+import static org.keycloak.client.admin.cli.util.ConfigUtil.loadConfig;
+import static org.keycloak.client.admin.cli.util.OsUtil.CMD;
+import static org.keycloak.client.admin.cli.util.OsUtil.EOL;
+import static org.keycloak.client.admin.cli.util.OsUtil.PROMPT;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+@CommandDefinition(name = "add-roles", description = "[ARGUMENTS]")
+public class AddRolesCmd extends AbstractAuthOptionsCmd {
+
+ @Option(name = "uusername", description = "Target user's 'username'")
+ String uusername;
+
+ @Option(name = "uid", description = "Target user's 'id'")
+ String uid;
+
+ @Option(name = "gname", description = "Target group's 'name'")
+ String gname;
+
+ @Option(name = "gpath", description = "Target group's 'path'")
+ String gpath;
+
+ @Option(name = "gid", description = "Target group's 'id'")
+ String gid;
+
+ @Option(name = "cclientid", description = "Target client's 'clientId'")
+ String cclientid;
+
+ @Option(name = "cid", description = "Target client's 'id'")
+ String cid;
+
+ @Override
+ public CommandResult execute(CommandInvocation commandInvocation) throws CommandException, InterruptedException {
+
+ List<String> roleNames = new LinkedList<>();
+ List<String> roleIds = new LinkedList<>();
+
+ try {
+ if (printHelp()) {
+ return help ? CommandResult.SUCCESS : CommandResult.FAILURE;
+ }
+
+ processGlobalOptions();
+
+ Iterator<String> it = args.iterator();
+
+ while (it.hasNext()) {
+ String option = it.next();
+ switch (option) {
+ case "--rolename": {
+ optionRequiresValueCheck(it, option);
+ roleNames.add(it.next());
+ break;
+ }
+ case "--roleid": {
+ optionRequiresValueCheck(it, option);
+ roleIds.add(it.next());
+ break;
+ }
+ default: {
+ throw new IllegalArgumentException("Invalid option: " + option);
+ }
+ }
+ }
+
+ if (uid != null && uusername != null) {
+ throw new IllegalArgumentException("Incompatible options: --uid and --uusername are mutually exclusive");
+ }
+
+ if ((gid != null && gname != null) || (gid != null && gpath != null) || (gname != null && gpath != null)) {
+ throw new IllegalArgumentException("Incompatible options: --gid, --gname and --gpath are mutually exclusive");
+ }
+
+ if (roleNames.isEmpty() && roleIds.isEmpty()) {
+ throw new IllegalArgumentException("No role specified. Use --rolename or --roleid to specify roles");
+ }
+
+ if (cid != null && cclientid != null) {
+ throw new IllegalArgumentException("Incompatible options: --cid and --cclientid are mutually exclusive");
+ }
+
+ if (isUserSpecified() && isGroupSpecified()) {
+ throw new IllegalArgumentException("Incompatible options: --uusername / --uid can't be used at the same time as --gname / --gid / --gpath");
+ }
+
+ if (!isUserSpecified() && !isGroupSpecified()) {
+ throw new IllegalArgumentException("No user nor group specified. Use --uusername / --uid to specify user or --gname / --gid / --gpath to specify group");
+ }
+
+
+ ConfigData config = loadConfig();
+ config = copyWithServerInfo(config);
+
+ setupTruststore(config, commandInvocation);
+
+ String auth = null;
+
+ config = ensureAuthInfo(config, commandInvocation);
+ config = copyWithServerInfo(config);
+ if (credentialsAvailable(config)) {
+ auth = ensureToken(config);
+ }
+
+ auth = auth != null ? "Bearer " + auth : null;
+
+ final String server = config.getServerUrl();
+ final String realm = getTargetRealm(config);
+ final String adminRoot = adminRestRoot != null ? adminRestRoot : composeAdminRoot(server);
+
+
+ if (isUserSpecified()) {
+ if (uid == null) {
+ uid = UserOperations.getIdFromUsername(adminRoot, realm, auth, uusername);
+ }
+ if (isClientSpecified()) {
+ // list client roles for a user
+ if (cid == null) {
+ cid = ClientOperations.getIdFromClientId(adminRoot, realm, auth, cclientid);
+ }
+
+ List<ObjectNode> roles = RoleOperations.getClientRoles(adminRoot, realm, cid, auth);
+ Set<ObjectNode> rolesToAdd = getRoleRepresentations(roleNames, roleIds, new LocalSearch(roles));
+
+ // now add all the roles
+ UserOperations.addClientRoles(adminRoot, realm, auth, uid, cid, new ArrayList<>(rolesToAdd));
+
+ } else {
+
+ Set<ObjectNode> rolesToAdd = getRoleRepresentations(roleNames, roleIds,
+ new LocalSearch(RoleOperations.getRealmRolesAsNodes(adminRoot, realm, auth)));
+
+ // now add all the roles
+ UserOperations.addRealmRoles(adminRoot, realm, auth, uid, new ArrayList<>(rolesToAdd));
+ }
+
+ } else if (isGroupSpecified()) {
+ if (gname != null) {
+ gid = GroupOperations.getIdFromName(adminRoot, realm, auth, gname);
+ } else if (gpath != null) {
+ gid = GroupOperations.getIdFromPath(adminRoot, realm, auth, gpath);
+ }
+ if (isClientSpecified()) {
+ // list client roles for a group
+ if (cid == null) {
+ cid = ClientOperations.getIdFromClientId(adminRoot, realm, auth, cclientid);
+ }
+
+ List<ObjectNode> roles = RoleOperations.getClientRoles(adminRoot, realm, cid, auth);
+ Set<ObjectNode> rolesToAdd = getRoleRepresentations(roleNames, roleIds, new LocalSearch(roles));
+
+ // now add all the roles
+ GroupOperations.addClientRoles(adminRoot, realm, auth, gid, cid, new ArrayList<>(rolesToAdd));
+
+ } else {
+
+ Set<ObjectNode> rolesToAdd = getRoleRepresentations(roleNames, roleIds,
+ new LocalSearch(RoleOperations.getRealmRolesAsNodes(adminRoot, realm, auth)));
+
+ // now add all the roles
+ GroupOperations.addRealmRoles(adminRoot, realm, auth, gid, new ArrayList<>(rolesToAdd));
+ }
+
+ } else {
+
+ throw new IllegalArgumentException("No user nor group specified. Use --uusername / --uid to specify user or --gname / --gid / --gpath to specify group");
+ }
+
+ return CommandResult.SUCCESS;
+
+ } catch (IllegalArgumentException e) {
+ throw new IllegalArgumentException(e.getMessage() + suggestHelp(), e);
+ } finally {
+ commandInvocation.stop();
+ }
+ }
+
+ private Set<ObjectNode> getRoleRepresentations(List<String> roleNames, List<String> roleIds, LocalSearch roleSearch) {
+ Set<ObjectNode> rolesToAdd = new HashSet<>();
+
+ // now we process roles
+ for (String name : roleNames) {
+ ObjectNode r = roleSearch.exactMatchOne(name, "name");
+ if (r == null) {
+ throw new RuntimeException("Role not found for name: " + name);
+ }
+ rolesToAdd.add(r);
+ }
+ for (String id : roleIds) {
+ ObjectNode r = roleSearch.exactMatchOne(id, "id");
+ if (r == null) {
+ throw new RuntimeException("Role not found for id: " + id);
+ }
+ rolesToAdd.add(r);
+ }
+ return rolesToAdd;
+ }
+
+ private void optionRequiresValueCheck(Iterator<String> it, String option) {
+ if (!it.hasNext()) {
+ throw new IllegalArgumentException("Option " + option + " requires a value");
+ }
+ }
+
+ private boolean isClientSpecified() {
+ return cid != null || cclientid != null;
+ }
+
+ private boolean isGroupSpecified() {
+ return gid != null || gname != null || gpath != null;
+ }
+
+ private boolean isUserSpecified() {
+ return uid != null || uusername != null;
+ }
+
+
+ @Override
+ protected boolean nothingToDo() {
+ return noOptions() && uusername == null && uid == null && cclientid == null && (args == null || args.size() == 0);
+ }
+
+ protected String suggestHelp() {
+ return EOL + "Try '" + CMD + " help add-roles' for more information";
+ }
+
+ protected String help() {
+ return usage();
+ }
+
+ public static String usage() {
+ StringWriter sb = new StringWriter();
+ PrintWriter out = new PrintWriter(sb);
+ out.println("Usage: " + CMD + " add-roles (--uusername USERNAME | --uid ID) [--cclientid CLIENT_ID | --cid ID] (--rolename NAME | --roleid ID)+ [ARGUMENTS]");
+ out.println("Usage: " + CMD + " add-roles (--gname NAME | --gpath PATH | --gid ID) [--cclientid CLIENT_ID | --cid ID] (--rolename NAME | --roleid ID)+ [ARGUMENTS]");
+ out.println();
+ out.println("Command to add realm or client roles to a user or group.");
+ out.println();
+ out.println("Use `" + CMD + " config credentials` to establish an authenticated session, or use CREDENTIALS OPTIONS");
+ out.println("to perform one time authentication.");
+ out.println();
+ out.println("If client is specified using --cclientid or --cid then roles to add are client roles, otherwise they are realm roles.");
+ out.println("Either a user, or a group needs to be specified. If user is specified using --uusername or --uid then roles are added");
+ out.println("to a specific user. If group is specified using --gname, --gpath or --gid then roles are added to a specific group.");
+ out.println("One or more roles have to be specified using --rolename or --roleid so that they are added to a group or a user.");
+ out.println();
+ out.println("Arguments:");
+ out.println();
+ out.println(" Global options:");
+ out.println(" -x Print full stack trace when exiting with error");
+ out.println(" --config Path to the config file (" + DEFAULT_CONFIG_FILE_STRING + " by default)");
+ out.println(" --truststore PATH Path to a truststore containing trusted certificates");
+ out.println(" --trustpass PASSWORD Truststore password (prompted for if not specified and --truststore is used)");
+ out.println(" CREDENTIALS OPTIONS Same set of options as accepted by '" + CMD + " config credentials' in order to establish");
+ out.println(" an authenticated sessions. This allows on-the-fly transient authentication that does");
+ out.println(" not touch a config file.");
+ out.println();
+ out.println(" Command specific options:");
+ out.println(" --uusername User's 'username'. If more than one user exists with the same username");
+ out.println(" you'll have to use --uid to specify the target user");
+ out.println(" --uid User's 'id' attribute");
+ out.println(" --gname Group's 'name'. If more than one group exists with the same name you'll have");
+ out.println(" to use --gid, or --gpath to specify the target group");
+ out.println(" --gpath Group's 'path' attribute");
+ out.println(" --gid Group's 'id' attribute");
+ out.println(" --cclientid Client's 'clientId' attribute");
+ out.println(" --cid Client's 'id' attribute");
+ out.println(" --rolename Role's 'name' attribute");
+ out.println(" --roleid Role's 'id' attribute");
+ out.println(" -a, --admin-root URL URL of Admin REST endpoint root if not default - e.g. http://localhost:8080/auth/admin");
+ out.println(" -r, --target-realm REALM Target realm to issue requests against if not the one authenticated against");
+ out.println();
+ out.println("Examples:");
+ out.println();
+ out.println("Add 'offline_access' realm role to a user:");
+ out.println(" " + PROMPT + " " + CMD + " add-roles -r demorealm --uusername testuser --rolename offline_access");
+ out.println();
+ out.println("Add 'realm-management' client roles 'view-users', 'view-clients' and 'view-realm' to a user:");
+ out.println(" " + PROMPT + " " + CMD + " add-roles -r demorealm --uusername testuser --cclientid realm-management --rolename view-users --rolename view-clients --rolename view-realm");
+ out.println();
+ out.println("Add 'uma_authorization' realm role to a group:");
+ out.println(" " + PROMPT + " " + CMD + " add-roles -r demorealm --gname PowerUsers --rolename uma_authorization");
+ out.println();
+ out.println("Add 'realm-management' client roles 'realm-admin' to a group:");
+ out.println(" " + PROMPT + " " + CMD + " add-roles -r demorealm --gname PowerUsers --cclientid realm-management --rolename realm-admin");
+ out.println();
+ out.println();
+ out.println("Use '" + CMD + " help' for general information and a list of commands");
+ return sb.toString();
+ }
+}
diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/ConfigCmd.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/ConfigCmd.java
new file mode 100644
index 0000000..f297561
--- /dev/null
+++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/ConfigCmd.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.client.admin.cli.commands;
+
+import org.jboss.aesh.cl.GroupCommandDefinition;
+import org.jboss.aesh.console.command.CommandException;
+import org.jboss.aesh.console.command.CommandResult;
+import org.jboss.aesh.console.command.invocation.CommandInvocation;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+
+import static org.keycloak.client.admin.cli.util.OsUtil.CMD;
+import static org.keycloak.client.admin.cli.util.OsUtil.EOL;
+
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+
+@GroupCommandDefinition(name = "config", description = "COMMAND [ARGUMENTS]", groupCommands = {ConfigCredentialsCmd.class} )
+public class ConfigCmd extends AbstractAuthOptionsCmd {
+
+ public CommandResult execute(CommandInvocation commandInvocation) throws CommandException, InterruptedException {
+ try {
+ if (args != null && args.size() > 0) {
+ String cmd = args.get(0);
+ switch (cmd) {
+ case "credentials": {
+ args.remove(0);
+ ConfigCredentialsCmd command = new ConfigCredentialsCmd();
+ command.initFromParent(this);
+ return command.execute(commandInvocation);
+ }
+ case "truststore": {
+ args.remove(0);
+ ConfigTruststoreCmd command = new ConfigTruststoreCmd();
+ command.initFromParent(this);
+ return command.execute(commandInvocation);
+ }
+ default: {
+ if (printHelp()) {
+ return help ? CommandResult.SUCCESS : CommandResult.FAILURE;
+ }
+ throw new IllegalArgumentException("Unknown sub-command: " + cmd + suggestHelp());
+ }
+ }
+ }
+
+ if (printHelp()) {
+ return help ? CommandResult.SUCCESS : CommandResult.FAILURE;
+ }
+
+ throw new IllegalArgumentException("Sub-command required by '" + CMD + " config' - one of: 'credentials', 'truststore'");
+
+ } finally {
+ commandInvocation.stop();
+ }
+ }
+
+ protected String suggestHelp() {
+ return EOL + "Try '" + CMD + " help config' for more information";
+ }
+
+ protected String help() {
+ return usage();
+ }
+
+ public static String usage() {
+ StringWriter sb = new StringWriter();
+ PrintWriter out = new PrintWriter(sb);
+ out.println("Usage: " + CMD + " config SUB_COMMAND [ARGUMENTS]");
+ out.println();
+ out.println("Where SUB_COMMAND is one of: 'credentials', 'truststore'");
+ out.println();
+ out.println();
+ out.println("Use '" + CMD + " help config SUB_COMMAND' for more info.");
+ out.println("Use '" + CMD + " help' for general information and a list of commands.");
+ return sb.toString();
+ }
+}
diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/ConfigCredentialsCmd.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/ConfigCredentialsCmd.java
new file mode 100644
index 0000000..c84c52e
--- /dev/null
+++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/ConfigCredentialsCmd.java
@@ -0,0 +1,275 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.client.admin.cli.commands;
+
+import org.jboss.aesh.cl.CommandDefinition;
+import org.jboss.aesh.console.command.CommandException;
+import org.jboss.aesh.console.command.CommandResult;
+import org.jboss.aesh.console.command.invocation.CommandInvocation;
+import org.keycloak.client.admin.cli.config.ConfigData;
+import org.keycloak.client.admin.cli.config.RealmConfigData;
+import org.keycloak.client.admin.cli.util.AuthUtil;
+import org.keycloak.representations.AccessTokenResponse;
+
+import java.io.File;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.net.URL;
+
+import static org.keycloak.client.admin.cli.util.AuthUtil.getAuthTokens;
+import static org.keycloak.client.admin.cli.util.AuthUtil.getAuthTokensByJWT;
+import static org.keycloak.client.admin.cli.util.AuthUtil.getAuthTokensBySecret;
+import static org.keycloak.client.admin.cli.util.ConfigUtil.DEFAULT_CONFIG_FILE_STRING;
+import static org.keycloak.client.admin.cli.util.ConfigUtil.getHandler;
+import static org.keycloak.client.admin.cli.util.ConfigUtil.loadConfig;
+import static org.keycloak.client.admin.cli.util.ConfigUtil.saveTokens;
+import static org.keycloak.client.admin.cli.util.IoUtil.printErr;
+import static org.keycloak.client.admin.cli.util.IoUtil.readSecret;
+import static org.keycloak.client.admin.cli.util.OsUtil.CMD;
+import static org.keycloak.client.admin.cli.util.OsUtil.EOL;
+import static org.keycloak.client.admin.cli.util.OsUtil.OS_ARCH;
+import static org.keycloak.client.admin.cli.util.OsUtil.PROMPT;
+
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+@CommandDefinition(name = "credentials", description = "--server SERVER_URL --realm REALM [ARGUMENTS]")
+public class ConfigCredentialsCmd extends AbstractAuthOptionsCmd {
+
+ private int sigLifetime = 600;
+
+
+ public void init(ConfigData configData) {
+ if (server == null) {
+ server = configData.getServerUrl();
+ }
+ if (realm == null) {
+ realm = configData.getRealm();
+ }
+ if (trustStore == null) {
+ trustStore = configData.getTruststore();
+ }
+
+ RealmConfigData rdata = configData.getRealmConfigData(server, realm);
+ if (rdata == null) {
+ return;
+ }
+
+ if (clientId == null) {
+ clientId = rdata.getClientId();
+ }
+ }
+
+
+ @Override
+ public CommandResult execute(CommandInvocation commandInvocation) throws CommandException, InterruptedException {
+ try {
+ if (printHelp()) {
+ return help ? CommandResult.SUCCESS : CommandResult.FAILURE;
+ }
+
+ processGlobalOptions();
+
+ return process(commandInvocation);
+ } catch (IllegalArgumentException e) {
+ throw new IllegalArgumentException(e.getMessage() + suggestHelp(), e);
+ } finally {
+ commandInvocation.stop();
+ }
+ }
+
+ @Override
+ protected boolean nothingToDo() {
+ return noOptions();
+ }
+
+ public CommandResult process(CommandInvocation commandInvocation) throws CommandException, InterruptedException {
+
+ // check server
+ if (server == null) {
+ throw new IllegalArgumentException("Required option not specified: --server");
+ }
+
+ try {
+ new URL(server);
+ } catch (Exception e) {
+ throw new RuntimeException("Invalid server endpoint url: " + server, e);
+ }
+
+ if (realm == null)
+ throw new IllegalArgumentException("Required option not specified: --realm");
+
+ String signedRequestToken = null;
+ boolean clientSet = clientId != null;
+
+ applyDefaultOptionValues();
+
+ if (user != null) {
+ printErr("Logging into " + server + " as user " + user + " of realm " + realm);
+
+ // if user was set there needs to be a password so we can authenticate
+ if (password == null) {
+ password = readSecret("Enter password: ", commandInvocation);
+ }
+ // if secret was set to be read from stdin, then ask for it
+ if ("-".equals(secret) && keystore == null) {
+ secret = readSecret("Enter client secret: ", commandInvocation);
+ }
+ } else if (keystore != null || secret != null || clientSet) {
+ printErr("Logging into " + server + " as " + "service-account-" + clientId + " of realm " + realm);
+ if (keystore == null) {
+ if (secret == null) {
+ secret = readSecret("Enter client secret: ", commandInvocation);
+ }
+ }
+ }
+
+ if (keystore != null) {
+ if (secret != null) {
+ throw new IllegalArgumentException("Can't use both --keystore and --secret");
+ }
+
+ if (!new File(keystore).isFile()) {
+ throw new RuntimeException("No such keystore file: " + keystore);
+ }
+
+ if (storePass == null) {
+ storePass = readSecret("Enter keystore password: ", commandInvocation);
+ keyPass = readSecret("Enter key password: ", commandInvocation);
+ }
+
+ if (keyPass == null) {
+ keyPass = storePass;
+ }
+
+ if (alias == null) {
+ alias = clientId;
+ }
+
+ String realmInfoUrl = server + "/realms/" + realm;
+
+ signedRequestToken = AuthUtil.getSignedRequestToken(keystore, storePass, keyPass,
+ alias, sigLifetime, clientId, realmInfoUrl);
+ }
+
+ // if only server and realm are set, just save config and be done
+ if (user == null && secret == null && keystore == null) {
+ getHandler().saveMergeConfig(config -> {
+ config.setServerUrl(server);
+ config.setRealm(realm);
+ });
+ return CommandResult.SUCCESS;
+ }
+
+ setupTruststore(copyWithServerInfo(loadConfig()), commandInvocation);
+
+ // now use the token endpoint to retrieve access token, and refresh token
+ AccessTokenResponse tokens = signedRequestToken != null ?
+ getAuthTokensByJWT(server, realm, user, password, clientId, signedRequestToken) :
+ secret != null ?
+ getAuthTokensBySecret(server, realm, user, password, clientId, secret) :
+ getAuthTokens(server, realm, user, password, clientId);
+
+ Long sigExpiresAt = signedRequestToken == null ? null : System.currentTimeMillis() + sigLifetime * 1000;
+
+ // save tokens to config file
+ saveTokens(tokens, server, realm, clientId, signedRequestToken, sigExpiresAt, secret);
+
+ return CommandResult.SUCCESS;
+ }
+
+ protected String suggestHelp() {
+ return EOL + "Try '" + CMD + " help config credentials' for more information";
+ }
+
+ protected String help() {
+ return usage();
+ }
+
+ public static String usage() {
+ StringWriter sb = new StringWriter();
+ PrintWriter out = new PrintWriter(sb);
+ out.println("Usage: " + CMD + " config credentials --server SERVER_URL --realm REALM --user USER [--password PASSWORD] [ARGUMENTS]");
+ out.println(" " + CMD + " config credentials --server SERVER_URL --realm REALM --client CLIENT_ID [--secret SECRET] [ARGUMENTS]");
+ out.println(" " + CMD + " config credentials --server SERVER_URL --realm REALM --client CLIENT_ID [--keystore KEYSTORE] [ARGUMENTS]");
+ out.println();
+ out.println("Command to establish an authenticated client session with the server. There are many authentication");
+ out.println("options available, and it depends on server side client authentication configuration how client can or should authenticate.");
+ out.println("The information always required includes --server, and --realm. Then, --user and / or --client need to be used to authenticate.");
+ out.println("If --client is not provided it defaults to 'admin-cli'. The authentication options / requirements depend on how this client is configured.");
+ out.println();
+ out.println("If confidential client authentication is also configured, you may have to specify a client id, and client credentials in addition to");
+ out.println("user credentials. Client credentials are either a client secret, or a keystore information to use Signed JWT mechanism.");
+ out.println("If only client credentials are provided, and no user credentials, then the service account is used for login.");
+ out.println();
+ out.println("Arguments:");
+ out.println();
+ out.println(" Global options:");
+ out.println(" -x Print full stack trace when exiting with error");
+ out.println(" --config Path to a config file (" + DEFAULT_CONFIG_FILE_STRING + " by default)");
+ out.println(" --truststore PATH Path to a truststore containing trusted certificates");
+ out.println(" --trustpass PASSWORD Truststore password (prompted for if not specified and --truststore is used)");
+ out.println();
+ out.println(" Command specific options:");
+ out.println(" --server SERVER_URL Server endpoint url (e.g. 'http://localhost:8080/auth')");
+ out.println(" --realm REALM Realm name to use");
+ out.println(" --user USER Username to login with");
+ out.println(" --password PASSWORD Password to login with (prompted for if not specified and --user is used)");
+ out.println(" --client CLIENT_ID ClientId used by this client tool ('admin-cli' by default)");
+ out.println(" --secret SECRET Secret to authenticate the client (prompted for if --client is specified, and no --keystore is specified)");
+ out.println(" --keystore PATH Path to a keystore containing private key");
+ out.println(" --storepass PASSWORD Keystore password (prompted for if not specified and --keystore is used)");
+ out.println(" --keypass PASSWORD Key password (prompted for if not specified and --keystore is used without --storepass,");
+ out.println(" otherwise defaults to keystore password)");
+ out.println(" --alias ALIAS Alias of the key inside a keystore (defaults to the value of ClientId)");
+ out.println();
+ out.println();
+ out.println("Examples:");
+ out.println();
+ out.println("Login as 'admin' user of 'master' realm to a local Keycloak server running on default port.");
+ out.println("You will be prompted for a password:");
+ out.println(" " + PROMPT + " " + CMD + " config credentials --server http://localhost:8080/auth --realm master --user admin");
+ out.println();
+ out.println("Login to Keycloak server at non-default endpoint passing the password via standard input:");
+ if (OS_ARCH.isWindows()) {
+ out.println(" " + PROMPT + " echo mypassword | " + CMD + " config credentials --server http://localhost:9080/auth --realm master --user admin");
+ } else {
+ out.println(" " + PROMPT + " " + CMD + " config credentials --server http://localhost:9080/auth --realm master --user admin << EOF");
+ out.println(" mypassword");
+ out.println(" EOF");
+ }
+ out.println();
+ out.println("Login specifying a password through command line:");
+ out.println(" " + PROMPT + " " + CMD + " config credentials --server http://localhost:9080/auth --realm master --user admin --password " + OS_ARCH.envVar("PASSWORD"));
+ out.println();
+ out.println("Login using a client service account of a custom client. You will be prompted for a client secret:");
+ out.println(" " + PROMPT + " " + CMD + " config credentials --server http://localhost:9080/auth --realm master --client reg-cli");
+ out.println();
+ out.println("Login using a client service account of a custom client, authenticating with signed JWT.");
+ out.println("You will be prompted for a keystore password, and a key password:");
+ out.println(" " + PROMPT + " " + CMD + " config credentials --server http://localhost:9080/auth --realm master --client reg-cli --keystore " + OS_ARCH.path("~/.keycloak/keystore.jks"));
+ out.println();
+ out.println("Login as 'user' while also authenticating a custom client with signed JWT.");
+ out.println("You will be prompted for a user password, a keystore password, and a key password:");
+ out.println(" " + PROMPT + " " + CMD + " config credentials --server http://localhost:9080/auth --realm master --user user --client reg-cli --keystore " + OS_ARCH.path("~/.keycloak/keystore.jks"));
+ out.println();
+ out.println();
+ out.println("Use '" + CMD + " help' for general information and a list of commands");
+ return sb.toString();
+ }
+}
diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/ConfigTruststoreCmd.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/ConfigTruststoreCmd.java
new file mode 100644
index 0000000..38f7b9f
--- /dev/null
+++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/ConfigTruststoreCmd.java
@@ -0,0 +1,200 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.client.admin.cli.commands;
+
+import org.jboss.aesh.cl.CommandDefinition;
+import org.jboss.aesh.console.command.CommandException;
+import org.jboss.aesh.console.command.CommandResult;
+import org.jboss.aesh.console.command.invocation.CommandInvocation;
+
+import java.io.File;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+import static org.keycloak.client.admin.cli.util.ConfigUtil.DEFAULT_CONFIG_FILE_STRING;
+import static org.keycloak.client.admin.cli.util.ConfigUtil.saveMergeConfig;
+import static org.keycloak.client.admin.cli.util.IoUtil.readSecret;
+import static org.keycloak.client.admin.cli.util.OsUtil.CMD;
+import static org.keycloak.client.admin.cli.util.OsUtil.EOL;
+import static org.keycloak.client.admin.cli.util.OsUtil.OS_ARCH;
+import static org.keycloak.client.admin.cli.util.OsUtil.PROMPT;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+@CommandDefinition(name = "truststore", description = "PATH [ARGUMENTS]")
+public class ConfigTruststoreCmd extends AbstractAuthOptionsCmd {
+
+ private ConfigCmd parent;
+
+ private boolean delete;
+
+
+ protected void initFromParent(ConfigCmd parent) {
+ this.parent = parent;
+ super.initFromParent(parent);
+ }
+
+ @Override
+ public CommandResult execute(CommandInvocation commandInvocation) throws CommandException, InterruptedException {
+ try {
+ if (printHelp()) {
+ return help ? CommandResult.SUCCESS : CommandResult.FAILURE;
+ }
+
+ return process(commandInvocation);
+
+ } catch (IllegalArgumentException e) {
+ throw new IllegalArgumentException(e.getMessage() + suggestHelp(), e);
+ } finally {
+ commandInvocation.stop();
+ }
+ }
+
+ @Override
+ protected boolean nothingToDo() {
+ return noOptions();
+ }
+
+ public CommandResult process(CommandInvocation commandInvocation) throws CommandException, InterruptedException {
+
+ List<String> args = new ArrayList<>();
+
+ Iterator<String> it = parent.args.iterator();
+
+ while (it.hasNext()) {
+ String arg = it.next();
+ switch (arg) {
+ case "-d":
+ case "--delete": {
+ delete = true;
+ break;
+ }
+ default: {
+ args.add(arg);
+ }
+ }
+ }
+
+ if (args.size() > 1) {
+ throw new IllegalArgumentException("Invalid option: " + args.get(1));
+ }
+
+ String truststore = null;
+ if (args.size() > 0) {
+ truststore = args.get(0);
+ }
+
+ checkUnsupportedOptions("--server", server,
+ "--realm", realm,
+ "--client", clientId,
+ "--user", user,
+ "--password", password,
+ "--secret", secret,
+ "--truststore", trustStore,
+ "--keystore", keystore,
+ "--keypass", keyPass,
+ "--alias", alias);
+
+ // now update the config
+ processGlobalOptions();
+
+ String store;
+ String pass;
+
+ if (!delete) {
+
+ if (truststore == null) {
+ throw new IllegalArgumentException("No truststore specified");
+ }
+
+ if (!new File(truststore).isFile()) {
+ throw new RuntimeException("Truststore file not found: " + truststore);
+ }
+
+ if ("-".equals(trustPass)) {
+ trustPass = readSecret("Enter truststore password: ", commandInvocation);
+ }
+
+ store = truststore;
+ pass = trustPass;
+
+ } else {
+ if (truststore != null) {
+ throw new IllegalArgumentException("Option --delete is mutually exclusive with specifying a TRUSTSTORE");
+ }
+ if (trustPass != null) {
+ throw new IllegalArgumentException("Options --trustpass and --delete are mutually exclusive");
+ }
+ store = null;
+ pass = null;
+ }
+
+ saveMergeConfig(config -> {
+ config.setTruststore(store);
+ config.setTrustpass(pass);
+ });
+
+ return CommandResult.SUCCESS;
+ }
+
+ protected String suggestHelp() {
+ return EOL + "Try '" + CMD + " help config truststore' for more information";
+ }
+
+ protected String help() {
+ return usage();
+ }
+
+ public static String usage() {
+ StringWriter sb = new StringWriter();
+ PrintWriter out = new PrintWriter(sb);
+ out.println("Usage: " + CMD + " config truststore [TRUSTSTORE | --delete] [--trustpass PASSWORD] [ARGUMENTS]");
+ out.println();
+ out.println("Command to configure a global truststore to use when using https to connect to Keycloak server.");
+ out.println();
+ out.println("Arguments:");
+ out.println();
+ out.println(" Global options:");
+ out.println(" -x Print full stack trace when exiting with error");
+ out.println(" --config Path to the config file (" + DEFAULT_CONFIG_FILE_STRING + " by default)");
+ out.println();
+ out.println(" Command specific options:");
+ out.println(" TRUSTSTORE Path to truststore file");
+ out.println(" --trustpass PASSWORD Truststore password to unlock truststore (prompted for if set to '-')");
+ out.println(" -d, --delete Remove truststore configuration");
+ out.println();
+ out.println();
+ out.println("Examples:");
+ out.println();
+ out.println("Specify a truststore - you will be prompted for truststore password every time it is used:");
+ out.println(" " + PROMPT + " " + CMD + " config truststore " + OS_ARCH.path("~/.keycloak/truststore.jks"));
+ out.println();
+ out.println("Specify a truststore, and password - truststore will automatically be used without prompting for password:");
+ out.println(" " + PROMPT + " " + CMD + " config truststore --storepass " + OS_ARCH.envVar("PASSWORD") + " " + OS_ARCH.path("~/.keycloak/truststore.jks"));
+ out.println();
+ out.println("Remove truststore configuration:");
+ out.println(" " + PROMPT + " " + CMD + " config truststore --delete");
+ out.println();
+ out.println();
+ out.println("Use '" + CMD + " help' for general information and a list of commands");
+ return sb.toString();
+ }
+}
diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/CreateCmd.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/CreateCmd.java
new file mode 100644
index 0000000..63591f1
--- /dev/null
+++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/CreateCmd.java
@@ -0,0 +1,167 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.client.admin.cli.commands;
+
+import org.jboss.aesh.cl.CommandDefinition;
+import org.jboss.aesh.cl.Option;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+
+import static org.keycloak.client.admin.cli.util.ConfigUtil.DEFAULT_CONFIG_FILE_STRING;
+import static org.keycloak.client.admin.cli.util.OsUtil.CMD;
+import static org.keycloak.client.admin.cli.util.OsUtil.EOL;
+import static org.keycloak.client.admin.cli.util.OsUtil.OS_ARCH;
+import static org.keycloak.client.admin.cli.util.OsUtil.PROMPT;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+@CommandDefinition(name = "create", description = "Command to create new resources")
+public class CreateCmd extends AbstractRequestCmd {
+
+ @Option(shortName = 'f', name = "file", description = "Read object from file or standard input if FILENAME is set to '-'", hasValue = true)
+ String file;
+
+ @Option(shortName = 'F', name = "fields", description = "A pattern specifying which attributes of JSON response body to actually display as result - causes mismatch with Content-Length header", hasValue = true)
+ String fields;
+
+ @Option(shortName = 'H', name = "print-headers", description = "Print response headers", hasValue = false)
+ boolean printHeaders;
+
+ @Option(shortName = 'i', name = "id", description = "After creation only print id of created resource to standard output", hasValue = false)
+ boolean returnId = false;
+
+ @Option(shortName = 'o', name = "output", description = "After creation output the new resource to standard output", hasValue = false)
+ boolean outputResult = false;
+
+ @Option(shortName = 'c', name = "compressed", description = "Don't pretty print the output", hasValue = false)
+ boolean compressed = false;
+
+ //@OptionGroup(shortName = 's', name = "set", description = "Set attribute to the specified value")
+ //Map<String, String> attributes = new LinkedHashMap<>();
+
+ @Override
+ void initOptions() {
+ // set options on parent
+ super.file = file;
+ super.fields = fields;
+ super.printHeaders = printHeaders;
+ super.returnId = returnId;
+ super.outputResult = outputResult;
+ super.compressed = compressed;
+ super.httpVerb = "post";
+ }
+
+ @Override
+ protected boolean nothingToDo() {
+ return noOptions() && file == null && (args == null || args.size() == 0);
+ }
+
+ protected String suggestHelp() {
+ return EOL + "Try '" + CMD + " help create' for more information";
+ }
+
+ protected String help() {
+ return usage();
+ }
+
+ public static String usage() {
+ StringWriter sb = new StringWriter();
+ PrintWriter out = new PrintWriter(sb);
+ out.println("Usage: " + CMD + " create ENDPOINT_URI [ARGUMENTS]");
+ out.println();
+ out.println("Command to create new resources on the server.");
+ out.println();
+ out.println("Use `" + CMD + " config credentials` to establish an authenticated sessions, or use CREDENTIALS OPTIONS");
+ out.println("to perform one time authentication.");
+ out.println();
+ out.println("Arguments:");
+ out.println();
+ out.println(" Global options:");
+ out.println(" -x Print full stack trace when exiting with error");
+ out.println(" --config Path to the config file (" + DEFAULT_CONFIG_FILE_STRING + " by default)");
+ out.println(" --truststore PATH Path to a truststore containing trusted certificates");
+ out.println(" --trustpass PASSWORD Truststore password (prompted for if not specified and --truststore is used)");
+ out.println(" CREDENTIALS OPTIONS Same set of options as accepted by '" + CMD + " config credentials' in order to establish");
+ out.println(" an authenticated sessions. This allows on-the-fly transient authentication that does");
+ out.println(" not touch a config file.");
+ out.println();
+ out.println(" Command specific options:");
+ out.println(" ENDPOINT_URI URI used to compose a target resource url. Commonly used values are:");
+ out.println(" realms, users, roles, groups, clients, keys, serverinfo, components ...");
+ out.println(" If it starts with 'http://' then it will be used as target resource url");
+ out.println(" -r, --target-realm REALM Target realm to issue requests against if not the one authenticated against");
+ out.println(" -s, --set NAME=VALUE Set a specific attribute NAME to a specified value VALUE");
+ out.println(" -d, --delete NAME Remove a specific attribute NAME from JSON request body");
+ out.println(" -f, --file FILENAME Read object from file or standard input if FILENAME is set to '-'");
+ out.println(" -q, --query NAME=VALUE Add to request URI a NAME query parameter with value VALUE");
+ out.println(" -h, --header NAME=VALUE Set request header NAME to VALUE");
+ out.println();
+ out.println(" -H, --print-headers Print response headers");
+ out.println(" -o, --output After creation output the new resource to standard output");
+ out.println(" -i, --id After creation only print id of the new resource to standard output");
+ out.println(" -F, --fields FILTER A filter pattern to specify which fields of a JSON response to output");
+ out.println(" -c, --compressed Don't pretty print the output");
+ out.println(" -a, --admin-root URL URL of Admin REST endpoint root if not default - e.g. http://localhost:8080/auth/admin");
+ out.println();
+ out.println();
+ out.println("Nested attributes are supported by using '.' to separate components of a KEY. Optionaly, the KEY components ");
+ out.println("can be quoted with double quotes - e.g. my_client.attributes.\"external.user.id\". If VALUE starts with [ and ");
+ out.println("ends with ] the attribute will be set as a JSON array. If VALUE starts with { and ends with } the attribute ");
+ out.println("will be set as a JSON object. If KEY ends with an array index - e.g. clients[3]=VALUE - then the specified item");
+ out.println("of the array is updated. If KEY+=VALUE syntax is used, then KEY is assumed to be an array, and another item is");
+ out.println("added to it.");
+ out.println();
+ out.println("Attributes can also be deleted. If KEY ends with an array index, then the targeted item of an array is removed");
+ out.println("and the following items are shifted.");
+ out.println();
+ out.println();
+ out.println("Examples:");
+ out.println();
+ out.println("Create a new realm:");
+ out.println(" " + PROMPT + " " + CMD + " create realms -s realm=demorealm -s enabled=true");
+ out.println();
+ out.println("Create a new realm role in realm 'demorealm' returning newly created role:");
+ out.println(" " + PROMPT + " " + CMD + " create roles -r demorealm -s name=manage-all -o");
+ out.println();
+ out.println("Create a new user in realm 'demorealm' returning only 'id', and 'username' attributes:");
+ out.println(" " + PROMPT + " " + CMD + " create users -r demorealm -s username=testuser -s enabled=true -o --fields id,username");
+ out.println();
+ out.println("Create a new client using configuration read from standard input:");
+ if (OS_ARCH.isWindows()) {
+ out.println(" " + PROMPT + " echo { \"clientId\": \"my_client\" } | " + CMD + " create clients -r demorealm -f -");
+ } else {
+ out.println(" " + PROMPT + " " + CMD + " create clients -r demorealm -f - << EOF");
+ out.println(" {");
+ out.println(" \"clientId\": \"my_client\"");
+ out.println(" }");
+ out.println(" EOF");
+ }
+ out.println();
+ out.println("Create a client using file as a template, and override some attributes - return an 'id' of new client:");
+ out.println(" " + PROMPT + " " + CMD + " create clients -r demorealm -f my_client.json -s clientId=my_client2 -s 'redirectUris=[\"http://localhost:8980/myapp/*\"]' -i");
+ out.println();
+ out.println("Create a new client role for client my_client in realm 'demorealm' (replace ID with output of previous example command):");
+ out.println(" " + PROMPT + " " + CMD + " create clients/ID/roles -r demorealm -s name=client_role");
+ out.println();
+ out.println();
+ out.println("Use '" + CMD + " help' for general information and a list of commands");
+ return sb.toString();
+ }
+
+}
diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/DeleteCmd.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/DeleteCmd.java
new file mode 100644
index 0000000..7ef31e1
--- /dev/null
+++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/DeleteCmd.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.client.admin.cli.commands;
+
+import org.jboss.aesh.cl.CommandDefinition;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+
+import static org.keycloak.client.admin.cli.util.ConfigUtil.DEFAULT_CONFIG_FILE_STRING;
+import static org.keycloak.client.admin.cli.util.OsUtil.CMD;
+import static org.keycloak.client.admin.cli.util.OsUtil.EOL;
+import static org.keycloak.client.admin.cli.util.OsUtil.PROMPT;
+
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+@CommandDefinition(name = "delete", description = "CLIENT [GLOBAL_OPTIONS]")
+public class DeleteCmd extends CreateCmd {
+
+ void initOptions() {
+ super.initOptions();
+ httpVerb = "delete";
+ }
+
+ @Override
+ protected boolean nothingToDo() {
+ return noOptions() && (args == null || args.size() == 0);
+ }
+
+ protected String suggestHelp() {
+ return EOL + "Try '" + CMD + " help delete' for more information";
+ }
+
+ protected String help() {
+ return usage();
+ }
+
+ public static String usage() {
+ StringWriter sb = new StringWriter();
+ PrintWriter out = new PrintWriter(sb);
+ out.println("Usage: " + CMD + " delete ENDPOINT_URI [ARGUMENTS]");
+ out.println();
+ out.println("Command to delete resources on the server.");
+ out.println();
+ out.println("Use `" + CMD + " config credentials` to establish an authenticated sessions, or use CREDENTIALS OPTIONS");
+ out.println("to perform one time authentication.");
+ out.println();
+ out.println("Arguments:");
+ out.println();
+ out.println(" Global options:");
+ out.println(" -x Print full stack trace when exiting with error");
+ out.println(" --config Path to the config file (" + DEFAULT_CONFIG_FILE_STRING + " by default)");
+ out.println(" --truststore PATH Path to a truststore containing trusted certificates");
+ out.println(" --trustpass PASSWORD Truststore password (prompted for if not specified and --truststore is used)");
+ out.println(" CREDENTIALS OPTIONS Same set of options as accepted by '" + CMD + " config credentials' in order to establish");
+ out.println(" an authenticated sessions. This allows on-the-fly transient authentication that does");
+ out.println(" not touch a config file.");
+ out.println();
+ out.println(" Command specific options:");
+ out.println(" ENDPOINT_URI URI used to compose a target resource url. Commonly used values start with:");
+ out.println(" realms/, users/, roles/, groups/, clients/, keys/, components/ ...");
+ out.println(" If it starts with 'http://' then it will be used as target resource url");
+ out.println(" -r, --target-realm REALM Target realm to issue requests against if not the one authenticated against");
+ out.println(" -s, --set NAME=VALUE Send a body with request - set a specific attribute NAME to a specified value VALUE");
+ out.println(" -d, --delete NAME Remove a specific attribute NAME from JSON request body");
+ out.println(" -f, --file FILENAME Send a body with request - read object from file or standard input if FILENAME is set to '-'");
+ out.println(" -q, --query NAME=VALUE Add to request URI a NAME query parameter with value VALUE");
+ out.println(" -h, --header NAME=VALUE Set request header NAME to VALUE");
+ out.println();
+ out.println(" -H, --print-headers Print response headers");
+ out.println(" -o, --output After delete output any response to standard output");
+ out.println(" -F, --fields FILTER A filter pattern to specify which fields of a JSON response to output");
+ out.println(" -c, --compressed Don't pretty print the output");
+ out.println(" -a, --admin-root URL URL of Admin REST endpoint root if not default - e.g. http://localhost:8080/auth/admin");
+ out.println();
+ out.println("Examples:");
+ out.println();
+ out.println("Delete a realm role:");
+ out.println(" " + PROMPT + " " + CMD + " delete roles/manage-all -r demorealm");
+ out.println();
+ out.println("Delete a user (replace USER_ID with the value of user's 'id' attribute):");
+ out.println(" " + PROMPT + " " + CMD + " delete users/USER_ID -r demorealm");
+ out.println();
+ out.println();
+ out.println("Use '" + CMD + " help' for general information and a list of commands");
+ return sb.toString();
+ }
+}
diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/GetCmd.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/GetCmd.java
new file mode 100644
index 0000000..27005e4
--- /dev/null
+++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/GetCmd.java
@@ -0,0 +1,168 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.client.admin.cli.commands;
+
+import org.jboss.aesh.cl.CommandDefinition;
+import org.jboss.aesh.cl.Option;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+
+import static org.keycloak.client.admin.cli.util.ConfigUtil.DEFAULT_CONFIG_FILE_STRING;
+import static org.keycloak.client.admin.cli.util.OsUtil.CMD;
+import static org.keycloak.client.admin.cli.util.OsUtil.EOL;
+import static org.keycloak.client.admin.cli.util.OsUtil.PROMPT;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+@CommandDefinition(name = "get", description = "[ARGUMENTS]")
+public class GetCmd extends AbstractRequestCmd {
+
+ @Option(name = "noquotes", description = "", hasValue = false)
+ boolean unquoted;
+
+ @Option(shortName = 'F', name = "fields", description = "A pattern specifying which attributes of JSON response body to actually display as result - causes mismatch with Content-Length header")
+ String fields;
+
+ @Option(shortName = 'H', name = "print-headers", description = "Print response headers", hasValue = false)
+ boolean printHeaders;
+
+ @Option(shortName = 'c', name = "compressed", description = "Don't pretty print the output", hasValue = false)
+ boolean compressed;
+
+ @Option(shortName = 'o', name = "offset", description = "Number of results from beginning of resultset to skip")
+ Integer offset;
+
+ @Option(shortName = 'l', name = "limit", description = "Maksimum number of results to return")
+ Integer limit;
+
+ @Option(name = "format", description = "Output format - one of: json, csv", defaultValue = "json")
+ String format;
+
+
+ @Override
+ void initOptions() {
+ // set options on parent
+ super.fields = fields;
+ super.printHeaders = printHeaders;
+ super.returnId = false;
+ super.outputResult = true;
+ super.compressed = compressed;
+ super.offset = offset;
+ super.limit = limit;
+ super.format = format;
+ super.unquoted = unquoted;
+ super.httpVerb = "get";
+ }
+
+ @Override
+ protected boolean nothingToDo() {
+ return noOptions() && (args == null || args.size() == 0);
+ }
+
+ protected String suggestHelp() {
+ return EOL + "Try '" + CMD + " help get' for more information";
+ }
+
+ protected String help() {
+ return usage();
+ }
+
+ public static String usage() {
+ StringWriter sb = new StringWriter();
+ PrintWriter out = new PrintWriter(sb);
+ out.println("Usage: " + CMD + " get ENDPOINT_URI [ARGUMENTS]");
+ out.println();
+ out.println("Command to retrieve existing resources from the server.");
+ out.println();
+ out.println("Use `" + CMD + " config credentials` to establish an authenticated session, or use CREDENTIALS OPTIONS");
+ out.println("to perform one time authentication.");
+ out.println();
+ out.println("Arguments:");
+ out.println();
+ out.println(" Global options:");
+ out.println(" -x Print full stack trace when exiting with error");
+ out.println(" --config Path to the config file (" + DEFAULT_CONFIG_FILE_STRING + " by default)");
+ out.println(" --truststore PATH Path to a truststore containing trusted certificates");
+ out.println(" --trustpass PASSWORD Truststore password (prompted for if not specified and --truststore is used)");
+ out.println(" CREDENTIALS OPTIONS Same set of options as accepted by '" + CMD + " config credentials' in order to establish");
+ out.println(" an authenticated sessions. This allows on-the-fly transient authentication that does");
+ out.println(" not touch a config file.");
+ out.println();
+ out.println(" Command specific options:");
+ out.println(" ENDPOINT_URI URI used to compose a target resource url. Commonly used values are:");
+ out.println(" realms, users, roles, groups, clients, keys, serverinfo, components ...");
+ out.println(" If it starts with 'http://' then it will be used as target resource url");
+ out.println(" -r, --target-realm REALM Target realm to issue requests against if not the one authenticated against");
+ out.println(" -q, --query NAME=VALUE Add to request URI a NAME query parameter with value VALUE");
+ out.println(" -h, --header NAME=VALUE Set request header NAME to VALUE");
+ out.println(" -o, --offset OFFSET Set paging offset - adds a query parameter 'first' which some endpoints recognize");
+ out.println(" -l, --limit LIMIT Set limit to number of items in result - adds a query parameter 'max' ");
+ out.println(" which some endpoints recognize");
+ out.println();
+ out.println(" -H, --print-headers Print response headers");
+ out.println(" -o, --output After delete output any response to standard output");
+ out.println(" -F, --fields FILTER A filter pattern to specify which fields of a JSON response to output");
+ out.println(" -c, --compressed Don't pretty print the output");
+ out.println(" --format FORMAT Set output format to comma-separated-values by using 'csv'. Default format is 'json'");
+ out.println(" --noquotes Don't quote strings when output format is 'csv'");
+ out.println(" -a, --admin-root URL URL of Admin REST endpoint root if not default - e.g. http://localhost:8080/auth/admin");
+ out.println();
+ out.println("Examples:");
+ out.println();
+ out.println("Get all realms, displaying only some of the attributes:");
+ out.println(" " + PROMPT + " " + CMD + " get realms --fields id,realm,enabled");
+ out.println();
+ out.println("Get 'demorealm':");
+ out.println(" " + PROMPT + " " + CMD + " get realms/demorealm");
+ out.println();
+ out.println("Get all configured identity providers in demorealm, displaying only some of the attributes:");
+ out.println(" " + PROMPT + " " + CMD + " get identity-provider/instances -r demorealm --fields alias,providerId,enabled");
+ out.println();
+ out.println("Get all clients in demorealm, displaying only some of the attributes:");
+ out.println(" " + PROMPT + " " + CMD + " get clients -r demorealm --fields 'id,clientId,protocolMappers(id,name,protocol,protocolMapper)'");
+ out.println();
+ out.println("Get specific client in demorealm, and remove 'id', and 'protocolMappers' attributes in order to use");
+ out.println("it as a template (replace ID with client's 'id'):");
+ out.println(" " + PROMPT + " " + CMD + " get clients/ID -r demorealm --fields '*(*),-id,-protocolMappers' > realm-template.json");
+ out.println();
+ out.println("Display first level attributes available on 'serverinfo' resource:");
+ out.println(" " + PROMPT + " " + CMD + " get serverinfo -r demorealm --fields '*'");
+ out.println();
+ out.println("Display system info and memory info:");
+ out.println(" " + PROMPT + " " + CMD + " get serverinfo -r demorealm --fields 'systemInfo(*),memoryInfo(*)'");
+ out.println();
+ out.println("Get adapter configuration for the client (replace ID with client's 'id'):");
+ out.println(" " + PROMPT + " " + CMD + " get clients/ID/installation/providers/keycloak-oidc-keycloak-json -r demorealm");
+ out.println();
+ out.println("Get first 100 users at the most:");
+ out.println(" " + PROMPT + " " + CMD + " get users -r demorealm --offset 0 --limit 100");
+ out.println();
+ out.println("Note: 'users' endpoint knows how to handle --offset and --limit. Most other endpoints don't.");
+ out.println();
+ out.println("Get all users whose 'username' matches '*test*' pattern, and 'email' matches '*@google.com*':");
+ out.println(" " + PROMPT + " " + CMD + " get users -r demorealm -q username=test -q email=@google.com");
+ out.println();
+ out.println("Note: it is the 'users' endpoint that interprets query parameters 'username', and 'email' in such a way that");
+ out.println("it results in the described semantics. Another endpoint may provide a different semantics.");
+ out.println();
+ out.println();
+ out.println("Use '" + CMD + " help' for general information and a list of commands");
+ return sb.toString();
+ }
+}
diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/GetRolesCmd.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/GetRolesCmd.java
new file mode 100644
index 0000000..7b0fd5f
--- /dev/null
+++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/GetRolesCmd.java
@@ -0,0 +1,325 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.client.admin.cli.commands;
+
+import org.jboss.aesh.cl.CommandDefinition;
+import org.jboss.aesh.cl.Option;
+import org.jboss.aesh.console.command.CommandException;
+import org.jboss.aesh.console.command.CommandResult;
+import org.jboss.aesh.console.command.invocation.CommandInvocation;
+import org.keycloak.client.admin.cli.config.ConfigData;
+import org.keycloak.client.admin.cli.operations.ClientOperations;
+import org.keycloak.client.admin.cli.operations.GroupOperations;
+import org.keycloak.client.admin.cli.operations.RoleOperations;
+import org.keycloak.client.admin.cli.operations.UserOperations;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.ArrayList;
+
+import static org.keycloak.client.admin.cli.util.AuthUtil.ensureToken;
+import static org.keycloak.client.admin.cli.util.ConfigUtil.DEFAULT_CONFIG_FILE_STRING;
+import static org.keycloak.client.admin.cli.util.ConfigUtil.credentialsAvailable;
+import static org.keycloak.client.admin.cli.util.ConfigUtil.loadConfig;
+import static org.keycloak.client.admin.cli.util.HttpUtil.composeResourceUrl;
+import static org.keycloak.client.admin.cli.util.OsUtil.CMD;
+import static org.keycloak.client.admin.cli.util.OsUtil.PROMPT;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+@CommandDefinition(name = "get-roles", description = "[ARGUMENTS]")
+public class GetRolesCmd extends GetCmd {
+
+ @Option(name = "uusername", description = "Target user's 'username'")
+ String uusername;
+
+ @Option(name = "uid", description = "Target user's 'id'")
+ String uid;
+
+ @Option(name = "cclientid", description = "Target client's 'clientId'")
+ String cclientid;
+
+ @Option(name = "cid", description = "Target client's 'id'")
+ String cid;
+
+ @Option(name = "rolename", description = "Target role's 'name'")
+ String rname;
+
+ @Option(name = "roleid", description = "Target role's 'id'")
+ String rid;
+
+ @Option(name = "gname", description = "Target group's 'name'")
+ String gname;
+
+ @Option(name = "gpath", description = "Target group's 'path'")
+ String gpath;
+
+ @Option(name = "gid", description = "Target group's 'id'")
+ String gid;
+
+ @Option(name = "available", description = "List only available roles", hasValue = false)
+ boolean available;
+
+ @Option(name = "effective", description = "List assigned roles including transitively included roles", hasValue = false)
+ boolean effective;
+
+ @Option(name = "all", description = "List roles for all clients in addition to realm roles", hasValue = false)
+ boolean all;
+
+
+ void initOptions() {
+
+ super.initOptions();
+
+ // hack args so that GetCmd option check doesn't fail
+ // set a placeholder
+ if (args == null) {
+ args = new ArrayList();
+ }
+ if (args.size() == 0) {
+ args.add("uri");
+ } else {
+ args.add(0, "uri");
+ }
+ }
+
+ void processOptions(CommandInvocation commandInvocation) {
+
+ if (uid != null && uusername != null) {
+ throw new IllegalArgumentException("Incompatible options: --uid and --uusername are mutually exclusive");
+ }
+
+ if ((gid != null && gname != null) || (gid != null && gpath != null) || (gname != null && gpath != null)) {
+ throw new IllegalArgumentException("Incompatible options: --gid, --gname and --gpath are mutually exclusive");
+ }
+
+ if (rid != null && rname != null) {
+ throw new IllegalArgumentException("Incompatible options: --roleid and --rolename are mutually exclusive");
+ }
+
+ if (cid != null && cclientid != null) {
+ throw new IllegalArgumentException("Incompatible options: --cid and --cclientid are mutually exclusive");
+ }
+
+ if (isUserSpecified() && isGroupSpecified()) {
+ throw new IllegalArgumentException("Incompatible options: --uusername / --uid can't be used at the same time as --gname / --gid / --gpath");
+ }
+
+ super.processOptions(commandInvocation);
+ }
+
+ public CommandResult process(CommandInvocation commandInvocation) throws CommandException, InterruptedException {
+
+ ConfigData config = loadConfig();
+ config = copyWithServerInfo(config);
+
+ setupTruststore(config, commandInvocation);
+
+ String auth = null;
+
+ config = ensureAuthInfo(config, commandInvocation);
+ config = copyWithServerInfo(config);
+ if (credentialsAvailable(config)) {
+ auth = ensureToken(config);
+ }
+
+ auth = auth != null ? "Bearer " + auth : null;
+
+ final String server = config.getServerUrl();
+ final String realm = getTargetRealm(config);
+ final String adminRoot = adminRestRoot != null ? adminRestRoot : composeAdminRoot(server);
+
+ if (isUserSpecified()) {
+ if (uid == null) {
+ uid = UserOperations.getIdFromUsername(adminRoot, realm, auth, uusername);
+ }
+ if (isClientSpecified()) {
+ // list client roles for a user
+ if (cid == null) {
+ cid = ClientOperations.getIdFromClientId(adminRoot, realm, auth, cclientid);
+ }
+ if (available) {
+ super.url = composeResourceUrl(adminRoot, realm, "users/" + uid + "/role-mappings/clients/" + cid + "/available");
+ } else if (effective) {
+ super.url = composeResourceUrl(adminRoot, realm, "users/" + uid + "/role-mappings/clients/" + cid + "/composite");
+ } else {
+ super.url = composeResourceUrl(adminRoot, realm, "users/" + uid + "/role-mappings/clients/" + cid);
+ }
+ } else {
+ // list realm roles for a user
+ if (available) {
+ super.url = composeResourceUrl(adminRoot, realm, "users/" + uid + "/role-mappings/realm/available");
+ } else if (effective) {
+ super.url = composeResourceUrl(adminRoot, realm, "users/" + uid + "/role-mappings/realm/composite");
+ } else {
+ super.url = composeResourceUrl(adminRoot, realm, "users/" + uid + "/role-mappings/realm");
+ }
+ }
+ } else if (isGroupSpecified()) {
+ if (gname != null) {
+ gid = GroupOperations.getIdFromName(adminRoot, realm, auth, gname);
+ } else if (gpath != null) {
+ gid = GroupOperations.getIdFromPath(adminRoot, realm, auth, gpath);
+ }
+ if (isClientSpecified()) {
+ // list client roles for a group
+ if (cid == null) {
+ cid = ClientOperations.getIdFromClientId(adminRoot, realm, auth, cclientid);
+ }
+ if (available) {
+ super.url = composeResourceUrl(adminRoot, realm, "groups/" + gid + "/role-mappings/clients/" + cid + "/available");
+ } else if (effective) {
+ super.url = composeResourceUrl(adminRoot, realm, "groups/" + gid + "/role-mappings/clients/" + cid + "/composite");
+ } else {
+ super.url = composeResourceUrl(adminRoot, realm, "groups/" + gid + "/role-mappings/clients/" + cid);
+ }
+ } else {
+ // list realm roles for a group
+ if (available) {
+ super.url = composeResourceUrl(adminRoot, realm, "groups/" + gid + "/role-mappings/realm/available");
+ } else if (effective) {
+ super.url = composeResourceUrl(adminRoot, realm, "groups/" + gid + "/role-mappings/realm/composite");
+ } else {
+ super.url = composeResourceUrl(adminRoot, realm, "groups/" + gid + "/role-mappings/realm");
+ }
+ }
+ } else if (isClientSpecified()) {
+ if (cid == null) {
+ cid = ClientOperations.getIdFromClientId(adminRoot, realm, auth, cclientid);
+ }
+
+ if (isRoleSpecified()) {
+ // get specific client role
+ if (rname == null) {
+ rname = RoleOperations.getClientRoleNameFromId(adminRoot, realm, auth, cid, rid);
+ }
+ super.url = composeResourceUrl(adminRoot, realm, "clients/" + cid + "/roles/" + rname);
+ } else {
+ // list defined client roles
+ super.url = composeResourceUrl(adminRoot, realm, "clients/" + cid + "/roles");
+ }
+ } else {
+ if (isRoleSpecified()) {
+ // get specific realm role
+ if (rname == null) {
+ rname = RoleOperations.getClientRoleNameFromId(adminRoot, realm, auth, cid, rid);
+ }
+ super.url = composeResourceUrl(adminRoot, realm, "roles/" + rname);
+ } else {
+ // list defined realm roles
+ super.url = composeResourceUrl(adminRoot, realm, "roles");
+ }
+ }
+
+ return super.process(commandInvocation);
+ }
+
+ private boolean isRoleSpecified() {
+ return rid != null || rname != null;
+ }
+
+ private boolean isClientSpecified() {
+ return cid != null || cclientid != null;
+ }
+
+ private boolean isGroupSpecified() {
+ return gid != null || gname != null || gpath != null;
+ }
+
+ private boolean isUserSpecified() {
+ return uid != null || uusername != null;
+ }
+
+ protected String suggestHelp() {
+ return "";
+ }
+
+ protected boolean nothingToDo() {
+ return false;
+ }
+
+ protected String help() {
+ return usage();
+ }
+
+ public static String usage() {
+ StringWriter sb = new StringWriter();
+ PrintWriter out = new PrintWriter(sb);
+ out.println("Usage: " + CMD + " get-roles [--cclientid CLIENT_ID | --cid ID] [ARGUMENTS]");
+ out.println("Usage: " + CMD + " get-roles (--uusername USERNAME | --uid ID) [--cclientid CLIENT_ID | --cid ID] [--available | --effective] (ARGUMENTS)");
+ out.println("Usage: " + CMD + " get-roles (--gname NAME | --gpath PATH | --gid ID) [--cclientid CLIENT_ID | --cid ID] [--available | --effective] [ARGUMENTS]");
+ out.println();
+ out.println("Command to list realm or client roles on a realm, user or group.");
+ out.println();
+ out.println("Use `" + CMD + " config credentials` to establish an authenticated session, or use CREDENTIALS OPTIONS");
+ out.println("to perform one time authentication.");
+ out.println();
+ out.println("If client is specified using --cclientid or --cid then client roles are listed, otherwise realm roles are listed.");
+ out.println("If user is specified using --uusername or --uid then roles are listed for a specific user.");
+ out.println("If group is specified using --gname, --gpath or --gid then roles are listed for a specific group.");
+ out.println("If neither user nor group is specified then defined roles are listed for a realm or specific client");
+ out.println("If role is specified using --rolename or --roleid then only that specific role is returned.");
+ out.println("If --available is specified, then only roles not yet added to the target user or group are returned.");
+ out.println("If --effective is specified, then roles added to the target user or group are transitively resolved and a full");
+ out.println("set of roles in effect for that user or group is returned.");
+ out.println();
+ out.println("Arguments:");
+ out.println();
+ out.println(" Global options:");
+ out.println(" -x Print full stack trace when exiting with error");
+ out.println(" --config Path to the config file (" + DEFAULT_CONFIG_FILE_STRING + " by default)");
+ out.println(" --truststore PATH Path to a truststore containing trusted certificates");
+ out.println(" --trustpass PASSWORD Truststore password (prompted for if not specified and --truststore is used)");
+ out.println(" CREDENTIALS OPTIONS Same set of options as accepted by '" + CMD + " config credentials' in order to establish");
+ out.println(" an authenticated sessions. This allows on-the-fly transient authentication that does");
+ out.println(" not touch a config file.");
+ out.println();
+ out.println(" Command specific options:");
+ out.println(" --uusername User's 'username'. If more than one user exists with the same username");
+ out.println(" you'll have to use --uid to specify the target user");
+ out.println(" --uid User's 'id' attribute");
+ out.println(" --gname Group's 'name'. If more than one group exists with the same name you'll have");
+ out.println(" to use --gid, or --gpath to specify the target group");
+ out.println(" --gpath Group's 'path' attribute");
+ out.println(" --gid Group's 'id' attribute");
+ out.println(" --cclientid Client's 'clientId' attribute");
+ out.println(" --cid Client's 'id' attribute");
+ out.println(" --rolename Role's 'name' attribute");
+ out.println(" --roleid Role's 'id' attribute");
+ out.println(" -a, --admin-root URL URL of Admin REST endpoint root if not default - e.g. http://localhost:8080/auth/admin");
+ out.println(" -r, --target-realm REALM Target realm to issue requests against if not the one authenticated against");
+ out.println();
+ out.println("Examples:");
+ out.println();
+ out.println("Get all realm roles defined on a realm:");
+ out.println(" " + PROMPT + " " + CMD + " get-roles -r demorealm");
+ out.println();
+ out.println("Get all client roles defined on a specific client, displaying only 'id' and 'name':");
+ out.println(" " + PROMPT + " " + CMD + " get-roles -r demorealm --cclientid realm-management --fields id,name");
+ out.println();
+ out.println("List all realm roles for a specific user:");
+ out.println(" " + PROMPT + " " + CMD + " get-roles -r demorealm --uusername testuser");
+ out.println();
+ out.println("List effective client roles for 'realm-management' client for a specific user:");
+ out.println(" " + PROMPT + " " + CMD + " get-roles -r demorealm --uusername testuser --cclientid realm-management --effective");
+ out.println();
+ out.println();
+ out.println("Use '" + CMD + " help' for general information and a list of commands");
+ return sb.toString();
+ }
+}
diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/HelpCmd.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/HelpCmd.java
new file mode 100644
index 0000000..191f7d9
--- /dev/null
+++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/HelpCmd.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.client.admin.cli.commands;
+
+import org.jboss.aesh.cl.Arguments;
+import org.jboss.aesh.cl.CommandDefinition;
+import org.jboss.aesh.console.command.Command;
+import org.jboss.aesh.console.command.CommandException;
+import org.jboss.aesh.console.command.CommandResult;
+import org.jboss.aesh.console.command.invocation.CommandInvocation;
+
+import java.util.List;
+
+import static org.keycloak.client.admin.cli.util.IoUtil.printOut;
+
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+@CommandDefinition(name = "help", description = "This help")
+public class HelpCmd implements Command {
+
+ @Arguments
+ List<String> args;
+
+ @Override
+ public CommandResult execute(CommandInvocation commandInvocation) throws CommandException, InterruptedException {
+ try {
+ if (args == null || args.size() == 0) {
+ printOut(KcAdmCmd.usage());
+ } else {
+ outer:
+ switch (args.get(0)) {
+ case "config": {
+ if (args.size() > 1) {
+ switch (args.get(1)) {
+ case "credentials": {
+ printOut(ConfigCredentialsCmd.usage());
+ break outer;
+ }
+ case "truststore": {
+ printOut(ConfigTruststoreCmd.usage());
+ break outer;
+ }
+ }
+ }
+ printOut(ConfigCmd.usage());
+ break;
+ }
+ case "create": {
+ printOut(CreateCmd.usage());
+ break;
+ }
+ case "get": {
+ printOut(GetCmd.usage());
+ break;
+ }
+ case "update": {
+ printOut(UpdateCmd.usage());
+ break;
+ }
+ case "delete": {
+ printOut(DeleteCmd.usage());
+ break;
+ }
+ case "get-roles": {
+ printOut(GetRolesCmd.usage());
+ break;
+ }
+ case "add-roles": {
+ printOut(AddRolesCmd.usage());
+ break;
+ }
+ case "remove-roles": {
+ printOut(RemoveRolesCmd.usage());
+ break;
+ }
+ case "set-password": {
+ printOut(SetPasswordCmd.usage());
+ break;
+ }
+ default: {
+ throw new RuntimeException("Unknown command: " + args.get(0));
+ }
+ }
+ }
+
+ return CommandResult.SUCCESS;
+ } finally {
+ commandInvocation.stop();
+ }
+ }
+}
diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/KcAdmCmd.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/KcAdmCmd.java
new file mode 100644
index 0000000..d621834
--- /dev/null
+++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/KcAdmCmd.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.client.admin.cli.commands;
+
+import org.jboss.aesh.cl.GroupCommandDefinition;
+import org.jboss.aesh.console.command.CommandException;
+import org.jboss.aesh.console.command.CommandResult;
+import org.jboss.aesh.console.command.invocation.CommandInvocation;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+
+import static org.keycloak.client.admin.cli.util.ConfigUtil.DEFAULT_CONFIG_FILE_STRING;
+import static org.keycloak.client.admin.cli.util.IoUtil.printErr;
+import static org.keycloak.client.admin.cli.util.IoUtil.printOut;
+import static org.keycloak.client.admin.cli.util.OsUtil.CMD;
+import static org.keycloak.client.admin.cli.util.OsUtil.PROMPT;
+
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+
+@GroupCommandDefinition(name = "kcadm", description = "COMMAND [ARGUMENTS]", groupCommands = {
+ HelpCmd.class, ConfigCmd.class, NewObjectCmd.class, CreateCmd.class, GetCmd.class, UpdateCmd.class, DeleteCmd.class,
+ AddRolesCmd.class, RemoveRolesCmd.class, GetRolesCmd.class, SetPasswordCmd.class} )
+public class KcAdmCmd extends AbstractGlobalOptionsCmd {
+
+ @Override
+ public CommandResult execute(CommandInvocation commandInvocation) throws CommandException, InterruptedException {
+ try {
+ // if --help was requested then status is SUCCESS
+ // if not we print help anyway, but status is FAILURE
+ if (printHelp()) {
+ return CommandResult.SUCCESS;
+ } else if (args != null && args.size() > 0) {
+ printErr("Unknown command: " + args.get(0));
+ return CommandResult.FAILURE;
+ } else {
+ printOut(usage());
+ return CommandResult.FAILURE;
+ }
+ } finally {
+ commandInvocation.stop();
+ }
+ }
+
+ public static String usage() {
+ StringWriter sb = new StringWriter();
+ PrintWriter out = new PrintWriter(sb);
+ out.println("Keycloak Admin CLI");
+ out.println();
+ out.println("Use '" + CMD + " config credentials' command with username and password to start a session against a specific");
+ out.println("server and realm.");
+ out.println();
+ out.println("For example:");
+ out.println();
+ out.println(" " + PROMPT + " " + CMD + " config credentials --server http://localhost:8080/auth --realm master --user admin");
+ out.println(" Enter password: ");
+ out.println(" Logging into http://localhost:8080/auth as user admin of realm master");
+ out.println();
+ out.println("Any configured username can be used for login, but to perform admin operations the user");
+ out.println("needs proper roles, otherwise operations will fail.");
+ out.println();
+ out.println("Usage: " + CMD + " COMMAND [ARGUMENTS]");
+ out.println();
+ out.println("Global options:");
+ out.println(" -x Print full stack trace when exiting with error");
+ out.println(" --help Print help for specific command");
+ out.println(" --config Path to the config file (" + DEFAULT_CONFIG_FILE_STRING + " by default)");
+ out.println();
+ out.println("Commands: ");
+ out.println(" config Set up credentials, and other configuration settings using the config file");
+ out.println(" create Create new resource");
+ out.println(" get Get a resource");
+ out.println(" update Update a resource");
+ out.println(" delete Delete a resource");
+ out.println(" get-roles List roles for a user or a group");
+ out.println(" add-roles Add role to a user or a group");
+ out.println(" remove-roles Remove role from a user or a group");
+ out.println(" set-password Re-set password for a user");
+ out.println(" help This help");
+ out.println();
+ out.println("Use '" + CMD + " help <command>' for more information about a given command.");
+ return sb.toString();
+ }
+}
diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/NewObjectCmd.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/NewObjectCmd.java
new file mode 100644
index 0000000..33fcdc6
--- /dev/null
+++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/NewObjectCmd.java
@@ -0,0 +1,204 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.client.admin.cli.commands;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import org.jboss.aesh.cl.CommandDefinition;
+import org.jboss.aesh.cl.Option;
+import org.jboss.aesh.console.command.CommandException;
+import org.jboss.aesh.console.command.CommandResult;
+import org.jboss.aesh.console.command.invocation.CommandInvocation;
+import org.keycloak.client.admin.cli.common.AttributeOperation;
+import org.keycloak.client.admin.cli.common.CmdStdinContext;
+import org.keycloak.client.admin.cli.util.AccessibleBufferOutputStream;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.InputStream;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.nio.charset.Charset;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+
+import static org.keycloak.client.admin.cli.common.AttributeOperation.Type.SET;
+import static org.keycloak.client.admin.cli.util.IoUtil.copyStream;
+import static org.keycloak.client.admin.cli.util.IoUtil.printErr;
+import static org.keycloak.client.admin.cli.util.OsUtil.CMD;
+import static org.keycloak.client.admin.cli.util.OsUtil.EOL;
+import static org.keycloak.client.admin.cli.util.OsUtil.OS_ARCH;
+import static org.keycloak.client.admin.cli.util.OsUtil.PROMPT;
+import static org.keycloak.client.admin.cli.util.OutputUtil.MAPPER;
+import static org.keycloak.client.admin.cli.util.ParseUtil.mergeAttributes;
+import static org.keycloak.client.admin.cli.util.ParseUtil.parseFileOrStdin;
+import static org.keycloak.client.admin.cli.util.ParseUtil.parseKeyVal;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+@CommandDefinition(name = "new-object", description = "Command to create new JSON objects locally")
+public class NewObjectCmd extends AbstractGlobalOptionsCmd {
+
+ @Option(shortName = 'f', name = "file", description = "Read object from file or standard input if FILENAME is set to '-'", hasValue = true)
+ String file;
+
+ @Option(shortName = 'c', name = "compressed", description = "Don't pretty print the output", hasValue = false)
+ boolean compressed;
+
+ //@OptionGroup(shortName = 's', name = "set", description = "Set attribute to the specified value")
+ //Map<String, String> attributes = new LinkedHashMap<>();
+
+
+ @Override
+ public CommandResult execute(CommandInvocation commandInvocation) throws CommandException, InterruptedException {
+ try {
+ if (printHelp()) {
+ return help ? CommandResult.SUCCESS : CommandResult.FAILURE;
+ }
+
+ processGlobalOptions();
+
+ return process(commandInvocation);
+ } catch (IllegalArgumentException e) {
+ throw new IllegalArgumentException(e.getMessage() + suggestHelp(), e);
+ } finally {
+ commandInvocation.stop();
+ }
+ }
+
+ public CommandResult process(CommandInvocation commandInvocation) throws CommandException, InterruptedException {
+
+ List<AttributeOperation> attrs = new LinkedList<>();
+
+ Iterator<String> it = args.iterator();
+
+ while (it.hasNext()) {
+ String option = it.next();
+ switch (option) {
+ case "-s":
+ case "--set": {
+ if (!it.hasNext()) {
+ throw new IllegalArgumentException("Option " + option + " requires a value");
+ }
+ String[] keyVal = parseKeyVal(it.next());
+ attrs.add(new AttributeOperation(SET, keyVal[0], keyVal[1]));
+ break;
+ }
+ default: {
+ throw new IllegalArgumentException("Invalid option: " + option);
+ }
+ }
+ }
+
+ InputStream body = null;
+
+ CmdStdinContext<JsonNode> ctx = new CmdStdinContext<>();
+
+ if (file != null) {
+ ctx = parseFileOrStdin(file);
+ }
+
+ if (attrs.size() > 0) {
+ ctx = mergeAttributes(ctx, MAPPER.createObjectNode(), attrs);
+ }
+
+ if (body == null && ctx.getContent() != null) {
+ body = new ByteArrayInputStream(ctx.getContent().getBytes(Charset.forName("utf-8")));
+ }
+
+ AccessibleBufferOutputStream abos = new AccessibleBufferOutputStream(System.out);
+
+ if (!compressed) {
+ ByteArrayOutputStream buffer = new ByteArrayOutputStream();
+ copyStream(body, buffer);
+
+ try {
+ JsonNode rootNode = MAPPER.readValue(buffer.toByteArray(), JsonNode.class);
+ // now pretty print it to output
+ MAPPER.writeValue(abos, rootNode);
+ } catch (Exception ignored) {
+ copyStream(new ByteArrayInputStream(buffer.toByteArray()), abos);
+ }
+ } else {
+ copyStream(body, System.out);
+ }
+
+ int lastByte = abos.getLastByte();
+ if (lastByte != -1 && lastByte != 13 && lastByte != 10) {
+ printErr("");
+ }
+
+ return CommandResult.SUCCESS;
+ }
+
+
+ @Override
+ protected boolean nothingToDo() {
+ return file == null && (args == null || args.size() == 0);
+ }
+
+ protected String suggestHelp() {
+ return EOL + "Try '" + CMD + " help create' for more information";
+ }
+
+ protected String help() {
+ return usage();
+ }
+
+ public static String usage() {
+ StringWriter sb = new StringWriter();
+ PrintWriter out = new PrintWriter(sb);
+ out.println("Usage: " + CMD + " new-object [ARGUMENTS]");
+ out.println();
+ out.println("Command to compose JSON objects from attributes, and merge changes into existing JSON documents.");
+ out.println();
+ out.println("This is a local command that does not perform any server requests. It's functionality is fully ");
+ out.println("integrated into 'create', 'update' and 'delete' commands. It's supposed to be a helper tool only.");
+ out.println();
+ out.println("Arguments:");
+ out.println();
+ out.println(" Global options:");
+ out.println(" -x Print full stack trace when exiting with error");
+ out.println();
+ out.println(" Command specific options:");
+ out.println(" -s, --set NAME=VALUE Set a specific attribute NAME to a specified value VALUE");
+ out.println(" -f, --file FILENAME Read object from file or standard input if FILENAME is set to '-'");
+ out.println(" -c, --compressed Don't pretty print the output");
+ out.println();
+ out.println("Examples:");
+ out.println();
+ out.println("Create a new JSON document with two top level attributes:");
+ out.println(" " + PROMPT + " " + CMD + " new-object -s realm=demorealm -s enabled=true");
+ out.println();
+ out.println("Read a JSON document and apply changes on top of it:");
+ if (OS_ARCH.isWindows()) {
+ out.println(" " + PROMPT + " echo { \"clientId\": \"my_client\" } | " + CMD + " new-object -s enabled=true -f -");
+ } else {
+ out.println(" " + PROMPT + " " + CMD + " new-object -s enabled=true -f - << EOF");
+ out.println(" {");
+ out.println(" \"clientId\": \"my_client\"");
+ out.println(" }");
+ out.println(" EOF");
+ }
+ out.println();
+ out.println();
+ out.println("Use '" + CMD + " help' for general information and a list of commands");
+ return sb.toString();
+ }
+
+}
diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/RemoveRolesCmd.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/RemoveRolesCmd.java
new file mode 100644
index 0000000..84698c3
--- /dev/null
+++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/RemoveRolesCmd.java
@@ -0,0 +1,334 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.client.admin.cli.commands;
+
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import org.jboss.aesh.cl.CommandDefinition;
+import org.jboss.aesh.cl.Option;
+import org.jboss.aesh.console.command.CommandException;
+import org.jboss.aesh.console.command.CommandResult;
+import org.jboss.aesh.console.command.invocation.CommandInvocation;
+import org.keycloak.client.admin.cli.config.ConfigData;
+import org.keycloak.client.admin.cli.operations.ClientOperations;
+import org.keycloak.client.admin.cli.operations.GroupOperations;
+import org.keycloak.client.admin.cli.operations.RoleOperations;
+import org.keycloak.client.admin.cli.operations.LocalSearch;
+import org.keycloak.client.admin.cli.operations.UserOperations;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Set;
+
+import static org.keycloak.client.admin.cli.util.AuthUtil.ensureToken;
+import static org.keycloak.client.admin.cli.util.ConfigUtil.DEFAULT_CONFIG_FILE_STRING;
+import static org.keycloak.client.admin.cli.util.ConfigUtil.credentialsAvailable;
+import static org.keycloak.client.admin.cli.util.ConfigUtil.loadConfig;
+import static org.keycloak.client.admin.cli.util.OsUtil.CMD;
+import static org.keycloak.client.admin.cli.util.OsUtil.EOL;
+import static org.keycloak.client.admin.cli.util.OsUtil.PROMPT;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+@CommandDefinition(name = "remove-roles", description = "[ARGUMENTS]")
+public class RemoveRolesCmd extends AbstractAuthOptionsCmd {
+
+ @Option(name = "uusername", description = "Target user's 'username'")
+ String uusername;
+
+ @Option(name = "uid", description = "Target user's 'id'")
+ String uid;
+
+ @Option(name = "gname", description = "Target group's 'name'")
+ String gname;
+
+ @Option(name = "gpath", description = "Target group's 'path'")
+ String gpath;
+
+ @Option(name = "gid", description = "Target group's 'id'")
+ String gid;
+
+ @Option(name = "cclientid", description = "Target client's 'clientId'")
+ String cclientid;
+
+ @Option(name = "cid", description = "Target client's 'id'")
+ String cid;
+
+ @Override
+ public CommandResult execute(CommandInvocation commandInvocation) throws CommandException, InterruptedException {
+
+ List<String> roleNames = new LinkedList<>();
+ List<String> roleIds = new LinkedList<>();
+
+ try {
+ if (printHelp()) {
+ return help ? CommandResult.SUCCESS : CommandResult.FAILURE;
+ }
+
+ processGlobalOptions();
+
+ Iterator<String> it = args.iterator();
+
+ while (it.hasNext()) {
+ String option = it.next();
+ switch (option) {
+ case "--rolename": {
+ optionRequiresValueCheck(it, option);
+ roleNames.add(it.next());
+ break;
+ }
+ case "--roleid": {
+ optionRequiresValueCheck(it, option);
+ roleIds.add(it.next());
+ break;
+ }
+ default: {
+ throw new IllegalArgumentException("Invalid option: " + option);
+ }
+ }
+ }
+
+ if (uid != null && uusername != null) {
+ throw new IllegalArgumentException("Incompatible options: --uid and --uusername are mutually exclusive");
+ }
+
+ if ((gid != null && gname != null) || (gid != null && gpath != null) || (gname != null && gpath != null)) {
+ throw new IllegalArgumentException("Incompatible options: --gid, --gname and --gpath are mutually exclusive");
+ }
+
+ if (roleNames.isEmpty() && roleIds.isEmpty()) {
+ throw new IllegalArgumentException("No role specified. Use --rolename or --roleid to specify roles");
+ }
+
+ if (cid != null && cclientid != null) {
+ throw new IllegalArgumentException("Incompatible options: --cid and --cclientid are mutually exclusive");
+ }
+
+ if (isUserSpecified() && isGroupSpecified()) {
+ throw new IllegalArgumentException("Incompatible options: --uusername / --uid can't be used at the same time as --gname / --gid / --gpath");
+ }
+
+ if (!isUserSpecified() && !isGroupSpecified()) {
+ throw new IllegalArgumentException("No user nor group specified. Use --uusername / --uid to specify user or --gname / --gid / --gpath to specify group");
+ }
+
+
+ ConfigData config = loadConfig();
+ config = copyWithServerInfo(config);
+
+ setupTruststore(config, commandInvocation);
+
+ String auth = null;
+
+ config = ensureAuthInfo(config, commandInvocation);
+ config = copyWithServerInfo(config);
+ if (credentialsAvailable(config)) {
+ auth = ensureToken(config);
+ }
+
+ auth = auth != null ? "Bearer " + auth : null;
+
+ final String server = config.getServerUrl();
+ final String realm = getTargetRealm(config);
+ final String adminRoot = adminRestRoot != null ? adminRestRoot : composeAdminRoot(server);
+
+
+ if (isUserSpecified()) {
+ if (uid == null) {
+ uid = UserOperations.getIdFromUsername(adminRoot, realm, auth, uusername);
+ }
+ if (isClientSpecified()) {
+ // remove client roles from a user
+ if (cid == null) {
+ cid = ClientOperations.getIdFromClientId(adminRoot, realm, auth, cclientid);
+ }
+
+ List<ObjectNode> roles = RoleOperations.getClientRoles(adminRoot, realm, cid, auth);
+ Set<ObjectNode> rolesToAdd = getRoleRepresentations(roleNames, roleIds, new LocalSearch(roles));
+
+ // now remove the roles
+ UserOperations.removeClientRoles(adminRoot, realm, auth, uid, cid, new ArrayList<>(rolesToAdd));
+
+ } else {
+
+ Set<ObjectNode> rolesToAdd = getRoleRepresentations(roleNames, roleIds,
+ new LocalSearch(RoleOperations.getRealmRolesAsNodes(adminRoot, realm, auth)));
+
+ // now remove the roles
+ UserOperations.removeRealmRoles(adminRoot, realm, auth, uid, new ArrayList<>(rolesToAdd));
+ }
+
+ } else if (isGroupSpecified()) {
+ if (gname != null) {
+ gid = GroupOperations.getIdFromName(adminRoot, realm, auth, gname);
+ } else if (gpath != null) {
+ gid = GroupOperations.getIdFromPath(adminRoot, realm, auth, gpath);
+ }
+ if (isClientSpecified()) {
+ // remove client roles from a group
+ if (cid == null) {
+ cid = ClientOperations.getIdFromClientId(adminRoot, realm, auth, cclientid);
+ }
+
+ List<ObjectNode> roles = RoleOperations.getClientRoles(adminRoot, realm, cid, auth);
+ Set<ObjectNode> rolesToAdd = getRoleRepresentations(roleNames, roleIds, new LocalSearch(roles));
+
+ // now remove the roles
+ GroupOperations.removeClientRoles(adminRoot, realm, auth, gid, cid, new ArrayList<>(rolesToAdd));
+
+ } else {
+
+ Set<ObjectNode> rolesToAdd = getRoleRepresentations(roleNames, roleIds,
+ new LocalSearch(RoleOperations.getRealmRolesAsNodes(adminRoot, realm, auth)));
+
+ // now remove the roles
+ GroupOperations.removeRealmRoles(adminRoot, realm, auth, gid, new ArrayList<>(rolesToAdd));
+ }
+
+ } else {
+
+ throw new IllegalArgumentException("No user nor group specified. Use --uusername / --uid to specify user or --gname / --gid / --gpath to specify group");
+ }
+
+ return CommandResult.SUCCESS;
+
+ } catch (IllegalArgumentException e) {
+ throw new IllegalArgumentException(e.getMessage() + suggestHelp(), e);
+ } finally {
+ commandInvocation.stop();
+ }
+ }
+
+ private Set<ObjectNode> getRoleRepresentations(List<String> roleNames, List<String> roleIds, LocalSearch roleSearch) {
+ Set<ObjectNode> rolesToAdd = new HashSet<>();
+
+ // now we process roles
+ for (String name : roleNames) {
+ ObjectNode r = roleSearch.exactMatchOne(name, "name");
+ if (r == null) {
+ throw new RuntimeException("Role not found for name: " + name);
+ }
+ rolesToAdd.add(r);
+ }
+ for (String id : roleIds) {
+ ObjectNode r = roleSearch.exactMatchOne(id, "id");
+ if (r == null) {
+ throw new RuntimeException("Role not found for id: " + id);
+ }
+ rolesToAdd.add(r);
+ }
+ return rolesToAdd;
+ }
+
+ private void optionRequiresValueCheck(Iterator<String> it, String option) {
+ if (!it.hasNext()) {
+ throw new IllegalArgumentException("Option " + option + " requires a value");
+ }
+ }
+
+ private boolean isClientSpecified() {
+ return cid != null || cclientid != null;
+ }
+
+ private boolean isGroupSpecified() {
+ return gid != null || gname != null || gpath != null;
+ }
+
+ private boolean isUserSpecified() {
+ return uid != null || uusername != null;
+ }
+
+
+ @Override
+ protected boolean nothingToDo() {
+ return noOptions() && uusername == null && uid == null && cclientid == null && (args == null || args.size() == 0);
+ }
+
+ protected String suggestHelp() {
+ return EOL + "Try '" + CMD + " help remove-roles' for more information";
+ }
+
+ protected String help() {
+ return usage();
+ }
+
+ public static String usage() {
+ StringWriter sb = new StringWriter();
+ PrintWriter out = new PrintWriter(sb);
+ out.println("Usage: " + CMD + " remove-roles (--uusername USERNAME | --uid ID) [--cclientid CLIENT_ID | --cid ID] (--rolename NAME | --roleid ID)+ [ARGUMENTS]");
+ out.println("Usage: " + CMD + " remove-roles (--gname NAME | --gpath PATH | --gid ID) [--cclientid CLIENT_ID | --cid ID] (--rolename NAME | --roleid ID)+ [ARGUMENTS]");
+ out.println();
+ out.println("Command to remove realm or client roles from a user or group.");
+ out.println();
+ out.println("Use `" + CMD + " config credentials` to establish an authenticated session, or use CREDENTIALS OPTIONS");
+ out.println("to perform one time authentication.");
+ out.println();
+ out.println("If client is specified using --cclientid or --cid then roles to remove are client roles, otherwise they are realm roles.");
+ out.println("Either a user, or a group needs to be specified. If user is specified using --uusername or --uid then roles are removed");
+ out.println("from a specific user. If group is specified using --gname, --gpath or --gid then roles are removed from a specific group.");
+ out.println("One or more roles have to be specified using --rolename or --roleid to be removed from a group or a user.");
+ out.println();
+ out.println("Arguments:");
+ out.println();
+ out.println(" Global options:");
+ out.println(" -x Print full stack trace when exiting with error");
+ out.println(" --config Path to the config file (" + DEFAULT_CONFIG_FILE_STRING + " by default)");
+ out.println(" --truststore PATH Path to a truststore containing trusted certificates");
+ out.println(" --trustpass PASSWORD Truststore password (prompted for if not specified and --truststore is used)");
+ out.println(" CREDENTIALS OPTIONS Same set of options as accepted by '" + CMD + " config credentials' in order to establish");
+ out.println(" an authenticated sessions. This allows on-the-fly transient authentication that does");
+ out.println(" not touch a config file.");
+ out.println();
+ out.println(" Command specific options:");
+ out.println(" --uusername User's 'username'. If more than one user exists with the same username");
+ out.println(" you'll have to use --uid to specify the target user");
+ out.println(" --uid User's 'id' attribute");
+ out.println(" --gname Group's 'name'. If more than one group exists with the same name you'll have");
+ out.println(" to use --gid, or --gpath to specify the target group");
+ out.println(" --gpath Group's 'path' attribute");
+ out.println(" --gid Group's 'id' attribute");
+ out.println(" --cclientid Client's 'clientId' attribute");
+ out.println(" --cid Client's 'id' attribute");
+ out.println(" --rolename Role's 'name' attribute");
+ out.println(" --roleid Role's 'id' attribute");
+ out.println(" -a, --admin-root URL URL of Admin REST endpoint root if not default - e.g. http://localhost:8080/auth/admin");
+ out.println(" -r, --target-realm REALM Target realm to issue requests against if not the one authenticated against");
+ out.println();
+ out.println("Examples:");
+ out.println();
+ out.println("Remove 'offline_access' realm role from a user:");
+ out.println(" " + PROMPT + " " + CMD + " remove-roles -r demorealm --uusername testuser --rolename offline_access");
+ out.println();
+ out.println("Remove 'realm-management' client roles 'view-users', 'view-clients' and 'view-realm' from a user:");
+ out.println(" " + PROMPT + " " + CMD + " remove-roles -r demorealm --uusername testuser --cclientid realm-management --rolename view-users --rolename view-clients --rolename view-realm");
+ out.println();
+ out.println("Remove 'uma_authorization' realm role to a group:");
+ out.println(" " + PROMPT + " " + CMD + " remove-roles -r demorealm --gname PowerUsers --rolename uma_authorization");
+ out.println();
+ out.println("Remove 'realm-management' client roles 'realm-admin' from a group:");
+ out.println(" " + PROMPT + " " + CMD + " remove-roles -r demorealm --gname PowerUsers --cclientid realm-management --rolename realm-admin");
+ out.println();
+ out.println();
+ out.println("Use '" + CMD + " help' for general information and a list of commands");
+ return sb.toString();
+ }
+}
diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/SetPasswordCmd.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/SetPasswordCmd.java
new file mode 100644
index 0000000..f3ce6ea
--- /dev/null
+++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/SetPasswordCmd.java
@@ -0,0 +1,177 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.client.admin.cli.commands;
+
+import org.jboss.aesh.cl.CommandDefinition;
+import org.jboss.aesh.cl.Option;
+import org.jboss.aesh.console.command.CommandException;
+import org.jboss.aesh.console.command.CommandResult;
+import org.jboss.aesh.console.command.invocation.CommandInvocation;
+import org.keycloak.client.admin.cli.config.ConfigData;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+
+import static org.keycloak.client.admin.cli.operations.UserOperations.getIdFromUsername;
+import static org.keycloak.client.admin.cli.operations.UserOperations.resetUserPassword;
+import static org.keycloak.client.admin.cli.util.AuthUtil.ensureToken;
+import static org.keycloak.client.admin.cli.util.ConfigUtil.DEFAULT_CONFIG_FILE_STRING;
+import static org.keycloak.client.admin.cli.util.ConfigUtil.credentialsAvailable;
+import static org.keycloak.client.admin.cli.util.ConfigUtil.loadConfig;
+import static org.keycloak.client.admin.cli.util.IoUtil.readSecret;
+import static org.keycloak.client.admin.cli.util.OsUtil.CMD;
+import static org.keycloak.client.admin.cli.util.OsUtil.EOL;
+import static org.keycloak.client.admin.cli.util.OsUtil.PROMPT;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+@CommandDefinition(name = "set-password", description = "[ARGUMENTS]")
+public class SetPasswordCmd extends AbstractAuthOptionsCmd {
+
+ @Option(name = "username", description = "Username")
+ String username;
+
+ @Option(name = "userid", description = "User ID")
+ String userid;
+
+ @Option(shortName = 'p', name = "new-password", description = "New password")
+ String pass;
+
+ @Option(shortName = 't', name = "temporary", description = "is password temporary", hasValue = false)
+ boolean temporary;
+
+
+ @Override
+ public CommandResult execute(CommandInvocation commandInvocation) throws CommandException, InterruptedException {
+ try {
+ if (printHelp()) {
+ return help ? CommandResult.SUCCESS : CommandResult.FAILURE;
+ }
+
+ processGlobalOptions();
+
+ return process(commandInvocation);
+ } catch (IllegalArgumentException e) {
+ throw new IllegalArgumentException(e.getMessage() + suggestHelp(), e);
+ } finally {
+ commandInvocation.stop();
+ }
+ }
+
+
+ public CommandResult process(CommandInvocation commandInvocation) throws CommandException, InterruptedException {
+
+ if (args != null && args.size() > 0) {
+ throw new IllegalArgumentException("Invalid option: " + args.get(0));
+ }
+
+ if (userid == null && username == null) {
+ throw new IllegalArgumentException("No user specified. Use --username or --userid to specify user");
+ }
+
+ if (userid != null && username != null) {
+ throw new IllegalArgumentException("Options --userid and --username are mutually exclusive");
+ }
+
+ if (pass == null) {
+ pass = readSecret("Enter password: ", commandInvocation);
+ }
+
+ ConfigData config = loadConfig();
+ config = copyWithServerInfo(config);
+
+ setupTruststore(config, commandInvocation);
+
+ String auth = null;
+
+ config = ensureAuthInfo(config, commandInvocation);
+ config = copyWithServerInfo(config);
+ if (credentialsAvailable(config)) {
+ auth = ensureToken(config);
+ }
+
+ auth = auth != null ? "Bearer " + auth : null;
+
+ final String server = config.getServerUrl();
+ final String realm = getTargetRealm(config);
+ final String adminRoot = adminRestRoot != null ? adminRestRoot : composeAdminRoot(server);
+
+ // if username is specified resolve id
+ if (username != null) {
+ userid = getIdFromUsername(adminRoot, realm, auth, username);
+ }
+
+ resetUserPassword(adminRoot, realm, auth, userid, pass, temporary);
+
+ return CommandResult.SUCCESS;
+ }
+
+ @Override
+ protected boolean nothingToDo() {
+ return noOptions() && username == null && userid == null && pass == null;
+ }
+
+ protected String suggestHelp() {
+ return EOL + "Try '" + CMD + " help set-password' for more information";
+ }
+
+
+ protected String help() {
+ return usage();
+ }
+
+ public static String usage() {
+ StringWriter sb = new StringWriter();
+ PrintWriter out = new PrintWriter(sb);
+ out.println("Usage: " + CMD + " set-password (--username USERNAME | --userid ID) [--password PASSWORD] [ARGUMENTS]");
+ out.println();
+ out.println("Command to reset user's password.");
+ out.println();
+ out.println("Use `" + CMD + " config credentials` to establish an authenticated session, or use CREDENTIALS OPTIONS");
+ out.println("to perform one time authentication.");
+ out.println();
+ out.println("Arguments:");
+ out.println();
+ out.println(" Global options:");
+ out.println(" -x Print full stack trace when exiting with error");
+ out.println(" --config Path to the config file (" + DEFAULT_CONFIG_FILE_STRING + " by default)");
+ out.println(" --truststore PATH Path to a truststore containing trusted certificates");
+ out.println(" --trustpass PASSWORD Truststore password (prompted for if not specified and --truststore is used)");
+ out.println(" CREDENTIALS OPTIONS Same set of options as accepted by '" + CMD + " config credentials' in order to establish");
+ out.println(" an authenticated sessions. This allows on-the-fly transient authentication that does");
+ out.println(" not touch a config file.");
+ out.println();
+ out.println(" Command specific options:");
+ out.println(" --username USERNAME Identify target user by 'username'");
+ out.println(" --userid ID Identify target user by 'id'");
+ out.println(" -p, --new-password New password to set. If not specified you will be prompted for it.");
+ out.println(" -t, --temporary Make the new password temporary - user has to change it on next logon");
+ out.println(" -a, --admin-root URL URL of Admin REST endpoint root if not default - e.g. http://localhost:8080/auth/admin");
+ out.println(" -r, --target-realm REALM Target realm to issue requests against if not the one authenticated against");
+ out.println();
+ out.println("Examples:");
+ out.println();
+ out.println("Set new temporary password for the user:");
+ out.println(" " + PROMPT + " " + CMD + " set-password -r demorealm --username testuser --password NEWPASS -t");
+ out.println();
+ out.println();
+ out.println("Use '" + CMD + " help' for general information and a list of commands");
+ return sb.toString();
+ }
+
+}
diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/UpdateCmd.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/UpdateCmd.java
new file mode 100644
index 0000000..3ce91b3
--- /dev/null
+++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/UpdateCmd.java
@@ -0,0 +1,165 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.client.admin.cli.commands;
+
+import org.jboss.aesh.cl.CommandDefinition;
+import org.jboss.aesh.cl.Option;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+
+import static org.keycloak.client.admin.cli.util.ConfigUtil.DEFAULT_CONFIG_FILE_STRING;
+import static org.keycloak.client.admin.cli.util.OsUtil.CMD;
+import static org.keycloak.client.admin.cli.util.OsUtil.EOL;
+import static org.keycloak.client.admin.cli.util.OsUtil.PROMPT;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+@CommandDefinition(name = "update", description = "CLIENT_ID [ARGUMENTS]")
+public class UpdateCmd extends AbstractRequestCmd {
+
+ @Option(shortName = 'f', name = "file", description = "Read object from file or standard input if FILENAME is set to '-'")
+ String file;
+
+ @Option(shortName = 'F', name = "fields", description = "A pattern specifying which attributes of JSON response body to actually display as result - causes mismatch with Content-Length header")
+ String fields;
+
+ @Option(shortName = 'H', name = "print-headers", description = "Print response headers", hasValue = false)
+ boolean printHeaders;
+
+ @Option(shortName = 'm', name = "merge", description = "Merge new values with existing configuration on the server - for when the default is not to merge (i.e. if --file is used)", hasValue = false)
+ boolean mergeMode;
+
+ @Option(shortName = 'n', name = "no-merge", description = "Don't merge new values with existing configuration on the server - for when the default is to merge (i.e. is --set is used while --file is not used)", hasValue = false)
+ boolean noMerge;
+
+ @Option(shortName = 'o', name = "output", description = "After update output the new client configuration", hasValue = false)
+ boolean outputResult;
+
+ @Option(shortName = 'c', name = "compressed", description = "Don't pretty print the output", hasValue = false)
+ boolean compressed;
+
+ //@GroupOption(shortName = 's', name = "set", description = "Set specific attribute to a specified value", hasValue = true)
+ //private List<String> attributes = new ArrayList<>();
+
+
+ @Override
+ void initOptions() {
+ // set options on parent
+ super.file = file;
+ super.fields = fields;
+ super.printHeaders = printHeaders;
+ super.returnId = false;
+ super.outputResult = true;
+ super.compressed = compressed;
+ super.mergeMode = mergeMode;
+ super.noMerge = noMerge;
+ super.outputResult = outputResult;
+ super.httpVerb = "put";
+ }
+
+ @Override
+ protected boolean nothingToDo() {
+ return noOptions() && file == null && (args == null || args.size() == 0);
+ }
+
+ protected String suggestHelp() {
+ return EOL + "Try '" + CMD + " help update' for more information";
+ }
+
+ protected String help() {
+ return usage();
+ }
+
+ public static String usage() {
+ StringWriter sb = new StringWriter();
+ PrintWriter out = new PrintWriter(sb);
+ out.println("Usage: " + CMD + " update ENDPOINT_URI [ARGUMENTS]");
+ out.println();
+ out.println("Command to update existing resources on the server.");
+ out.println();
+ out.println("Use `" + CMD + " config credentials` to establish an authenticated sessions, or use CREDENTIALS OPTIONS");
+ out.println("to perform one time authentication.");
+ out.println();
+ out.println("Arguments:");
+ out.println();
+ out.println(" Global options:");
+ out.println(" -x Print full stack trace when exiting with error");
+ out.println(" --config Path to the config file (" + DEFAULT_CONFIG_FILE_STRING + " by default)");
+ out.println(" --truststore PATH Path to a truststore containing trusted certificates");
+ out.println(" --trustpass PASSWORD Truststore password (prompted for if not specified and --truststore is used)");
+ out.println(" CREDENTIALS OPTIONS Same set of options as accepted by '" + CMD + " config credentials' in order to establish");
+ out.println(" an authenticated sessions. This allows on-the-fly transient authentication that does");
+ out.println(" not touch a config file.");
+ out.println();
+ out.println(" Command specific options:");
+ out.println(" ENDPOINT_URI URI used to compose a target resource url. Commonly used values start with:");
+ out.println(" realms/, users/, roles/, groups/, clients/, keys/, components/ ...");
+ out.println(" If it starts with 'http://' then it will be used as target resource url");
+ out.println(" -r, --target-realm REALM Target realm to issue requests against if not the one authenticated against");
+ out.println(" -s, --set NAME=VALUE Set a specific attribute NAME to a specified value VALUE");
+ out.println(" NAME+=VALUE Add item VALUE to list attribute NAME");
+ out.println(" -d, --delete NAME Remove a specific attribute NAME from JSON request body");
+ out.println(" -f, --file FILENAME Read object from file or standard input if FILENAME is set to '-'");
+ out.println(" -q, --query NAME=VALUE Add to request URI a NAME query parameter with value VALUE");
+ out.println(" -h, --header NAME=VALUE Set request header NAME to VALUE");
+ out.println(" -m, --merge Merge new values with existing configuration on the server");
+ out.println(" Merge is automatically enabled unless --file is specified");
+ out.println(" -n, --no-merge Suppress merge mode");
+ out.println();
+ out.println(" -H, --print-headers Print response headers");
+ out.println(" -o, --output After update output the new resource to standard output");
+ out.println(" -F, --fields FILTER A filter pattern to specify which fields of a JSON response to output");
+ out.println(" -c, --compressed Don't pretty print the output");
+ out.println(" -a, --admin-root URL URL of Admin REST endpoint root if not default - e.g. http://localhost:8080/auth/admin");
+ out.println();
+ out.println();
+ out.println("Nested attributes are supported by using '.' to separate components of a KEY. Optionaly, the KEY components ");
+ out.println("can be quoted with double quotes - e.g. my_client.attributes.\"external.user.id\". If VALUE starts with [ and ");
+ out.println("ends with ] the attribute will be set as a JSON array. If VALUE starts with { and ends with } the attribute ");
+ out.println("will be set as a JSON object. If KEY ends with an array index - e.g. clients[3]=VALUE - then the specified item");
+ out.println("of the array is updated. If KEY+=VALUE syntax is used, then KEY is assumed to be an array, and another item is");
+ out.println("added to it.");
+ out.println();
+ out.println("Attributes can also be deleted. If KEY ends with an array index, then the targeted item of an array is removed");
+ out.println("and the following items are shifted.");
+ out.println();
+ out.println("Merged mode fetches target resource item from the server, applies attribute changes to it, and sends it");
+ out.println("back to the server.");
+ out.println();
+ out.println();
+ out.println("Examples:");
+ out.println();
+ out.println("Update a target realm by fetching current configuration from the server, and applying specified changes");
+ out.println(" " + PROMPT + " " + CMD + " update realms/demorealm -s registrationAllowed=true");
+ out.println();
+ out.println("Update a client by overwriting existing configuration using local file as a template (replace ID with client's 'id'):");
+ out.println(" " + PROMPT + " " + CMD + " update clients/ID -f new_my_client.json -s 'redirectUris=[\"http://localhost:8080/myapp/*\"]'");
+ out.println();
+ out.println("Update client by fetching current configuration from server and merging with specified changes (replace ID with client's 'id'):");
+ out.println(" " + PROMPT + " " + CMD + " update clients/ID -f new_my_client.json -s enabled=true --merge");
+ out.println();
+ out.println("Reset user's password (replace ID with user's 'id'):");
+ out.println(" " + PROMPT + " " + CMD + " update users/ID/reset-password -r demorealm -s type=password -s value=NEWPASSWORD -s temporary=true -n");
+ out.println();
+ out.println();
+ out.println("Use '" + CMD + " help' for general information and a list of commands");
+ return sb.toString();
+ }
+}
diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/common/AttributeKey.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/common/AttributeKey.java
new file mode 100644
index 0000000..7cb0dc8
--- /dev/null
+++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/common/AttributeKey.java
@@ -0,0 +1,170 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.client.admin.cli.common;
+
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public class AttributeKey {
+
+ private static final int START = 0;
+ private static final int QUOTED = 1;
+ private static final int UNQUOTED = 2;
+ private static final int END = 3;
+
+ private List<Component> components;
+ private boolean append;
+
+ public AttributeKey() {
+ components = Collections.emptyList();
+ }
+
+ public AttributeKey(String key) {
+ if (key.endsWith("+")) {
+ append = true;
+ key = key.substring(0, key.length() - 1);
+ }
+ components = parse(key);
+ }
+
+ static List<Component> parse(String key) {
+
+ if (key == null || "".equals(key)) {
+ return Collections.emptyList();
+ }
+
+ List<Component> cs = new LinkedList<>();
+ StringBuilder sb = new StringBuilder();
+ int state = START;
+
+ char[] buf = key.toCharArray();
+
+ for (int pos = 0; pos < buf.length; pos++) {
+ char c = buf[pos];
+
+ if (state == START) {
+ if ('\"' == c) {
+ state = QUOTED;
+ } else if ('.' == c) {
+ throw new RuntimeException("Invalid attribute key: " + key + " (at position " + (pos + 1) + ")");
+ } else {
+ state = UNQUOTED;
+ sb.append(c);
+ }
+ } else if (state == QUOTED) {
+ if ('\"' == c) {
+ state = END;
+ } else {
+ sb.append(c);
+ }
+ } else if (state == UNQUOTED || state == END) {
+ if ('.' == c) {
+ state = START;
+ cs.add(new Component(sb.toString()));
+ sb.setLength(0);
+ } else if (state == END || '\"' == c) {
+ throw new RuntimeException("Invalid attribute key: " + key + " (at position " + (pos + 1) + ")");
+ } else {
+ sb.append(c);
+ }
+ }
+ }
+
+ boolean ok = false;
+ if (sb.length() > 0) {
+ if (state == UNQUOTED || state == END) {
+ cs.add(new Component(sb.toString()));
+ ok = true;
+ }
+ } else if (state == END) {
+ ok = true;
+ }
+
+ if (!ok) {
+ throw new RuntimeException("Invalid attribute key: " + key + " (at position " + (buf.length) + ")");
+ }
+
+ return Collections.unmodifiableList(cs);
+ }
+
+ public List<Component> getComponents() {
+ return components;
+ }
+
+ public boolean isAppend() {
+ return append;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ for (Component c: components) {
+ if (sb.length() > 0) {
+ sb.append(".");
+ }
+ sb.append(c.toString());
+ }
+ return sb.toString();
+ }
+
+
+
+ public static class Component {
+
+ private int index = -1;
+ private String name;
+
+ Component(String name) {
+ if (name.endsWith("]")) {
+ int pos = name.lastIndexOf("[", name.length() - 1);
+ if (pos == -1) {
+ throw new RuntimeException("Invalid attribute key: " + name + " (']' not allowed here)");
+ }
+ String idx = name.substring(pos + 1, name.length() - 1);
+ try {
+ index = Integer.parseInt(idx);
+ } catch (Exception e) {
+ throw new RuntimeException("Invalid attribute key: " + name + " (Invalid array index: '[" + idx + "]')");
+ }
+ this.name = name.substring(0, pos);
+ } else {
+ this.name = name;
+ }
+ }
+
+ public boolean isArray() {
+ return index >= 0;
+ }
+
+ public int getIndex() {
+ return index;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ @Override
+ public String toString() {
+ return name + (index != -1 ? "[" + index + "]" : "");
+ }
+ }
+}
diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/common/AttributeOperation.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/common/AttributeOperation.java
new file mode 100644
index 0000000..4db483e
--- /dev/null
+++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/common/AttributeOperation.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.client.admin.cli.common;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public class AttributeOperation {
+
+ private Type type;
+ private AttributeKey key;
+ private String value;
+
+ public AttributeOperation(Type type, String key) {
+ this(type, key, null);
+ }
+
+ public AttributeOperation(Type type, String key, String value) {
+ if (type == Type.DELETE && value != null) {
+ throw new IllegalArgumentException("When type is DELETE, value has to be null");
+ }
+ this.type = type;
+ this.key = new AttributeKey(key);
+ this.value = value;
+ }
+
+ public Type getType() {
+ return type;
+ }
+
+ public AttributeKey getKey() {
+ return key;
+ }
+
+ public String getValue() {
+ return value;
+ }
+
+
+ public enum Type {
+ SET,
+ DELETE
+ }
+}
diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/common/CmdStdinContext.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/common/CmdStdinContext.java
new file mode 100644
index 0000000..1c67526
--- /dev/null
+++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/common/CmdStdinContext.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.client.admin.cli.common;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public class CmdStdinContext<T> {
+
+ private T result;
+ private String content;
+
+ public CmdStdinContext() {}
+
+ public T getResult() {
+ return result;
+ }
+
+ public void setResult(T result) {
+ this.result = result;
+ }
+
+ public String getContent() {
+ return content;
+ }
+
+ public void setContent(String content) {
+ this.content = content;
+ }
+}
diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/config/ConfigData.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/config/ConfigData.java
new file mode 100644
index 0000000..e327335
--- /dev/null
+++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/config/ConfigData.java
@@ -0,0 +1,176 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.client.admin.cli.config;
+
+import org.keycloak.util.JsonSerialization;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public class ConfigData {
+
+ private String serverUrl;
+
+ private String realm;
+
+ private String truststore;
+
+ private String trustpass;
+
+ private Map<String, Map<String, RealmConfigData>> endpoints = new HashMap<>();
+
+
+ public String getServerUrl() {
+ return serverUrl;
+ }
+
+ public void setServerUrl(String serverUrl) {
+ this.serverUrl = serverUrl;
+ }
+
+ public String getRealm() {
+ return realm;
+ }
+
+ public void setRealm(String realm) {
+ this.realm = realm;
+ }
+
+ public String getTruststore() {
+ return truststore;
+ }
+
+ public void setTruststore(String truststore) {
+ this.truststore = truststore;
+ }
+
+ public String getTrustpass() {
+ return trustpass;
+ }
+
+ public void setTrustpass(String trustpass) {
+ this.trustpass = trustpass;
+ }
+
+ public Map<String, Map<String, RealmConfigData>> getEndpoints() {
+ return endpoints;
+ }
+
+ public void setEndpoints(Map<String, Map<String, RealmConfigData>> endpoints) {
+ for (Map.Entry<String, Map<String, RealmConfigData>> entry: endpoints.entrySet()) {
+ String endpoint = entry.getKey();
+ for (Map.Entry<String, RealmConfigData> sub: entry.getValue().entrySet()) {
+ RealmConfigData rdata = sub.getValue();
+ rdata.serverUrl(endpoint);
+ rdata.realm(sub.getKey());
+ }
+ }
+ this.endpoints = endpoints;
+ }
+
+ public RealmConfigData sessionRealmConfigData() {
+ if (serverUrl == null)
+ throw new RuntimeException("Illegal state - no current endpoint in config data");
+ if (realm == null)
+ throw new RuntimeException("Illegal state - no current realm in config data");
+ return ensureRealmConfigData(serverUrl, realm);
+ }
+
+ public RealmConfigData getRealmConfigData(String endpoint, String realm) {
+ Map<String, RealmConfigData> realmData = endpoints.get(endpoint);
+ if (realmData == null) {
+ return null;
+ }
+ return realmData.get(realm);
+ }
+
+ public RealmConfigData ensureRealmConfigData(String endpoint, String realm) {
+ RealmConfigData result = getRealmConfigData(endpoint, realm);
+ if (result == null) {
+ result = new RealmConfigData();
+ result.serverUrl(endpoint);
+ result.realm(realm);
+ setRealmConfigData(result);
+ }
+ return result;
+ }
+
+
+ public void setRealmConfigData(RealmConfigData data) {
+ Map<String, RealmConfigData> realm = endpoints.get(data.serverUrl());
+ if (realm == null) {
+ realm = new HashMap<>();
+ endpoints.put(data.serverUrl(), realm);
+ }
+ realm.put(data.realm(), data);
+ }
+
+ public void merge(ConfigData source) {
+ serverUrl = source.serverUrl;
+ realm = source.realm;
+ truststore = source.truststore;
+ trustpass = source.trustpass;
+
+ RealmConfigData current = getRealmConfigData(serverUrl, realm);
+ RealmConfigData sourceRealm = source.getRealmConfigData(serverUrl, realm);
+
+ if (current == null) {
+ setRealmConfigData(sourceRealm);
+ } else {
+ current.merge(sourceRealm);
+ }
+ }
+
+ public ConfigData deepcopy() {
+ ConfigData data = new ConfigData();
+ data.serverUrl = serverUrl;
+ data.realm = realm;
+ data.truststore = truststore;
+ data.trustpass = trustpass;
+ data.endpoints = new HashMap<>();
+
+ for (Map.Entry<String, Map<String, RealmConfigData>> item: endpoints.entrySet()) {
+
+ Map<String, RealmConfigData> nuitems = new HashMap<>();
+ Map<String, RealmConfigData> curitems = item.getValue();
+
+ if (curitems != null) {
+ for (Map.Entry<String, RealmConfigData> ditem : curitems.entrySet()) {
+ RealmConfigData nudata = ditem.getValue();
+ if (nudata != null) {
+ nuitems.put(ditem.getKey(), nudata.deepcopy());
+ }
+ }
+ data.endpoints.put(item.getKey(), nuitems);
+ }
+ }
+ return data;
+ }
+
+ @Override
+ public String toString() {
+ try {
+ return JsonSerialization.writeValueAsPrettyString(this);
+ } catch (IOException e) {
+ return super.toString() + " - Error: " + e.toString();
+ }
+ }
+}
diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/config/ConfigHandler.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/config/ConfigHandler.java
new file mode 100644
index 0000000..fd318c5
--- /dev/null
+++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/config/ConfigHandler.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.client.admin.cli.config;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public interface ConfigHandler {
+
+ void saveMergeConfig(ConfigUpdateOperation op);
+
+ ConfigData loadConfig();
+
+}
diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/config/ConfigUpdateOperation.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/config/ConfigUpdateOperation.java
new file mode 100644
index 0000000..15276a0
--- /dev/null
+++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/config/ConfigUpdateOperation.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.client.admin.cli.config;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public interface ConfigUpdateOperation {
+
+ void update(ConfigData data);
+
+}
diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/config/FileConfigHandler.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/config/FileConfigHandler.java
new file mode 100644
index 0000000..cb76dfe
--- /dev/null
+++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/config/FileConfigHandler.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.client.admin.cli.config;
+
+import org.keycloak.client.admin.cli.util.IoUtil;
+import org.keycloak.util.JsonSerialization;
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.RandomAccessFile;
+import java.nio.channels.FileChannel;
+import java.nio.channels.FileLock;
+import java.nio.channels.OverlappingFileLockException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+
+import static org.keycloak.client.admin.cli.util.IoUtil.printErr;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public class FileConfigHandler implements ConfigHandler {
+
+ private static final long MAX_SIZE = 10 * 1024 * 1024;
+ private static String configFile;
+
+ public static void setConfigFile(String filename) {
+ configFile = filename;
+ }
+
+ public static String getConfigFile() {
+ return configFile;
+ }
+
+ public ConfigData loadConfig() {
+ // for now just dumb impl ignoring file locks for read
+ File file = new File(configFile);
+ if (!file.isFile() || file.length() == 0) {
+ return new ConfigData();
+ }
+
+ try {
+ try (FileInputStream is = new FileInputStream(configFile)) {
+ return JsonSerialization.readValue(is, ConfigData.class);
+ }
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to load " + configFile, e);
+ }
+ }
+
+ public static void ensureFile() {
+ Path path = null;
+ try {
+ path = Paths.get(new File(configFile).getAbsolutePath());
+ IoUtil.ensureFile(path);
+ } catch (Exception e) {
+ throw new RuntimeException("Failed to create config file: " + path, e);
+ }
+ }
+
+ public void saveMergeConfig(ConfigUpdateOperation op) {
+ try {
+ ensureFile();
+
+ try (RandomAccessFile file = new RandomAccessFile(new File(configFile), "rw")) {
+ FileChannel fileChannel = file.getChannel();
+
+ FileLock fileLock = null;
+
+ // lock file for write
+ int tryCount = 0;
+ do try {
+ fileLock = fileChannel.tryLock();
+ break;
+ } catch (OverlappingFileLockException e) {
+ // sleep a little, and try again
+ try {
+ Thread.sleep(100);
+ continue;
+ } catch (InterruptedException e1) {
+ throw new RuntimeException("Interrupted");
+ }
+ } while (tryCount++ < 10);
+
+ if (fileLock != null) {
+ try {
+ // load config from file
+ ConfigData config = new ConfigData();
+ long size = file.length();
+ if (size > MAX_SIZE) {
+ printErr("Config file " + configFile + " is too big. It will be overwritten.");
+ file.setLength(0);
+ } else if (size > 0){
+ byte[] buf = new byte[(int) size];
+ file.readFully(buf);
+ config = JsonSerialization.readValue(new ByteArrayInputStream(buf), ConfigData.class);
+ }
+
+ // update loaded config
+ op.update(config);
+
+ // save config to file
+ byte [] content = JsonSerialization.writeValueAsPrettyString(config).getBytes("utf-8");
+ file.seek(0);
+ file.write(content);
+ file.setLength(content.length);
+
+ } finally {
+ fileLock.release();
+ }
+ } else {
+ throw new RuntimeException("Failed to get lock on " + configFile);
+ }
+ }
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to save " + configFile, e);
+ }
+ }
+}
diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/config/InMemoryConfigHandler.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/config/InMemoryConfigHandler.java
new file mode 100644
index 0000000..7ac4377
--- /dev/null
+++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/config/InMemoryConfigHandler.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.client.admin.cli.config;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public class InMemoryConfigHandler implements ConfigHandler {
+
+ private ConfigData cached;
+
+ @Override
+ public void saveMergeConfig(ConfigUpdateOperation config) {
+ config.update(cached);
+ }
+
+ @Override
+ public ConfigData loadConfig() {
+ return cached;
+ }
+
+ public void setConfigData(ConfigData data) {
+ this.cached = data;
+ }
+}
diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/config/RealmConfigData.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/config/RealmConfigData.java
new file mode 100644
index 0000000..2a8b163
--- /dev/null
+++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/config/RealmConfigData.java
@@ -0,0 +1,172 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.client.admin.cli.config;
+
+import org.keycloak.util.JsonSerialization;
+
+import java.io.IOException;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public class RealmConfigData {
+
+ private String serverUrl;
+
+ private String realm;
+
+ private String clientId;
+
+ private String token;
+
+ private String refreshToken;
+
+ private String signingToken;
+
+ private String secret;
+
+ private Long expiresAt;
+
+ private Long refreshExpiresAt;
+
+ private Long sigExpiresAt;
+
+
+ public String serverUrl() {
+ return serverUrl;
+ }
+
+ public void serverUrl(String serverUrl) {
+ this.serverUrl = serverUrl;
+ }
+
+ public String realm() {
+ return realm;
+ }
+
+ public void realm(String realm) {
+ this.realm = realm;
+ }
+
+ public String getClientId() {
+ return clientId;
+ }
+
+ public void setClientId(String clientId) {
+ this.clientId = clientId;
+ }
+
+ public String getToken() {
+ return token;
+ }
+
+ public void setToken(String token) {
+ this.token = token;
+ }
+
+ public String getRefreshToken() {
+ return refreshToken;
+ }
+
+ public void setRefreshToken(String refreshToken) {
+ this.refreshToken = refreshToken;
+ }
+
+ public String getSigningToken() {
+ return signingToken;
+ }
+
+ public void setSigningToken(String signingToken) {
+ this.signingToken = signingToken;
+ }
+
+ public String getSecret() {
+ return secret;
+ }
+
+ public void setSecret(String secret) {
+ this.secret = secret;
+ }
+
+ public Long getExpiresAt() {
+ return expiresAt;
+ }
+
+ public void setExpiresAt(Long expiresAt) {
+ this.expiresAt = expiresAt;
+ }
+
+ public Long getRefreshExpiresAt() {
+ return refreshExpiresAt;
+ }
+
+ public void setRefreshExpiresAt(Long refreshExpiresAt) {
+ this.refreshExpiresAt = refreshExpiresAt;
+ }
+
+ public Long getSigExpiresAt() {
+ return sigExpiresAt;
+ }
+
+ public void setSigExpiresAt(Long sigExpiresAt) {
+ this.sigExpiresAt = sigExpiresAt;
+ }
+
+ public void merge(RealmConfigData source) {
+ serverUrl = source.serverUrl;
+ realm = source.realm;
+ clientId = source.clientId;
+ token = source.token;
+ refreshToken = source.refreshToken;
+ signingToken = source.signingToken;
+ secret = source.secret;
+ expiresAt = source.expiresAt;
+ refreshExpiresAt = source.refreshExpiresAt;
+ sigExpiresAt = source.sigExpiresAt;
+ }
+
+ public void mergeRefreshTokens(RealmConfigData source) {
+ token = source.token;
+ refreshToken = source.refreshToken;
+ expiresAt = source.expiresAt;
+ refreshExpiresAt = source.refreshExpiresAt;
+ }
+
+ @Override
+ public String toString() {
+ try {
+ return JsonSerialization.writeValueAsPrettyString(this);
+ } catch (IOException e) {
+ return super.toString() + " - Error: " + e.toString();
+ }
+ }
+
+ public RealmConfigData deepcopy() {
+ RealmConfigData data = new RealmConfigData();
+ data.serverUrl = serverUrl;
+ data.realm = realm;
+ data.clientId = clientId;
+ data.token = token;
+ data.refreshToken = refreshToken;
+ data.signingToken = signingToken;
+ data.secret = secret;
+ data.expiresAt = expiresAt;
+ data.refreshExpiresAt = refreshExpiresAt;
+ data.sigExpiresAt = sigExpiresAt;
+ return data;
+ }
+}
diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/httpcomponents/HttpDelete.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/httpcomponents/HttpDelete.java
new file mode 100644
index 0000000..7d553ea
--- /dev/null
+++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/httpcomponents/HttpDelete.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.client.admin.cli.httpcomponents;
+
+import org.apache.http.annotation.NotThreadSafe;
+import org.apache.http.client.methods.HttpEntityEnclosingRequestBase;
+
+import java.net.URI;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+@NotThreadSafe
+public class HttpDelete extends HttpEntityEnclosingRequestBase {
+
+ public HttpDelete(final String uri) {
+ super();
+ setURI(URI.create(uri));
+ }
+
+ @Override
+ public String getMethod() {
+ return "DELETE";
+ }
+}
diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/KcAdmMain.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/KcAdmMain.java
new file mode 100644
index 0000000..9480077
--- /dev/null
+++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/KcAdmMain.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.client.admin.cli;
+
+import org.jboss.aesh.console.AeshConsoleBuilder;
+import org.jboss.aesh.console.AeshConsoleImpl;
+import org.jboss.aesh.console.Prompt;
+import org.jboss.aesh.console.command.registry.AeshCommandRegistryBuilder;
+import org.jboss.aesh.console.command.registry.CommandRegistry;
+import org.jboss.aesh.console.settings.Settings;
+import org.jboss.aesh.console.settings.SettingsBuilder;
+import org.keycloak.client.admin.cli.aesh.AeshEnhancer;
+import org.keycloak.client.admin.cli.aesh.Globals;
+import org.keycloak.client.admin.cli.aesh.ValveInputStream;
+import org.keycloak.client.admin.cli.commands.KcAdmCmd;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public class KcAdmMain {
+
+ public static void main(String [] args) {
+
+ Globals.stdin = new ValveInputStream();
+
+ Settings settings = new SettingsBuilder()
+ .logging(false)
+ .readInputrc(false)
+ .disableCompletion(true)
+ .disableHistory(true)
+ .enableAlias(false)
+ .enableExport(false)
+ .inputStream(Globals.stdin)
+ .create();
+
+ CommandRegistry registry = new AeshCommandRegistryBuilder()
+ .command(KcAdmCmd.class)
+ .create();
+
+ AeshConsoleImpl console = (AeshConsoleImpl) new AeshConsoleBuilder()
+ .settings(settings)
+ .commandRegistry(registry)
+ .prompt(new Prompt(""))
+// .commandInvocationProvider(new CommandInvocationServices() {
+//
+// })
+ .create();
+
+ AeshEnhancer.enhance(console);
+
+ // work around parser issues with quotes and brackets
+ ArrayList<String> arguments = new ArrayList<>();
+ arguments.add("kcadm");
+ arguments.addAll(Arrays.asList(args));
+ Globals.args = arguments;
+
+ StringBuilder b = new StringBuilder();
+ for (String s : args) {
+ // quote if necessary
+ boolean needQuote = false;
+ needQuote = s.indexOf(' ') != -1 || s.indexOf('\"') != -1 || s.indexOf('\'') != -1;
+ b.append(' ');
+ if (needQuote) {
+ b.append('\'');
+ }
+ b.append(s);
+ if (needQuote) {
+ b.append('\'');
+ }
+ }
+ console.setEcho(false);
+
+ console.execute("kcadm" + b.toString());
+
+ console.start();
+ }
+}
diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/operations/ClientOperations.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/operations/ClientOperations.java
new file mode 100644
index 0000000..7de4e29
--- /dev/null
+++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/operations/ClientOperations.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.client.admin.cli.operations;
+
+import static org.keycloak.client.admin.cli.util.HttpUtil.getIdForType;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public class ClientOperations {
+
+ public static String getIdFromClientId(String rootUrl, String realm, String auth, String clientId) {
+ return getIdForType(rootUrl, realm, auth, "clients", "clientId", clientId);
+ }
+}
diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/operations/GroupOperations.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/operations/GroupOperations.java
new file mode 100644
index 0000000..a9cec66
--- /dev/null
+++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/operations/GroupOperations.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.client.admin.cli.operations;
+
+import java.util.List;
+
+import static org.keycloak.client.admin.cli.util.HttpUtil.composeResourceUrl;
+import static org.keycloak.client.admin.cli.util.HttpUtil.doDeleteJSON;
+import static org.keycloak.client.admin.cli.util.HttpUtil.doPostJSON;
+import static org.keycloak.client.admin.cli.util.HttpUtil.getIdForType;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public class GroupOperations {
+
+ public static String getIdFromName(String rootUrl, String realm, String auth, String groupname) {
+ return getIdForType(rootUrl, realm, auth, "groups", "name", groupname);
+ }
+
+ public static String getIdFromPath(String rootUrl, String realm, String auth, String path) {
+ return getIdForType(rootUrl, realm, auth, "groups", "path", path);
+ }
+
+ public static void addRealmRoles(String rootUrl, String realm, String auth, String groupid, List<?> roles) {
+ String resourceUrl = composeResourceUrl(rootUrl, realm, "groups/" + groupid + "/role-mappings/realm");
+ doPostJSON(resourceUrl, auth, roles);
+ }
+
+ public static void addClientRoles(String rootUrl, String realm, String auth, String groupid, String idOfClient, List<?> roles) {
+ String resourceUrl = composeResourceUrl(rootUrl, realm, "groups/" + groupid + "/role-mappings/clients/" + idOfClient);
+ doPostJSON(resourceUrl, auth, roles);
+ }
+
+ public static void removeRealmRoles(String rootUrl, String realm, String auth, String groupid, List<?> roles) {
+ String resourceUrl = composeResourceUrl(rootUrl, realm, "groups/" + groupid + "/role-mappings/realm");
+ doDeleteJSON(resourceUrl, auth, roles);
+ }
+
+ public static void removeClientRoles(String rootUrl, String realm, String auth, String groupid, String idOfClient, List<?> roles) {
+ String resourceUrl = composeResourceUrl(rootUrl, realm, "groups/" + groupid + "/role-mappings/clients/" + idOfClient);
+ doDeleteJSON(resourceUrl, auth, roles);
+ }
+}
diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/operations/LocalSearch.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/operations/LocalSearch.java
new file mode 100644
index 0000000..c91bb95
--- /dev/null
+++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/operations/LocalSearch.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.client.admin.cli.operations;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public class LocalSearch {
+
+ private List<ObjectNode> items;
+
+ public LocalSearch(List<ObjectNode> items) {
+ this.items = items;
+ }
+
+ public ObjectNode exactMatchOne(String value, String ... attrs) {
+
+ List<ObjectNode> matched = new LinkedList<>();
+
+ for (ObjectNode item: items) {
+ for (String attr: attrs) {
+ JsonNode node = item.get(attr);
+ if (node != null && node.asText().equals(value)) {
+ matched.add(item);
+ break;
+ }
+ }
+ }
+
+ if (matched.size() == 0) {
+ return null;
+ }
+
+ if (matched.size() > 1) {
+ throw new RuntimeException("More than one match");
+ }
+
+ return matched.get(0);
+ }
+}
diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/operations/RoleOperations.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/operations/RoleOperations.java
new file mode 100644
index 0000000..d3274fd
--- /dev/null
+++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/operations/RoleOperations.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.client.admin.cli.operations;
+
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import org.keycloak.representations.idm.RoleRepresentation;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import static org.keycloak.client.admin.cli.util.HttpUtil.composeResourceUrl;
+import static org.keycloak.client.admin.cli.util.HttpUtil.doGetJSON;
+import static org.keycloak.client.admin.cli.util.HttpUtil.getAttrForType;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public class RoleOperations {
+
+ public static class LIST_OF_ROLES extends ArrayList<RoleRepresentation>{};
+ public static class LIST_OF_NODES extends ArrayList<ObjectNode>{};
+
+ public static String getRoleNameFromId(String adminRoot, String realm, String auth, String rid) {
+ return getAttrForType(adminRoot, realm, auth, "roles", "id", rid, "name");
+ }
+
+ public static String getClientRoleNameFromId(String adminRoot, String realm, String auth, String cid, String rid) {
+ return getAttrForType(adminRoot, realm, auth, "clients/" + cid + "/roles", "id", rid, "name");
+ }
+
+ public static List<RoleRepresentation> getRealmRoles(String rootUrl, String realm, String auth) {
+ String resourceUrl = composeResourceUrl(rootUrl, realm, "roles");
+ return doGetJSON(LIST_OF_ROLES.class, resourceUrl, auth);
+ }
+
+ public static ObjectNode getRealmRole(String rootUrl, String realm, String rolename, String auth) {
+ String resourceUrl = composeResourceUrl(rootUrl, realm, "roles/" + rolename);
+ return doGetJSON(ObjectNode.class, resourceUrl, auth);
+ }
+
+ public static List<ObjectNode> getClientRoles(String rootUrl, String realm, String idOfClient, String auth) {
+ String resourceUrl = composeResourceUrl(rootUrl, realm, "clients/" + idOfClient + "/roles");
+ return doGetJSON(LIST_OF_NODES.class, resourceUrl, auth);
+ }
+
+ public static ObjectNode getClientRole(String rootUrl, String realm, String idOfClient, String rolename, String auth) {
+ String resourceUrl = composeResourceUrl(rootUrl, realm, "clients/" + idOfClient + "/roles/" + rolename);
+ return doGetJSON(ObjectNode.class, resourceUrl, auth);
+ }
+
+ public static List<ObjectNode> getRealmRolesAsNodes(String rootUrl, String realm, String auth) {
+ String resourceUrl = composeResourceUrl(rootUrl, realm, "roles");
+ return doGetJSON(LIST_OF_NODES.class, resourceUrl, auth);
+ }
+
+ public static List<ObjectNode> getRealmRolesForUserAsNodes(String rootUrl, String realm, String userid, String auth) {
+ String resourceUrl = composeResourceUrl(rootUrl, realm, "users/" + userid + "/role-mappings/realm");
+ return doGetJSON(LIST_OF_NODES.class, resourceUrl, auth);
+ }
+
+ public static List<ObjectNode> getCompositeRealmRolesForUserAsNodes(String rootUrl, String realm, String userid, String auth) {
+ String resourceUrl = composeResourceUrl(rootUrl, realm, "users/" + userid + "/role-mappings/realm/composite");
+ return doGetJSON(LIST_OF_NODES.class, resourceUrl, auth);
+ }
+
+ public static List<ObjectNode> getAvailableRealmRolesForUserAsNodes(String rootUrl, String realm, String userid, String auth) {
+ String resourceUrl = composeResourceUrl(rootUrl, realm, "users/" + userid + "/role-mappings/realm/available");
+ return doGetJSON(LIST_OF_NODES.class, resourceUrl, auth);
+ }
+
+ public static List<ObjectNode> getClientRolesForUserAsNodes(String rootUrl, String realm, String userid, String idOfClient, String auth) {
+ String resourceUrl = composeResourceUrl(rootUrl, realm, "users/" + userid + "/role-mappings/clients/" + idOfClient);
+ return doGetJSON(LIST_OF_NODES.class, resourceUrl, auth);
+ }
+
+ public static List<ObjectNode> getCompositeClientRolesForUserAsNodes(String rootUrl, String realm, String userid, String idOfClient, String auth) {
+ String resourceUrl = composeResourceUrl(rootUrl, realm, "users/" + userid + "/role-mappings/clients/" + idOfClient + "/composite");
+ return doGetJSON(LIST_OF_NODES.class, resourceUrl, auth);
+ }
+
+ public static List<ObjectNode> getAvailableClientRolesForUserAsNodes(String rootUrl, String realm, String userid, String idOfClient, String auth) {
+ String resourceUrl = composeResourceUrl(rootUrl, realm, "users/" + userid + "/role-mappings/clients/" + idOfClient + "/available");
+ return doGetJSON(LIST_OF_NODES.class, resourceUrl, auth);
+ }
+
+ public static List<ObjectNode> getRealmRolesForGroupAsNodes(String rootUrl, String realm, String groupid, String auth) {
+ String resourceUrl = composeResourceUrl(rootUrl, realm, "groups/" + groupid + "/role-mappings/realm");
+ return doGetJSON(LIST_OF_NODES.class, resourceUrl, auth);
+ }
+
+ public static List<ObjectNode> getCompositeRealmRolesForGroupAsNodes(String rootUrl, String realm, String groupid, String auth) {
+ String resourceUrl = composeResourceUrl(rootUrl, realm, "groups/" + groupid + "/role-mappings/realm/composite");
+ return doGetJSON(LIST_OF_NODES.class, resourceUrl, auth);
+ }
+
+ public static List<ObjectNode> getAvailableRealmRolesForGroupAsNodes(String rootUrl, String realm, String groupid, String auth) {
+ String resourceUrl = composeResourceUrl(rootUrl, realm, "groups/" + groupid + "/role-mappings/realm/available");
+ return doGetJSON(LIST_OF_NODES.class, resourceUrl, auth);
+ }
+
+ public static List<ObjectNode> getClientRolesForGroupAsNodes(String rootUrl, String realm, String groupid, String idOfClient, String auth) {
+ String resourceUrl = composeResourceUrl(rootUrl, realm, "groups/" + groupid + "/role-mappings/clients/" + idOfClient);
+ return doGetJSON(LIST_OF_NODES.class, resourceUrl, auth);
+ }
+
+ public static List<ObjectNode> getCompositeClientRolesForGroupAsNodes(String rootUrl, String realm, String groupid, String idOfClient, String auth) {
+ String resourceUrl = composeResourceUrl(rootUrl, realm, "groups/" + groupid + "/role-mappings/clients/" + idOfClient + "/composite");
+ return doGetJSON(LIST_OF_NODES.class, resourceUrl, auth);
+ }
+
+ public static List<ObjectNode> getAvailableClientRolesForGroupAsNodes(String rootUrl, String realm, String groupid, String idOfClient, String auth) {
+ String resourceUrl = composeResourceUrl(rootUrl, realm, "groups/" + groupid + "/role-mappings/clients/" + idOfClient + "/available");
+ return doGetJSON(LIST_OF_NODES.class, resourceUrl, auth);
+ }
+}
diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/operations/UserOperations.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/operations/UserOperations.java
new file mode 100644
index 0000000..593c97e
--- /dev/null
+++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/operations/UserOperations.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.client.admin.cli.operations;
+
+import org.keycloak.client.admin.cli.util.Headers;
+import org.keycloak.client.admin.cli.util.HeadersBody;
+import org.keycloak.client.admin.cli.util.HeadersBodyStatus;
+import org.keycloak.client.admin.cli.util.HttpUtil;
+import org.keycloak.representations.idm.CredentialRepresentation;
+import org.keycloak.util.JsonSerialization;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.util.List;
+
+import static org.keycloak.client.admin.cli.util.HttpUtil.composeResourceUrl;
+import static org.keycloak.client.admin.cli.util.HttpUtil.doDeleteJSON;
+import static org.keycloak.client.admin.cli.util.HttpUtil.doPostJSON;
+import static org.keycloak.client.admin.cli.util.HttpUtil.getIdForType;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public class UserOperations {
+
+ public static void addRealmRoles(String rootUrl, String realm, String auth, String userid, List<?> roles) {
+ String resourceUrl = composeResourceUrl(rootUrl, realm, "users/" + userid + "/role-mappings/realm");
+ doPostJSON(resourceUrl, auth, roles);
+ }
+
+ public static void addClientRoles(String rootUrl, String realm, String auth, String userid, String idOfClient, List<?> roles) {
+ String resourceUrl = composeResourceUrl(rootUrl, realm, "users/" + userid + "/role-mappings/clients/" + idOfClient);
+ doPostJSON(resourceUrl, auth, roles);
+ }
+
+ public static void removeRealmRoles(String rootUrl, String realm, String auth, String userid, List<?> roles) {
+ String resourceUrl = composeResourceUrl(rootUrl, realm, "users/" + userid + "/role-mappings/realm");
+ doDeleteJSON(resourceUrl, auth, roles);
+ }
+
+ public static void removeClientRoles(String rootUrl, String realm, String auth, String userid, String idOfClient, List<?> roles) {
+ String resourceUrl = composeResourceUrl(rootUrl, realm, "users/" + userid + "/role-mappings/clients/" + idOfClient);
+ doDeleteJSON(resourceUrl, auth, roles);
+ }
+
+ public static void resetUserPassword(String rootUrl, String realm, String auth, String userid, String password, boolean temporary) {
+
+ String resourceUrl = composeResourceUrl(rootUrl, realm, "users/" + userid + "/reset-password");
+
+ Headers headers = new Headers();
+ if (auth != null) {
+ headers.add("Authorization", auth);
+ }
+ headers.add("Content-Type", "application/json");
+
+ CredentialRepresentation credentials = new CredentialRepresentation();
+ credentials.setType("password");
+ credentials.setTemporary(temporary);
+ credentials.setValue(password);
+
+ HeadersBodyStatus response;
+
+ byte[] body;
+ try {
+ body = JsonSerialization.writeValueAsBytes(credentials);
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to serialize JSON", e);
+ }
+
+ try {
+ response = HttpUtil.doRequest("put", resourceUrl, new HeadersBody(headers, new ByteArrayInputStream(body)));
+ } catch (IOException e) {
+ throw new RuntimeException("HTTP request failed: PUT " + resourceUrl + "\n" + new String(body), e);
+ }
+
+ response.checkSuccess();
+ }
+
+ public static String getIdFromUsername(String rootUrl, String realm, String auth, String username) {
+ return getIdForType(rootUrl, realm, auth, "users", "username", username);
+ }
+}
diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/AccessibleBufferOutputStream.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/AccessibleBufferOutputStream.java
new file mode 100644
index 0000000..984784e
--- /dev/null
+++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/AccessibleBufferOutputStream.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.client.admin.cli.util;
+
+import java.io.FilterOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public class AccessibleBufferOutputStream extends FilterOutputStream{
+
+ private byte[] buf;
+
+ /**
+ * Creates an output stream filter built on top of the specified
+ * underlying output stream.
+ *
+ * @param out the underlying output stream to be assigned to
+ * the field <tt>this.out</tt> for later use, or
+ * <code>null</code> if this instance is to be
+ * created without an underlying stream.
+ */
+ public AccessibleBufferOutputStream(OutputStream out) {
+ super(out);
+ }
+
+ @Override
+ public void write(int b) throws IOException {
+ super.write(b);
+ buf = new byte[] {(byte) b};
+ }
+
+ @Override
+ public void write(byte[] b, int off, int len) throws IOException {
+ super.write(b, off, len);
+ buf = new byte[len];
+ System.arraycopy(b, off, buf, 0, len);
+ }
+
+ public byte[] getBuffer() {
+ return buf;
+ }
+
+ public int getLastByte() {
+ if (buf != null && buf.length > 0) {
+ return 0xFF & buf[buf.length-1];
+ }
+ return -1;
+ }
+}
diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/AttributeException.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/AttributeException.java
new file mode 100644
index 0000000..721baf2
--- /dev/null
+++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/AttributeException.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.client.admin.cli.util;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public class AttributeException extends RuntimeException {
+
+ private final String attrName;
+
+ public AttributeException(String attrName, String message) {
+ super(message);
+ this.attrName = attrName;
+ }
+
+ public AttributeException(String attrName, String message, Throwable th) {
+ super(message, th);
+ this.attrName = attrName;
+ }
+
+ public String getAttributeName() {
+ return attrName;
+ }
+}
diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/AuthUtil.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/AuthUtil.java
new file mode 100644
index 0000000..ddfca0c
--- /dev/null
+++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/AuthUtil.java
@@ -0,0 +1,202 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.client.admin.cli.util;
+
+import org.keycloak.client.admin.cli.config.ConfigData;
+import org.keycloak.client.admin.cli.config.RealmConfigData;
+import org.keycloak.common.util.KeystoreUtil;
+import org.keycloak.common.util.Time;
+import org.keycloak.jose.jws.JWSBuilder;
+import org.keycloak.representations.AccessTokenResponse;
+import org.keycloak.representations.JsonWebToken;
+import org.keycloak.util.BasicAuthHelper;
+import org.keycloak.util.JsonSerialization;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UnsupportedEncodingException;
+import java.security.KeyPair;
+import java.util.UUID;
+
+import static java.lang.System.currentTimeMillis;
+import static org.keycloak.client.admin.cli.util.ConfigUtil.checkAuthInfo;
+import static org.keycloak.client.admin.cli.util.ConfigUtil.saveMergeConfig;
+import static org.keycloak.client.admin.cli.util.HttpUtil.APPLICATION_FORM_URL_ENCODED;
+import static org.keycloak.client.admin.cli.util.HttpUtil.APPLICATION_JSON;
+import static org.keycloak.client.admin.cli.util.HttpUtil.doPost;
+import static org.keycloak.client.admin.cli.util.HttpUtil.urlencode;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public class AuthUtil {
+
+ public static String ensureToken(ConfigData config) {
+
+ checkAuthInfo(config);
+
+ RealmConfigData realmConfig = config.sessionRealmConfigData();
+
+ long now = currentTimeMillis();
+
+ // check expires of access_token against time
+ // if it's less than 5s to expiry, renew it
+ if (realmConfig.getExpiresAt() - now < 5000) {
+
+ // check refresh_token against expiry time
+ // if it's less than 5s to expiry, fail with credentials expired
+ if (realmConfig.getRefreshExpiresAt() - now < 5000) {
+ throw new RuntimeException("Session has expired. Login again with '" + OsUtil.CMD + " config credentials'");
+ }
+
+ if (realmConfig.getSigExpiresAt() != null && realmConfig.getSigExpiresAt() - now < 5000) {
+ throw new RuntimeException("Session has expired. Login again with '" + OsUtil.CMD + " config credentials'");
+ }
+
+ try {
+ String authorization = null;
+
+ StringBuilder body = new StringBuilder("grant_type=refresh_token")
+ .append("&refresh_token=").append(realmConfig.getRefreshToken())
+ .append("&client_id=").append(urlencode(realmConfig.getClientId()));
+
+ if (realmConfig.getSigningToken() != null) {
+ body.append("&client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer")
+ .append("&client_assertion=").append(realmConfig.getSigningToken());
+ } else if (realmConfig.getSecret() != null) {
+ authorization = BasicAuthHelper.createHeader(realmConfig.getClientId(), realmConfig.getSecret());
+ }
+
+ InputStream result = doPost(realmConfig.serverUrl() + "/realms/" + realmConfig.realm() + "/protocol/openid-connect/token",
+ APPLICATION_FORM_URL_ENCODED, APPLICATION_JSON, body.toString(), authorization);
+
+ AccessTokenResponse token = JsonSerialization.readValue(result, AccessTokenResponse.class);
+
+ saveMergeConfig(cfg -> {
+ RealmConfigData realmData = cfg.sessionRealmConfigData();
+ realmData.setToken(token.getToken());
+ realmData.setRefreshToken(token.getRefreshToken());
+ realmData.setExpiresAt(currentTimeMillis() + token.getExpiresIn() * 1000);
+ realmData.setRefreshExpiresAt(currentTimeMillis() + token.getRefreshExpiresIn() * 1000);
+ });
+ return token.getToken();
+
+ } catch (Exception e) {
+ throw new RuntimeException("Failed to refresh access token - " + e.getMessage(), e);
+ }
+ }
+
+ return realmConfig.getToken();
+ }
+
+ public static AccessTokenResponse getAuthTokens(String server, String realm, String user, String password, String clientId) {
+ StringBuilder body = new StringBuilder();
+ try {
+ body.append("grant_type=password")
+ .append("&username=").append(urlencode(user))
+ .append("&password=").append(urlencode(password))
+ .append("&client_id=").append(urlencode(clientId));
+
+ InputStream result = doPost(server + "/realms/" + realm + "/protocol/openid-connect/token",
+ APPLICATION_FORM_URL_ENCODED, APPLICATION_JSON, body.toString(), null);
+ return JsonSerialization.readValue(result, AccessTokenResponse.class);
+
+ } catch (UnsupportedEncodingException e) {
+ throw new RuntimeException("Unexpected error: ", e);
+ } catch (IOException e) {
+ throw new RuntimeException("Error receiving response: ", e);
+ }
+ }
+
+ public static AccessTokenResponse getAuthTokensByJWT(String server, String realm, String user, String password, String clientId, String signedRequestToken) {
+ StringBuilder body = new StringBuilder();
+ try {
+ body.append("client_id=").append(urlencode(clientId))
+ .append("&client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer")
+ .append("&client_assertion=").append(signedRequestToken);
+
+ if (user != null) {
+ if (password == null) {
+ throw new RuntimeException("No password specified");
+ }
+ body.append("&grant_type=password")
+ .append("&username=").append(urlencode(user))
+ .append("&password=").append(urlencode(password));
+ } else {
+ body.append("&grant_type=client_credentials");
+ }
+
+ InputStream result = doPost(server + "/realms/" + realm + "/protocol/openid-connect/token",
+ APPLICATION_FORM_URL_ENCODED, APPLICATION_JSON, body.toString(), null);
+ return JsonSerialization.readValue(result, AccessTokenResponse.class);
+
+ } catch (UnsupportedEncodingException e) {
+ throw new RuntimeException("Unexpected error: ", e);
+ } catch (IOException e) {
+ throw new RuntimeException("Error receiving response: ", e);
+ }
+ }
+
+ public static AccessTokenResponse getAuthTokensBySecret(String server, String realm, String user, String password, String clientId, String secret) {
+
+ StringBuilder body = new StringBuilder();
+ try {
+ if (user != null) {
+ if (password == null) {
+ throw new RuntimeException("No password specified");
+ }
+
+ body.append("client_id=").append(urlencode(clientId))
+ .append("&grant_type=password")
+ .append("&username=").append(urlencode(user))
+ .append("&password=").append(urlencode(password));
+ } else {
+ body.append("grant_type=client_credentials");
+ }
+
+ InputStream result = doPost(server + "/realms/" + realm + "/protocol/openid-connect/token",
+ APPLICATION_FORM_URL_ENCODED, APPLICATION_JSON, body.toString(), BasicAuthHelper.createHeader(clientId, secret));
+ return JsonSerialization.readValue(result, AccessTokenResponse.class);
+
+ } catch (UnsupportedEncodingException e) {
+ throw new RuntimeException("Unexpected error: ", e);
+ } catch (IOException e) {
+ throw new RuntimeException("Error receiving response: ", e);
+ }
+ }
+
+ public static String getSignedRequestToken(String keystore, String storePass, String keyPass, String alias, int sigLifetime, String clientId, String realmInfoUrl) {
+
+ KeyPair keypair = KeystoreUtil.loadKeyPairFromKeystore(keystore, storePass, keyPass, alias, KeystoreUtil.KeystoreFormat.JKS);
+
+ JsonWebToken reqToken = new JsonWebToken();
+ reqToken.id(UUID.randomUUID().toString());
+ reqToken.issuer(clientId);
+ reqToken.subject(clientId);
+ reqToken.audience(realmInfoUrl);
+
+ int now = Time.currentTime();
+ reqToken.issuedAt(now);
+ reqToken.expiration(now + sigLifetime);
+ reqToken.notBefore(now);
+
+ String signedRequestToken = new JWSBuilder()
+ .jsonContent(reqToken)
+ .rsa256(keypair.getPrivate());
+ return signedRequestToken;
+ }
+}
diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/ConfigUtil.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/ConfigUtil.java
new file mode 100644
index 0000000..3699048
--- /dev/null
+++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/ConfigUtil.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.client.admin.cli.util;
+
+import org.keycloak.client.admin.cli.config.ConfigData;
+import org.keycloak.client.admin.cli.config.ConfigHandler;
+import org.keycloak.client.admin.cli.config.ConfigUpdateOperation;
+import org.keycloak.client.admin.cli.config.InMemoryConfigHandler;
+import org.keycloak.client.admin.cli.config.RealmConfigData;
+import org.keycloak.representations.AccessTokenResponse;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public class ConfigUtil {
+
+ public static final String DEFAULT_CLIENT = "admin-cli";
+
+ public static final String DEFAULT_CONFIG_FILE_STRING = OsUtil.OS_ARCH.isWindows() ? "%HOMEDRIVE%%HOMEPATH%\\.keycloak\\kcadm.config" : "~/.keycloak/kcadm.config";
+
+ public static final String DEFAULT_CONFIG_FILE_PATH = System.getProperty("user.home") + "/.keycloak/kcadm.config";
+
+ private static ConfigHandler handler;
+
+ public static ConfigHandler getHandler() {
+ return handler;
+ }
+
+ public static void setHandler(ConfigHandler handler) {
+ ConfigUtil.handler = handler;
+ }
+
+ public static void saveTokens(AccessTokenResponse tokens, String endpoint, String realm, String clientId, String signKey, Long sigExpiresAt, String secret) {
+ handler.saveMergeConfig(config -> {
+ config.setServerUrl(endpoint);
+ config.setRealm(realm);
+
+ RealmConfigData realmConfig = config.ensureRealmConfigData(endpoint, realm);
+ realmConfig.setToken(tokens.getToken());
+ realmConfig.setRefreshToken(tokens.getRefreshToken());
+ realmConfig.setSigningToken(signKey);
+ realmConfig.setSecret(secret);
+ realmConfig.setExpiresAt(System.currentTimeMillis() + tokens.getExpiresIn() * 1000);
+ realmConfig.setRefreshExpiresAt(tokens.getRefreshExpiresIn() == 0 ?
+ Long.MAX_VALUE : System.currentTimeMillis() + tokens.getRefreshExpiresIn() * 1000);
+ realmConfig.setSigExpiresAt(sigExpiresAt);
+ realmConfig.setClientId(clientId);
+ });
+ }
+
+ public static void checkServerInfo(ConfigData config) {
+ if (config.getServerUrl() == null || config.getRealm() == null) {
+ throw new RuntimeException("No server or realm specified. Use --server, --realm, or '" + OsUtil.CMD + " config credentials'.");
+ }
+ }
+
+ public static void checkAuthInfo(ConfigData config) {
+ checkServerInfo(config);
+ }
+
+ public static boolean credentialsAvailable(ConfigData config) {
+ return config.getServerUrl() != null && config.getRealm() != null
+ && config.sessionRealmConfigData() != null && config.sessionRealmConfigData().getRefreshToken() != null;
+ }
+
+ public static ConfigData loadConfig() {
+ if (handler == null) {
+ throw new RuntimeException("No ConfigHandler set");
+ }
+
+ return handler.loadConfig();
+ }
+
+ public static void saveMergeConfig(ConfigUpdateOperation op) {
+ if (handler == null) {
+ throw new RuntimeException("No ConfigHandler set");
+ }
+
+ handler.saveMergeConfig(op);
+ }
+
+ public static void setupInMemoryHandler(ConfigData config) {
+ InMemoryConfigHandler memhandler = null;
+ if (handler instanceof InMemoryConfigHandler) {
+ memhandler = (InMemoryConfigHandler) handler;
+ } else {
+ memhandler = new InMemoryConfigHandler();
+ handler = memhandler;
+ }
+ memhandler.setConfigData(config);
+ }
+
+ public static String getEffectiveClientId(ConfigData config) {
+ String clientId = DEFAULT_CLIENT;
+
+ RealmConfigData realmData = config.sessionRealmConfigData();
+ if (realmData != null && realmData.getClientId() != null) {
+ clientId = realmData.getClientId();
+ }
+ return clientId;
+ }
+}
diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/FilterUtil.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/FilterUtil.java
new file mode 100644
index 0000000..6a68f85
--- /dev/null
+++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/FilterUtil.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.client.admin.cli.util;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+
+import java.io.IOException;
+import java.util.Iterator;
+
+import static org.keycloak.client.admin.cli.util.OutputUtil.MAPPER;
+import static org.keycloak.client.admin.cli.util.OutputUtil.convertToJsonNode;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public class FilterUtil {
+
+ public static JsonNode copyFilteredObject(Object object, ReturnFields returnFields) throws IOException {
+
+ JsonNode node = convertToJsonNode(object);
+
+ JsonNode r = node;
+ if (node.isArray()) {
+ ArrayNode ar = MAPPER.createArrayNode();
+ for (JsonNode item: node) {
+ ar.add(copyFilteredObject(item, returnFields));
+ }
+ r = ar;
+
+ } else if (node.isObject()){
+ r = MAPPER.createObjectNode();
+ Iterator<String> fieldNames = node.fieldNames();
+ while (fieldNames.hasNext()) {
+ String name = fieldNames.next();
+ if (returnFields.included(name)) {
+ JsonNode value = copyFilteredObject(node.get(name), returnFields.child(name));
+ ((ObjectNode) r).set(name, value);
+ }
+ }
+ }
+ return r;
+ }
+}
diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/Header.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/Header.java
new file mode 100644
index 0000000..6ba4a60
--- /dev/null
+++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/Header.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.client.admin.cli.util;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public class Header {
+
+ private String name;
+ private String value;
+
+ public Header(String key, String value) {
+ this.name = key;
+ this.value = value;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public String getValue() {
+ return value;
+ }
+}
\ No newline at end of file
diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/Headers.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/Headers.java
new file mode 100644
index 0000000..338971f
--- /dev/null
+++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/Headers.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.client.admin.cli.util;
+
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public class Headers implements Iterable<Header> {
+
+ private LinkedHashMap<String, Header> headers = new LinkedHashMap<>();
+
+ public void add(String header, String value) {
+ headers.put(header.toLowerCase(), new Header(header, value));
+ }
+
+ public boolean addIfMissing(String header, String value) {
+ String key = header.toLowerCase();
+ if (!headers.containsKey(key)) {
+ headers.put(key, new Header(header, value));
+ return true;
+ }
+ return false;
+ }
+
+ public boolean contains(String header) {
+ String key = header.toLowerCase();
+ return headers.containsKey(key);
+ }
+
+ public Header get(String header) {
+ return headers.get(header.toLowerCase());
+ }
+
+ @Override
+ public Iterator<Header> iterator() {
+ return headers.values().iterator();
+ }
+}
diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/HeadersBody.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/HeadersBody.java
new file mode 100644
index 0000000..c217fd2
--- /dev/null
+++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/HeadersBody.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.client.admin.cli.util;
+
+import java.io.ByteArrayOutputStream;
+import java.io.InputStream;
+import java.nio.charset.Charset;
+
+import static org.keycloak.client.admin.cli.util.IoUtil.copyStream;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public class HeadersBody {
+
+ private Headers headers;
+ private InputStream body;
+
+
+ public HeadersBody(Headers headers) {
+ this.headers = headers;
+ }
+
+ public HeadersBody(Headers headers, InputStream body) {
+ this.headers = headers;
+ this.body = body;
+ }
+
+ public Headers getHeaders() {
+ return headers;
+ }
+
+ public InputStream getBody() {
+ return body;
+ }
+
+ public String readBodyString() {
+ byte [] buffer = readBodyBytes();
+ return new String(buffer, Charset.forName(getContentCharset()));
+ }
+
+ public byte[] readBodyBytes() {
+ ByteArrayOutputStream os = new ByteArrayOutputStream();
+ copyStream(getBody(), os);
+ return os.toByteArray();
+ }
+
+ public String getContentCharset() {
+ Header contentType = headers.get("Content-Type");
+ if (contentType != null) {
+ int pos = contentType.getValue().lastIndexOf("charset=");
+ if (pos != -1) {
+ return contentType.getValue().substring(pos + 8);
+ }
+ }
+ return "iso-8859-1";
+ }
+}
diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/HeadersBodyStatus.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/HeadersBodyStatus.java
new file mode 100644
index 0000000..9ce8965
--- /dev/null
+++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/HeadersBodyStatus.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.client.admin.cli.util;
+
+import org.keycloak.util.JsonSerialization;
+
+import java.io.InputStream;
+import java.util.Map;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public class HeadersBodyStatus extends HeadersBody {
+
+ private final String status;
+
+ public HeadersBodyStatus(String status, Headers headers, InputStream body) {
+ super(headers, body);
+ this.status = status;
+ }
+
+ public String getStatus() {
+ return status;
+ }
+
+ private String getStatusCodeAndReason() {
+ return getStatus().substring(9);
+ }
+
+ public void checkSuccess() {
+ int code = getStatusCode();
+ if (code < 200 || code >= 300) {
+ String content = readBodyString();
+ Map<String, String> error = null;
+ try {
+ error = JsonSerialization.readValue(content, Map.class);
+ } catch (Exception ignored) {
+ }
+
+ String message = null;
+ if (error != null) {
+ String description = error.get("error_description");
+ String err = error.get("error");
+ String msg = error.get("errorMessage");
+ message = msg != null ? msg : err != null ? (description + " ["+ error.get("error") + "]") : null;
+ }
+ throw new HttpResponseException(getStatusCodeAndReason(), message, new RuntimeException(content));
+ }
+ }
+
+ public int getStatusCode() {
+ return Integer.valueOf(status.split(" ")[1]);
+ }
+}
diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/HttpResponseException.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/HttpResponseException.java
new file mode 100644
index 0000000..692b384
--- /dev/null
+++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/HttpResponseException.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.client.admin.cli.util;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public class HttpResponseException extends RuntimeException {
+
+ private String status;
+
+ HttpResponseException(String status, String message, Throwable cause) {
+ super(message != null ? message : "HTTP error - " + status, cause);
+ this.status = status;
+ }
+
+ public int getStatusCode() {
+ return Integer.valueOf(status.split(" ")[0]);
+ }
+}
diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/HttpUtil.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/HttpUtil.java
new file mode 100644
index 0000000..f1261ab
--- /dev/null
+++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/HttpUtil.java
@@ -0,0 +1,450 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.client.admin.cli.util;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import org.apache.http.HeaderIterator;
+import org.apache.http.HttpHeaders;
+import org.apache.http.HttpResponse;
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.methods.HttpEntityEnclosingRequestBase;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.client.methods.HttpHead;
+import org.apache.http.client.methods.HttpOptions;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.client.methods.HttpPut;
+import org.apache.http.client.methods.HttpRequestBase;
+import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
+import org.apache.http.entity.InputStreamEntity;
+import org.apache.http.entity.StringEntity;
+import org.apache.http.impl.client.HttpClientBuilder;
+import org.apache.http.ssl.SSLContexts;
+import org.keycloak.client.admin.cli.httpcomponents.HttpDelete;
+import org.keycloak.client.admin.cli.operations.LocalSearch;
+import org.keycloak.client.admin.cli.operations.RoleOperations;
+import org.keycloak.util.JsonSerialization;
+
+import javax.net.ssl.SSLContext;
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+import java.security.KeyManagementException;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.cert.CertificateException;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import static org.keycloak.common.util.ObjectUtil.capitalize;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public class HttpUtil {
+
+ public static final String APPLICATION_XML = "application/xml";
+ public static final String APPLICATION_JSON = "application/json";
+ public static final String APPLICATION_FORM_URL_ENCODED = "application/x-www-form-urlencoded";
+ public static final String UTF_8 = "utf-8";
+
+ private static HttpClient httpClient;
+ private static SSLConnectionSocketFactory sslsf;
+
+ public static InputStream doGet(String url, String acceptType, String authorization) {
+ try {
+ HttpGet request = new HttpGet(url);
+ request.setHeader(HttpHeaders.ACCEPT, acceptType);
+ return doRequest(authorization, request);
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to send request - " + e.getMessage(), e);
+ }
+ }
+
+ public static InputStream doPost(String url, String contentType, String acceptType, String content, String authorization) {
+ try {
+ return doPostOrPut(contentType, acceptType, content, authorization, new HttpPost(url));
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to send request - " + e.getMessage(), e);
+ }
+ }
+
+ public static InputStream doPut(String url, String contentType, String acceptType, String content, String authorization) {
+ try {
+ return doPostOrPut(contentType, acceptType, content, authorization, new HttpPut(url));
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to send request - " + e.getMessage(), e);
+ }
+ }
+
+ public static void doDelete(String url, String authorization) {
+ try {
+ HttpDelete request = new HttpDelete(url);
+ doRequest(authorization, request);
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to send request - " + e.getMessage(), e);
+ }
+ }
+
+
+ public static HeadersBodyStatus doGet(String url, HeadersBody request) throws IOException {
+ return doRequest("get", url, request);
+ }
+
+ public static HeadersBodyStatus doPost(String url, HeadersBody request) throws IOException {
+ return doRequest("post", url, request);
+ }
+
+ public static HeadersBodyStatus doPut(String url, HeadersBody request) throws IOException {
+ return doRequest("put", url, request);
+ }
+
+ public static HeadersBodyStatus doDelete(String url, HeadersBody request) throws IOException {
+ return doRequest("delete", url, request);
+ }
+
+ public static HeadersBodyStatus doRequest(String type, String url, HeadersBody request) throws IOException {
+ HttpRequestBase req;
+ switch (type) {
+ case "get":
+ req = new HttpGet(url);
+ break;
+ case "post":
+ req = new HttpPost(url);
+ break;
+ case "put":
+ req = new HttpPut(url);
+ break;
+ case "delete":
+ req = new HttpDelete(url);
+ break;
+ case "options":
+ req = new HttpOptions(url);
+ break;
+ case "head":
+ req = new HttpHead(url);
+ break;
+ default:
+ throw new RuntimeException("Method not supported: " + type);
+ }
+ addHeaders(req, request.getHeaders());
+
+ if (request.getBody() != null) {
+ if (req instanceof HttpEntityEnclosingRequestBase == false) {
+ throw new RuntimeException("Request type does not support body: " + type);
+ }
+ ((HttpEntityEnclosingRequestBase) req).setEntity(new InputStreamEntity(request.getBody()));
+ }
+
+ HttpResponse res = getHttpClient().execute(req);
+ InputStream responseStream = null;
+ if (res.getEntity() != null) {
+ responseStream = res.getEntity().getContent();
+ } else {
+ responseStream = new InputStream() {
+ @Override
+ public int read () throws IOException {
+ return -1;
+ }
+ };
+ }
+
+ Headers headers = new Headers();
+ HeaderIterator it = res.headerIterator();
+ while (it.hasNext()) {
+ org.apache.http.Header header = it.nextHeader();
+ headers.add(header.getName(), header.getValue());
+ }
+
+ return new HeadersBodyStatus(res.getStatusLine().toString(), headers, responseStream);
+ }
+
+ private static void addHeaders(HttpRequestBase request, Headers headers) {
+ for (Header header: headers) {
+ request.setHeader(header.getName(), header.getValue());
+ }
+ }
+
+ private static InputStream doPostOrPut(String contentType, String acceptType, String content, String authorization, HttpEntityEnclosingRequestBase request) throws IOException {
+ request.setHeader(HttpHeaders.CONTENT_TYPE, contentType);
+ request.setHeader(HttpHeaders.ACCEPT, acceptType);
+ if (content != null) {
+ request.setEntity(new StringEntity(content));
+ }
+
+ return doRequest(authorization, request);
+ }
+
+ private static InputStream doRequest(String authorization, HttpRequestBase request) throws IOException {
+ addAuth(request, authorization);
+
+ HttpResponse response = getHttpClient().execute(request);
+ InputStream responseStream = null;
+ if (response.getEntity() != null) {
+ responseStream = response.getEntity().getContent();
+ }
+
+ int code = response.getStatusLine().getStatusCode();
+ if (code >= 200 && code < 300) {
+ return responseStream;
+ } else {
+ Map<String, String> error = null;
+ try {
+ org.apache.http.Header header = response.getEntity().getContentType();
+ if (header != null && APPLICATION_JSON.equals(header.getValue())) {
+ error = JsonSerialization.readValue(responseStream, Map.class);
+ }
+ } catch (Exception e) {
+ throw new RuntimeException("Failed to read error response - " + e.getMessage(), e);
+ } finally {
+ responseStream.close();
+ }
+
+ String message = null;
+ if (error != null) {
+ message = error.get("error_description") + " [" + error.get("error") + "]";
+ }
+ throw new RuntimeException(message != null ? message : response.getStatusLine().getStatusCode() + " " + response.getStatusLine().getReasonPhrase());
+ }
+ }
+
+ private static void addAuth(HttpRequestBase request, String authorization) {
+ if (authorization != null) {
+ request.setHeader(HttpHeaders.AUTHORIZATION, authorization);
+ }
+ }
+
+ public static HttpClient getHttpClient() {
+ if (httpClient == null) {
+ if (sslsf != null) {
+ httpClient = HttpClientBuilder.create().useSystemProperties().setSSLSocketFactory(sslsf).build();
+ } else {
+ httpClient = HttpClientBuilder.create().useSystemProperties().build();
+ }
+ }
+ return httpClient;
+ }
+
+ public static String urlencode(String value) {
+ try {
+ return URLEncoder.encode(value, UTF_8);
+ } catch (UnsupportedEncodingException e) {
+ throw new RuntimeException("Failed to urlencode", e);
+ }
+ }
+
+ public static void setTruststore(File file, String password) throws CertificateException, NoSuchAlgorithmException, KeyStoreException, IOException, KeyManagementException {
+ if (!file.isFile()) {
+ throw new RuntimeException("Truststore file not found: " + file.getAbsolutePath());
+ }
+ SSLContext theContext = SSLContexts.custom()
+ .useProtocol("TLS")
+ .loadTrustMaterial(file, password == null ? null : password.toCharArray())
+ .build();
+ sslsf = new SSLConnectionSocketFactory(theContext);
+ }
+
+ public static String extractIdFromLocation(String location) {
+ int last = location.lastIndexOf("/");
+ if (last != -1) {
+ return location.substring(last + 1);
+ }
+ return null;
+ }
+
+ public static String addQueryParamsToUri(String uri, String ... queryParams) {
+ if (queryParams == null) {
+ return uri;
+ }
+
+ if (queryParams.length % 2 != 0) {
+ throw new RuntimeException("Value missing for query parameter: " + queryParams[queryParams.length-1]);
+ }
+
+ Map<String, String> params = new LinkedHashMap<>();
+ for (int i = 0; i < queryParams.length; i += 2) {
+ params.put(queryParams[i], queryParams[i+1]);
+ }
+ return addQueryParamsToUri(uri, params);
+ }
+
+ public static String addQueryParamsToUri(String uri, Map<String, String> queryParams) {
+
+ if (queryParams.size() == 0) {
+ return uri;
+ }
+
+ StringBuilder query = new StringBuilder();
+ for (Map.Entry<String, String> params: queryParams.entrySet()) {
+ try {
+ if (query.length() > 0) {
+ query.append("&");
+ }
+ query.append(params.getKey()).append("=").append(URLEncoder.encode(params.getValue(), "utf-8"));
+ } catch (Exception e) {
+ throw new RuntimeException("Failed to encode query params: " + params.getKey() + "=" + params.getValue());
+ }
+ }
+
+ return uri + (uri.indexOf("?") == -1 ? "?" : "&") + query;
+ }
+
+ public static String composeResourceUrl(String adminRoot, String realm, String uri) {
+ if (!uri.startsWith("http:") && !uri.startsWith("https:")) {
+ if ("realms".equals(uri) || uri.startsWith("realms/")) {
+ uri = normalize(adminRoot) + uri;
+ } else if ("serverinfo".equals(uri)) {
+ uri = normalize(adminRoot) + uri;
+ } else {
+ uri = normalize(adminRoot) + "realms/" + realm + "/" + uri;
+ }
+ }
+ return uri;
+ }
+
+ public static String normalize(String value) {
+ return value.endsWith("/") ? value : value + "/";
+ }
+
+ public static void checkSuccess(String url, HeadersBodyStatus response) {
+ try {
+ response.checkSuccess();
+ } catch (HttpResponseException e) {
+ if (e.getStatusCode() == 404) {
+ throw new RuntimeException("Resource not found for url: " + url, e);
+ }
+ throw e;
+ }
+ }
+
+ public static <T> T doGetJSON(Class<T> type, String resourceUrl, String auth) {
+
+ Headers headers = new Headers();
+ if (auth != null) {
+ headers.add("Authorization", auth);
+ }
+ headers.add("Accept", "application/json");
+
+ HeadersBodyStatus response;
+ try {
+ response = HttpUtil.doRequest("get", resourceUrl, new HeadersBody(headers));
+ } catch (IOException e) {
+ throw new RuntimeException("HTTP request failed: GET " + resourceUrl, e);
+ }
+
+ checkSuccess(resourceUrl, response);
+
+ T result;
+ try {
+ result = JsonSerialization.readValue(response.getBody(), type);
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to read JSON response", e);
+ }
+
+ return result;
+ }
+
+ public static void doPostJSON(String resourceUrl, String auth, Object content) {
+ Headers headers = new Headers();
+ if (auth != null) {
+ headers.add("Authorization", auth);
+ }
+ headers.add("Content-Type", "application/json");
+
+ HeadersBodyStatus response;
+
+ byte[] body;
+ try {
+ body = JsonSerialization.writeValueAsBytes(content);
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to serialize JSON", e);
+ }
+
+ try {
+ response = HttpUtil.doRequest("post", resourceUrl, new HeadersBody(headers, new ByteArrayInputStream(body)));
+ } catch (IOException e) {
+ throw new RuntimeException("HTTP request failed: POST " + resourceUrl + "\n" + new String(body), e);
+ }
+
+ checkSuccess(resourceUrl, response);
+ }
+
+ public static void doDeleteJSON(String resourceUrl, String auth, Object content) {
+ Headers headers = new Headers();
+ if (auth != null) {
+ headers.add("Authorization", auth);
+ }
+ headers.add("Content-Type", "application/json");
+
+ HeadersBodyStatus response;
+
+ byte[] body;
+ try {
+ body = JsonSerialization.writeValueAsBytes(content);
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to serialize JSON", e);
+ }
+
+ try {
+ response = HttpUtil.doRequest("delete", resourceUrl, new HeadersBody(headers, new ByteArrayInputStream(body)));
+ } catch (IOException e) {
+ throw new RuntimeException("HTTP request failed: DELETE " + resourceUrl + "\n" + new String(body), e);
+ }
+
+ checkSuccess(resourceUrl, response);
+ }
+
+ public static String getIdForType(String rootUrl, String realm, String auth, String resourceEndpoint, String attrName, String attrValue) {
+
+ return getAttrForType(rootUrl, realm, auth, resourceEndpoint, attrName, attrValue, "id");
+ }
+
+ public static String getAttrForType(String rootUrl, String realm, String auth, String resourceEndpoint, String attrName, String attrValue, String returnAttrName) {
+
+ String resourceUrl = composeResourceUrl(rootUrl, realm, resourceEndpoint);
+ resourceUrl = HttpUtil.addQueryParamsToUri(resourceUrl, attrName, attrValue, "first", "0", "max", "2");
+
+ List<ObjectNode> users = doGetJSON(RoleOperations.LIST_OF_NODES.class, resourceUrl, auth);
+
+ ObjectNode user;
+ try {
+ user = new LocalSearch(users).exactMatchOne(attrValue, attrName);
+ } catch (Exception e) {
+ throw new RuntimeException("Multiple " + resourceEndpoint + " found for " + attrName + ": " + attrValue, e);
+ }
+
+ String typeName = singularize(resourceEndpoint);
+ if (user == null) {
+ throw new RuntimeException(capitalize(typeName) + " not found for " + attrName + ": " + attrValue);
+ }
+
+ JsonNode attr = user.get(returnAttrName);
+ if (attr == null) {
+ throw new RuntimeException("Returned " + typeName + " info has no '" + returnAttrName + "' attribute");
+ }
+ return attr.asText();
+ }
+
+
+ public static String singularize(String value) {
+ return value.substring(0, value.length()-1);
+ }
+}
diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/IoUtil.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/IoUtil.java
new file mode 100644
index 0000000..f0c50d3
--- /dev/null
+++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/IoUtil.java
@@ -0,0 +1,255 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.client.admin.cli.util;
+
+import org.jboss.aesh.console.AeshConsoleBufferBuilder;
+import org.jboss.aesh.console.AeshInputProcessorBuilder;
+import org.jboss.aesh.console.ConsoleBuffer;
+import org.jboss.aesh.console.InputProcessor;
+import org.jboss.aesh.console.Prompt;
+import org.jboss.aesh.console.command.invocation.CommandInvocation;
+import org.keycloak.client.admin.cli.aesh.Globals;
+
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.charset.Charset;
+import java.nio.file.FileSystem;
+import java.nio.file.FileSystems;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.attribute.AclEntry;
+import java.nio.file.attribute.AclEntryPermission;
+import java.nio.file.attribute.AclEntryType;
+import java.nio.file.attribute.AclFileAttributeView;
+import java.nio.file.attribute.PosixFilePermission;
+import java.nio.file.attribute.UserPrincipal;
+import java.util.Formatter;
+import java.util.HashSet;
+import java.util.List;
+import java.util.ListIterator;
+import java.util.Set;
+
+import static java.nio.file.Files.createDirectories;
+import static java.nio.file.Files.createFile;
+import static java.nio.file.Files.isDirectory;
+import static java.nio.file.Files.isRegularFile;
+import static org.keycloak.client.admin.cli.util.OsUtil.OS_ARCH;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public class IoUtil {
+
+ public static String readFileOrStdin(String file) {
+ String content;
+ if ("-".equals(file)) {
+ content = readFully(System.in);
+ } else {
+ try (InputStream is = new FileInputStream(file)) {
+ content = readFully(is);
+ } catch (FileNotFoundException e) {
+ throw new RuntimeException("File not found: " + file);
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to read file: " + file, e);
+ }
+ }
+ return content;
+ }
+
+ public static void waitFor(long millis) {
+ try {
+ Thread.sleep(millis);
+ } catch (InterruptedException e) {
+ throw new RuntimeException("Interrupted");
+ }
+ }
+
+ public static String readSecret(String prompt, CommandInvocation invocation) {
+
+ // TODO Windows hack - masking not working on Windows
+ char maskChar = OS_ARCH.isWindows() ? 0 : '*';
+ ConsoleBuffer consoleBuffer = new AeshConsoleBufferBuilder()
+ .shell(invocation.getShell())
+ .prompt(new Prompt(prompt, maskChar))
+ .create();
+ InputProcessor inputProcessor = new AeshInputProcessorBuilder()
+ .consoleBuffer(consoleBuffer)
+ .create();
+
+ consoleBuffer.displayPrompt();
+
+ // activate stdin
+ Globals.stdin.setInputStream(System.in);
+
+ String result;
+ try {
+ do {
+ result = inputProcessor.parseOperation(invocation.getInput());
+ } while (result == null);
+ } catch (Exception e) {
+ throw new RuntimeException("^C", e);
+ }
+ /*
+ if (!Globals.stdin.isStdinAvailable()) {
+ try {
+ return readLine(new InputStreamReader(System.in));
+ } catch (IOException e) {
+ throw new RuntimeException("Standard input not available");
+ }
+ }
+ */
+ // Windows hack - get rid of any \n
+ result = result.replaceAll("\\n", "");
+ return result;
+ }
+
+ public static String readFully(InputStream is) {
+ Charset charset = Charset.forName("utf-8");
+ StringBuilder out = new StringBuilder();
+ byte [] buf = new byte[8192];
+
+ int rc;
+ try {
+ while ((rc = is.read(buf)) != -1) {
+ out.append(new String(buf, 0, rc, charset));
+ }
+ } catch (Exception e) {
+ throw new RuntimeException("Failed to read stream", e);
+ }
+ return out.toString();
+ }
+
+ public static void copyStream(InputStream is, OutputStream os) {
+
+ byte [] buf = new byte[8192];
+
+ int rc;
+ try (InputStream input = is) {
+ while ((rc = input.read(buf)) != -1) {
+ os.write(buf, 0, rc);
+ }
+ } catch (Exception e) {
+ throw new RuntimeException("Failed to read/write a stream: ", e);
+ } finally {
+ try {
+ os.flush();
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to write a stream: ", e);
+ }
+ }
+ }
+
+ public static void ensureFile(Path path) throws IOException {
+
+ FileSystem fs = FileSystems.getDefault();
+ Set<String> supportedViews = fs.supportedFileAttributeViews();
+ Path parent = path.getParent();
+
+ if (!isDirectory(parent)) {
+ createDirectories(parent);
+ // make sure only owner can read/write it
+ if (supportedViews.contains("posix")) {
+ setUnixPermissions(parent);
+ } else if (supportedViews.contains("acl")) {
+ setWindowsPermissions(parent);
+ } else {
+ warnErr("Failed to restrict access permissions on .keycloak directory: " + parent);
+ }
+ }
+ if (!isRegularFile(path)) {
+ createFile(path);
+ // make sure only owner can read/write it
+ if (FileSystems.getDefault().supportedFileAttributeViews().contains("posix")) {
+ setUnixPermissions(path);
+ } else if (supportedViews.contains("acl")) {
+ setWindowsPermissions(path);
+ } else {
+ warnErr("Failed to restrict access permissions on config file: " + path);
+ }
+ }
+ }
+
+ private static void setUnixPermissions(Path path) throws IOException {
+ Set<PosixFilePermission> perms = new HashSet<>();
+ perms.add(PosixFilePermission.OWNER_READ);
+ perms.add(PosixFilePermission.OWNER_WRITE);
+ if (isDirectory(path)) {
+ perms.add(PosixFilePermission.OWNER_EXECUTE);
+ }
+ Files.setPosixFilePermissions(path, perms);
+ }
+
+ private static void setWindowsPermissions(Path path) throws IOException {
+ AclFileAttributeView view = Files.getFileAttributeView(path, AclFileAttributeView.class);
+ UserPrincipal owner = view.getOwner();
+ List<AclEntry> acl = view.getAcl();
+ ListIterator<AclEntry> it = acl.listIterator();
+ while (it.hasNext()) {
+ AclEntry entry = it.next();
+ if ("BUILTIN\\Administrators".equals(entry.principal().getName()) || "NT AUTHORITY\\SYSTEM".equals(entry.principal().getName())) {
+ continue;
+ }
+ it.remove();
+ }
+ AclEntry entry = AclEntry.newBuilder()
+ .setType(AclEntryType.ALLOW)
+ .setPrincipal(owner)
+ .setPermissions(AclEntryPermission.READ_DATA, AclEntryPermission.WRITE_DATA,
+ AclEntryPermission.APPEND_DATA, AclEntryPermission.READ_NAMED_ATTRS,
+ AclEntryPermission.WRITE_NAMED_ATTRS, AclEntryPermission.EXECUTE,
+ AclEntryPermission.READ_ATTRIBUTES, AclEntryPermission.WRITE_ATTRIBUTES,
+ AclEntryPermission.DELETE, AclEntryPermission.READ_ACL, AclEntryPermission.SYNCHRONIZE)
+ .build();
+ acl.add(entry);
+ view.setAcl(acl);
+ }
+
+ public static void printOut(String msg) {
+ System.out.println(msg);
+ }
+
+ public static void printErr(String msg) {
+ System.err.println(msg);
+ }
+
+ public static void printfOut(String format, String ... params) {
+ System.out.println(new Formatter().format("WARN: " + format, params));
+ }
+
+ public static void warnOut(String msg) {
+ System.out.println("WARN: " + msg);
+ }
+
+ public static void warnErr(String msg) {
+ System.err.println("WARN: " + msg);
+ }
+
+ public static void warnfOut(String format, String ... params) {
+ System.out.println(new Formatter().format("WARN: " + format, params));
+ }
+
+ public static void warnfErr(String format, String ... params) {
+ System.err.println(new Formatter().format("WARN: " + format, params));
+ }
+
+ public static void logOut(String msg) {
+ System.out.println("LOG: " + msg);
+ }
+}
diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/OsArch.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/OsArch.java
new file mode 100644
index 0000000..13cf424
--- /dev/null
+++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/OsArch.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.client.admin.cli.util;
+
+/**
+ * @author <a href="mailto:marko.strukelj@gmail.com">Marko Strukelj</a>
+ */
+public class OsArch {
+
+ private String os;
+ private String arch;
+ private boolean legacy;
+
+ public OsArch(String os, String arch) {
+ this(os, arch, false);
+ }
+
+ public OsArch(String os, String arch, boolean legacy) {
+ this.os = os;
+ this.arch = arch;
+ this.legacy = legacy;
+ }
+
+ public String os() {
+ return os;
+ }
+
+ public String arch() {
+ return arch;
+ }
+
+ public boolean isLegacy() {
+ return legacy;
+ }
+
+ public boolean isWindows() {
+ return "win32".equals(os);
+ }
+
+ public String envVar(String var) {
+ if (isWindows()) {
+ return "%" + var + "%";
+ } else {
+ return "$" + var;
+ }
+ }
+
+ public String path(String path) {
+ if (isWindows()) {
+ path = path.replaceAll("/", "\\\\");
+ if (path.startsWith("~")) {
+ path = "%HOMEPATH%" + path.substring(1);
+ }
+ }
+ return path;
+ }
+}
\ No newline at end of file
diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/OsUtil.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/OsUtil.java
new file mode 100644
index 0000000..d7a3840
--- /dev/null
+++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/OsUtil.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.client.admin.cli.util;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public class OsUtil {
+
+ public static final OsArch OS_ARCH = determineOSAndArch();
+ // TODO: move CMD out of this class
+ public static final String CMD = OS_ARCH.isWindows() ? "kcadm.bat" : "kcadm.sh";
+
+ public static final String PROMPT = OS_ARCH.isWindows() ? "c:\\>" : "$";
+
+ public static final String EOL = OS_ARCH.isWindows() ? "\r\n" : "\n";
+
+
+ public static OsArch determineOSAndArch() {
+ String os = System.getProperty("os.name").toLowerCase();
+ String arch = System.getProperty("os.arch");
+
+ if (arch.equals("amd64")) {
+ arch = "x86_64";
+ }
+
+ if (os.startsWith("linux")) {
+ if (arch.equals("x86") || arch.equals("i386") || arch.equals("i586")) {
+ arch = "i686";
+ }
+ return new OsArch("linux", arch);
+ } else if (os.startsWith("windows")) {
+ if (arch.equals("x86")) {
+ arch = "i386";
+ }
+ if (os.indexOf("2008") != -1 || os.indexOf("2003") != -1 || os.indexOf("vista") != -1) {
+ return new OsArch("win32", arch, true);
+ } else {
+ return new OsArch("win32", arch);
+ }
+ } else if (os.startsWith("sunos")) {
+ return new OsArch("sunos5", "x86_64");
+ } else if (os.startsWith("mac os x")) {
+ return new OsArch("osx", "x86_64");
+ }
+
+ // unsupported platform
+ throw new RuntimeException("Could not determine OS and architecture for this operating system: " + os);
+ }
+}
diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/OutputFormat.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/OutputFormat.java
new file mode 100644
index 0000000..31181cc
--- /dev/null
+++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/OutputFormat.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.client.admin.cli.util;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public enum OutputFormat {
+ JSON,
+ CSV
+}
diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/OutputUtil.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/OutputUtil.java
new file mode 100644
index 0000000..a9931d7
--- /dev/null
+++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/OutputUtil.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.client.admin.cli.util;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationFeature;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.TextNode;
+import org.keycloak.util.JsonSerialization;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.util.Iterator;
+import java.util.Map;
+
+import static org.keycloak.client.admin.cli.util.IoUtil.printOut;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public class OutputUtil {
+
+ public static ObjectMapper MAPPER = new ObjectMapper();
+
+ static {
+ MAPPER.enable(SerializationFeature.INDENT_OUTPUT);
+ MAPPER.setSerializationInclusion(JsonInclude.Include.NON_NULL);
+ }
+
+ public static JsonNode convertToJsonNode(Object object) throws IOException {
+ if (object instanceof JsonNode) {
+ return (JsonNode) object;
+ }
+
+ ByteArrayOutputStream buffer = new ByteArrayOutputStream();
+ buffer.write(JsonSerialization.writeValueAsBytes(object));
+ return MAPPER.readValue(buffer.toByteArray(), JsonNode.class);
+ }
+
+
+ public static void printAsCsv(Object object, ReturnFields fields, boolean unquoted) throws IOException {
+
+ JsonNode node = convertToJsonNode(object);
+ if (!node.isArray()) {
+ ArrayNode listNode = MAPPER.createArrayNode();
+ listNode.add(node);
+ node = listNode;
+ }
+
+ for (JsonNode item: node) {
+ StringBuilder buffer = new StringBuilder();
+ printObjectAsCsv(buffer, item, fields, unquoted);
+
+ printOut(buffer.length() > 0 ? buffer.substring(1) : "");
+ }
+ }
+
+ static void printObjectAsCsv(StringBuilder out, JsonNode node, boolean unquoted) {
+ printObjectAsCsv(out, node, null, unquoted);
+ }
+
+ static void printObjectAsCsv(StringBuilder out, JsonNode node, ReturnFields fields, boolean unquoted) {
+
+ if (node.isObject()) {
+ if (fields == null) {
+ Iterator<Map.Entry<String, JsonNode>> it = node.fields();
+ while (it.hasNext()) {
+ printObjectAsCsv(out, it.next().getValue(), unquoted);
+ }
+ } else {
+ Iterator<String> it = fields.iterator();
+ while (it.hasNext()) {
+ String field = it.next();
+ JsonNode attr = node.get(field);
+ printObjectAsCsv(out, attr, fields.child(field), unquoted);
+ }
+ }
+ } else if (node.isArray()) {
+ for (JsonNode item: node) {
+ printObjectAsCsv(out, item, fields, unquoted);
+ }
+ } else if (node != null) {
+ out.append(",");
+ if (unquoted && node instanceof TextNode) {
+ out.append(node.asText());
+ } else {
+ out.append(node.toString());
+ }
+ }
+ }
+}
diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/ParseUtil.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/ParseUtil.java
new file mode 100644
index 0000000..e1b2b9a
--- /dev/null
+++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/ParseUtil.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.client.admin.cli.util;
+
+import com.fasterxml.jackson.core.JsonParseException;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import org.keycloak.client.admin.cli.common.AttributeOperation;
+import org.keycloak.client.admin.cli.common.CmdStdinContext;
+import org.keycloak.util.JsonSerialization;
+
+import java.io.IOException;
+import java.util.List;
+
+import static org.keycloak.client.admin.cli.util.IoUtil.readFileOrStdin;
+import static org.keycloak.client.admin.cli.util.ReflectionUtil.setAttributes;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public class ParseUtil {
+
+ public static String[] parseKeyVal(String keyval) {
+ // we expect = as a separator
+ int pos = keyval.indexOf("=");
+ if (pos <= 0) {
+ throw new RuntimeException("Invalid key=value parameter: [" + keyval + "]");
+ }
+
+ String [] parsed = new String[2];
+ parsed[0] = keyval.substring(0, pos);
+ parsed[1] = keyval.substring(pos+1);
+
+ return parsed;
+ }
+
+ public static CmdStdinContext<JsonNode> parseFileOrStdin(String file) {
+
+ String content = readFileOrStdin(file).trim();
+ JsonNode result = null;
+
+ if (content.length() == 0) {
+ throw new RuntimeException("Document provided by --file option is empty");
+ }
+
+ try {
+ result = JsonSerialization.readValue(content, JsonNode.class);
+ } catch (JsonParseException e) {
+ throw new RuntimeException("Not a valid JSON document - " + e.getMessage(), e);
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to read the input document as JSON: " + e.getMessage(), e);
+ } catch (Exception e) {
+ throw new RuntimeException("Not a valid JSON document", e);
+ }
+
+ CmdStdinContext<JsonNode> ctx = new CmdStdinContext<>();
+ ctx.setContent(content);
+ ctx.setResult(result);
+ return ctx;
+ }
+
+ public static <T> CmdStdinContext<JsonNode> mergeAttributes(CmdStdinContext<JsonNode> ctx, ObjectNode newObject, List<AttributeOperation> attrs) {
+ String content = ctx.getContent();
+ JsonNode node = ctx.getResult();
+ if (node != null && !node.isObject()) {
+ throw new RuntimeException("Not a JSON object: " + node);
+ }
+ ObjectNode result = (ObjectNode) node;
+ try {
+
+ if (result == null) {
+ try {
+ result = newObject;
+ } catch (Throwable e) {
+ throw new RuntimeException("Failed to instantiate object: " + e.getMessage(), e);
+ }
+ }
+
+ if (result != null) {
+ try {
+ setAttributes(result, attrs);
+ } catch (AttributeException e) {
+ throw new RuntimeException("Failed to set attribute '" + e.getAttributeName() + "' on document type '" + result.getClass().getName() + "'", e);
+ }
+ content = JsonSerialization.writeValueAsString(result);
+ } else {
+ throw new RuntimeException("Setting attributes is not supported for type: " + result.getClass().getName());
+ }
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to merge set attributes with configuration from file", e);
+ }
+
+ ctx.setContent(content);
+ ctx.setResult(result);
+ return ctx;
+ }
+}
diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/ReflectionUtil.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/ReflectionUtil.java
new file mode 100644
index 0000000..81c2d29
--- /dev/null
+++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/ReflectionUtil.java
@@ -0,0 +1,228 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.client.admin.cli.util;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.BooleanNode;
+import com.fasterxml.jackson.databind.node.DoubleNode;
+import com.fasterxml.jackson.databind.node.LongNode;
+import com.fasterxml.jackson.databind.node.NullNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.fasterxml.jackson.databind.node.TextNode;
+import org.keycloak.client.admin.cli.common.AttributeKey;
+import org.keycloak.client.admin.cli.common.AttributeOperation;
+
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+import static org.keycloak.client.admin.cli.common.AttributeOperation.Type.SET;
+import static org.keycloak.client.admin.cli.util.OutputUtil.MAPPER;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public class ReflectionUtil {
+
+ public static void setAttributes(JsonNode client, List<AttributeOperation> attrs) {
+ for (AttributeOperation item: attrs) {
+ AttributeKey attr = item.getKey();
+ JsonNode nested = client;
+
+ List<AttributeKey.Component> cs = attr.getComponents();
+ for (int i = 0; i < cs.size(); i++) {
+ AttributeKey.Component c = cs.get(i);
+
+ // if this is the last component of the name,
+ // then if SET we need to set value on nested:
+ // if value already set on nested, then overwrite, maybe remove node + add new node
+ // if DELETE we need to remove or nullify value (if isArray)
+ // else get child and
+ // if exist set nested to child
+ // else if SET create new empty object or array - depending on c.isArray()
+ //
+
+ // if this is the last component of the name
+ if (i == cs.size() - 1) {
+ String val = item.getValue();
+ ObjectNode obj = (ObjectNode) nested;
+
+ if (SET == item.getType()) {
+ JsonNode valNode = valueToJsonNode(val);
+ if (c.isArray() || attr.isAppend()) {
+ JsonNode list = obj.get(c.getName());
+ // child expected to be an array
+ if ( ! (list instanceof ArrayNode)) {
+ // replace with new array
+ list = MAPPER.createArrayNode();
+ obj.set(c.getName(), list);
+ }
+ setArrayItem((ArrayNode) list, c.getIndex(), valNode);
+ } else {
+ ((ObjectNode) nested).set(c.getName(), valNode);
+ }
+ } else {
+ // type == DELETE
+ if (c.isArray()) {
+ JsonNode list = obj.get(c.getName());
+ // child expected to be an array
+ if (list instanceof ArrayNode) {
+ removeArrayItem((ArrayNode) list, c.getIndex());
+ }
+ } else {
+ obj.remove(c.getName());
+ }
+ }
+ } else {
+ // get child and
+ // if exist set nested to child
+ // else create new empty object or array - depending on c.isArray()
+ JsonNode node = nested.get(c.getName());
+ if (node == null) {
+ if (c.isArray()) {
+ node = MAPPER.createArrayNode();
+ } else {
+ node = MAPPER.createObjectNode();
+ }
+ ((ObjectNode) nested).set(c.getName(), node);
+ }
+ nested = node;
+ }
+ }
+ }
+ }
+
+ private static void setArrayItem(ArrayNode list, int index, JsonNode valNode) {
+ if (index == -1) {
+ // append to end of array
+ list.add(valNode);
+ return;
+ }
+ // make sure items up to index exist
+ for (int i = list.size(); i < index+1; i++) {
+ list.add(NullNode.instance);
+ }
+ list.set(index, valNode);
+ }
+
+ private static void removeArrayItem(ArrayNode list, int index) {
+ if (index == -1) {
+ throw new IllegalArgumentException("Internal error - should never be called with index == -1");
+ }
+ list.remove(index);
+ }
+
+ private static JsonNode valueToJsonNode(String val) {
+ // try get value as JSON object
+ try {
+ return MAPPER.readValue(val, ObjectNode.class);
+ } catch (Exception ignored) {
+ }
+
+ // try get value as JSON array
+ try {
+ return MAPPER.readValue(val, ArrayNode.class);
+ } catch (Exception ignored) {
+ }
+
+ if (isBoolean(val)) {
+ return BooleanNode.valueOf(Boolean.valueOf(val));
+ } else if (isInteger(val)) {
+ return LongNode.valueOf(Long.valueOf(val));
+ } else if (isNumber(val)) {
+ return DoubleNode.valueOf(Double.valueOf(val));
+ } else if (isQuoted(val)) {
+ return TextNode.valueOf(unquote(val));
+ }
+
+ return TextNode.valueOf(val);
+ }
+
+ private static boolean isInteger(String val) {
+ try {
+ Long.valueOf(val);
+ return true;
+ } catch (Exception ignored) {
+ return false;
+ }
+ }
+
+ private static boolean isNumber(String val) {
+ try {
+ Double.valueOf(val);
+ return true;
+ } catch (Exception ignored) {
+ return false;
+ }
+ }
+
+ private static boolean isBoolean(String val) {
+ return "false".equals(val) || "true".equals(val);
+ }
+
+ private static boolean isQuoted(String val) {
+ return val.startsWith("'") || val.startsWith("\"");
+ }
+
+ private static String unquote(String val) {
+ if (!(val.startsWith("'") || val.startsWith("\"")) || !(val.endsWith("'") || val.endsWith("\""))) {
+ throw new RuntimeException("Invalid string value: " + val);
+ }
+ return val.substring(1, val.length()-1);
+ }
+
+ public static void merge(JsonNode source, ObjectNode dest) {
+ // Iterate over source
+ // For each child check if exists on the destination
+ // if it does go deep
+ // otherwise copy over
+ // if it's last component, set it on destination
+
+ if (!source.isObject()) {
+ throw new RuntimeException("Not a JSON object: " + source);
+ }
+
+ Iterator<Map.Entry<String, JsonNode>> it = ((ObjectNode) source).fields();
+ while (it.hasNext()) {
+ Map.Entry<String, JsonNode> item = it.next();
+ String name = item.getKey();
+ JsonNode node = item.getValue();
+
+ JsonNode destNode = dest.get(name);
+ if (destNode != null) {
+ if (destNode.isObject()) {
+ if (node.isObject()) {
+ merge(node, (ObjectNode) destNode);
+ } else {
+ throw new RuntimeException("Attribute is of incompatible type - " + name + ": " + node);
+ }
+ } else if (destNode.isArray()) {
+ if (node.isArray()) {
+ dest.set(name, node);
+ } else {
+ throw new RuntimeException("Attribute is of incompatible type - " + name + ": " + node);
+ }
+ } else {
+ dest.set(name, node);
+ }
+ } else {
+ dest.set(name, node);
+ }
+ }
+ }
+}
diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/ReturnFields.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/ReturnFields.java
new file mode 100644
index 0000000..a185471
--- /dev/null
+++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/ReturnFields.java
@@ -0,0 +1,333 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.client.admin.cli.util;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * @author <a href="mailto:marko.strukelj@gmail.com">Marko Strukelj</a>
+ */
+public class ReturnFields implements Iterable<String> {
+
+ public static ReturnFields ALL = new ReturnFields() {
+ @Override
+ public ReturnFields child(String field) {
+ return NONE;
+ }
+
+ @Override
+ public boolean included(String... pathSegments) {
+ return true;
+ }
+
+ @Override
+ public boolean excluded(String field) {
+ return false;
+ }
+
+ @Override
+ public Iterator<String> iterator() {
+ return Collections.singletonList("*").iterator();
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return false;
+ }
+
+ public boolean isAll() {
+ return true;
+ }
+
+ @Override
+ public String toString() {
+ return "[ReturnFields ALL]";
+ }
+ };
+
+ public static ReturnFields NONE = new ReturnFields() {
+ @Override
+ public ReturnFields child(String field) {
+ return this;
+ }
+
+ @Override
+ public boolean included(String... pathSegments) {
+ return false;
+ }
+
+ @Override
+ public boolean excluded(String field) {
+ return false;
+ }
+
+ @Override
+ public Iterator<String> iterator() {
+ List<String> emptyList = Collections.emptyList();
+ return emptyList.iterator();
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return true;
+ }
+
+ @Override
+ public boolean isAll() {
+ return false;
+ }
+
+ @Override
+ public String toString() {
+ return "[ReturnFields NONE]";
+ }
+ };
+
+ public static ReturnFields ALL_RECURSIVELY = new ReturnFields() {
+ @Override
+ public ReturnFields child(String field) {
+ return this;
+ }
+
+ @Override
+ public boolean included(String... pathSegments) {
+ return true;
+ }
+
+ @Override
+ public boolean excluded(String field) {
+ return false;
+ }
+
+ @Override
+ public Iterator<String> iterator() {
+ List<String> emptyList = Collections.emptyList();
+ return emptyList.iterator();
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return false;
+ }
+
+ @Override
+ public boolean isAll() {
+ return true;
+ }
+ };
+
+ private enum TargetState {
+ IdentCommaOpen,
+ Ident,
+ Comma,
+ Anything
+ }
+
+ private enum FieldState {
+ start,
+ name,
+ end
+ }
+
+
+ private HashMap<String, ReturnFields> fields = new LinkedHashMap<>();
+
+
+
+ public ReturnFields() {}
+
+ public ReturnFields(String spec) {
+
+ if (spec == null || spec.trim().length() == 0) {
+ throw new IllegalArgumentException("Fields spec is null or empty!");
+ }
+ // parse the spec, building up the tree for nested children
+ char[] buf = spec.toCharArray();
+ StringBuilder token = new StringBuilder(buf.length);
+
+ // stack for handling depth
+ LinkedList<HashMap<String, ReturnFields>> specs = new LinkedList<>();
+ specs.add(fields);
+
+ // parser state
+ FieldState fldState = FieldState.start;
+ TargetState state = TargetState.Ident;
+
+ int i;
+ for (i = 0; i < buf.length; i++) {
+ char c = buf[i];
+
+ if (c == ',') {
+ if (state == TargetState.Ident) {
+ error(spec, i);
+ }
+ if (fldState == FieldState.name) {
+ specs.getLast().put(token.toString(), null);
+ token.setLength(0);
+ }
+ state = TargetState.Ident;
+ fldState = FieldState.start;
+ } else if (c == '(') {
+ if (state != TargetState.IdentCommaOpen && state != TargetState.Anything) {
+ error(spec, i);
+ }
+ ReturnFields sub = new ReturnFields();
+ specs.getLast().put(token.toString(), sub);
+ specs.add(sub.fields);
+ token.setLength(0);
+
+ state = TargetState.Ident;
+ fldState = FieldState.start;
+ } else if (c == ')') {
+ if (state != TargetState.Anything) {
+ error(spec, i);
+ }
+ if (fldState == FieldState.name) {
+ specs.getLast().put(token.toString(), null);
+ token.setLength(0);
+
+ }
+ specs.removeLast();
+
+ fldState = FieldState.end;
+ state = specs.size() > 1 ? TargetState.Anything : TargetState.Comma;
+ } else {
+ token.append(c);
+ if (fldState == FieldState.start) {
+ fldState = FieldState.name;
+ state = specs.size() > 1 ? TargetState.Anything : TargetState.IdentCommaOpen;
+ }
+ }
+ }
+
+ if (specs.size() > 1) {
+ error(spec, i);
+ }
+
+ if (token.length() > 0) {
+ specs.getLast().put(token.toString(), null);
+ } else if (!(state == TargetState.Anything || state == TargetState.Comma)) {
+ error(spec, i);
+ }
+ }
+
+ private void error(String spec, int i) {
+ throw new RuntimeException("Invalid fields specification at position " + i + ": " + spec);
+ }
+
+
+
+ /**
+ * Get ReturnFields for a child field of JSONObject type.
+ *
+ * <p>For basic-typed fields this always returns null. Use included() for those.</p>
+ *
+ * @param field The child field name for nested returns.
+ * @return ReturnFields for a child field
+ */
+ public ReturnFields child(String field) {
+ ReturnFields returnFields = fields.get(field);
+ if (returnFields == null) {
+ returnFields = fields.get("*");
+ if (returnFields == null) {
+ returnFields = ReturnFields.NONE;
+ }
+ }
+ return returnFields;
+ }
+
+ /**
+ * Check to see if the field should be included in JSON response.
+ *
+ * <p>The check can be performed for any level of depth relative to current nesting level, by specifying multiple path segments.</p>
+ *
+ * @param pathSegments Segments to test in the tree of return fields.
+ * @return true if the specified path should be part of JSON response or not
+ */
+ public boolean included(String... pathSegments) {
+
+ if (pathSegments == null || pathSegments.length == 0) {
+ throw new IllegalArgumentException("No path specified!");
+ }
+ ReturnFields current = this;
+
+ for (String path : pathSegments) {
+ if (current == null) {
+ return false;
+ }
+
+ if (current.fields.containsKey("-" + path)) {
+ return false;
+ }
+ if (current.fields.containsKey("*")) {
+ return true;
+ }
+ if (!current.fields.containsKey(path)) {
+ return false;
+ }
+ current = current.fields.get(path);
+ }
+ return true;
+ }
+
+ /**
+ * Check to see if the field specified is set to be explicitly excluded.
+ * @param field The field name to check
+ * @return If the field was explicitly set to be excluded
+ */
+ public boolean excluded(String field) {
+ if (fields.containsKey("-" + field)) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Iterate over child fields to be included in response.
+ *
+ * <p>To get nested field specifier use child(name) passing the field name this iterator returns.</p>
+ *
+ * @return iterator over child fields to be included in response.
+ */
+ public Iterator<String> iterator() {
+ return fields.keySet().iterator();
+ }
+
+ /**
+ * Determine if zero fields should be returned.
+ *
+ * @return <code>true</code> if the list is empty, else, <code>false</code>
+ */
+ public boolean isEmpty() {
+ return this.fields.isEmpty();
+ }
+
+ public boolean isAll() {
+ return this.fields.keySet().contains("*");
+ }
+
+ @Override
+ public String toString() {
+ return "[ReturnFieldsImpl: fields=" + this.fields + "]";
+ }
+}
diff --git a/integration/client-cli/admin-cli/src/main/resources/META-INF/services/javax.ws.rs.ext.Providers b/integration/client-cli/admin-cli/src/main/resources/META-INF/services/javax.ws.rs.ext.Providers
new file mode 100644
index 0000000..bc2a259
--- /dev/null
+++ b/integration/client-cli/admin-cli/src/main/resources/META-INF/services/javax.ws.rs.ext.Providers
@@ -0,0 +1,23 @@
+org.jboss.resteasy.plugins.providers.jackson.ResteasyJackson2Provider
+org.jboss.resteasy.plugins.providers.DataSourceProvider
+org.jboss.resteasy.plugins.providers.DocumentProvider
+org.jboss.resteasy.plugins.providers.DefaultTextPlain
+org.jboss.resteasy.plugins.providers.StringTextStar
+org.jboss.resteasy.plugins.providers.SourceProvider
+org.jboss.resteasy.plugins.providers.InputStreamProvider
+org.jboss.resteasy.plugins.providers.ReaderProvider
+org.jboss.resteasy.plugins.providers.ByteArrayProvider
+org.jboss.resteasy.plugins.providers.FormUrlEncodedProvider
+org.jboss.resteasy.plugins.providers.JaxrsFormProvider
+org.jboss.resteasy.plugins.providers.FileProvider
+org.jboss.resteasy.plugins.providers.FileRangeWriter
+org.jboss.resteasy.plugins.providers.StreamingOutputProvider
+org.jboss.resteasy.plugins.providers.IIOImageProvider
+org.jboss.resteasy.plugins.providers.SerializableProvider
+org.jboss.resteasy.plugins.interceptors.CacheControlFeature
+org.jboss.resteasy.plugins.interceptors.encoding.AcceptEncodingGZIPInterceptor
+org.jboss.resteasy.plugins.interceptors.encoding.AcceptEncodingGZIPFilter
+org.jboss.resteasy.plugins.interceptors.encoding.ClientContentEncodingAnnotationFeature
+org.jboss.resteasy.plugins.interceptors.encoding.GZIPDecodingInterceptor
+org.jboss.resteasy.plugins.interceptors.encoding.GZIPEncodingInterceptor
+org.jboss.resteasy.plugins.interceptors.encoding.ServerContentEncodingAnnotationFeature
diff --git a/integration/client-cli/admin-cli/src/test/java/org/keycloak/client/admin/cli/util/MergeAttributesTest.java b/integration/client-cli/admin-cli/src/test/java/org/keycloak/client/admin/cli/util/MergeAttributesTest.java
new file mode 100644
index 0000000..ef6d897
--- /dev/null
+++ b/integration/client-cli/admin-cli/src/test/java/org/keycloak/client/admin/cli/util/MergeAttributesTest.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.client.admin.cli.util;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import org.junit.Test;
+import org.keycloak.client.admin.cli.common.AttributeOperation;
+import org.keycloak.client.admin.cli.common.CmdStdinContext;
+
+import java.nio.charset.Charset;
+import java.util.LinkedList;
+import java.util.List;
+
+import static org.keycloak.client.admin.cli.common.AttributeOperation.Type.DELETE;
+import static org.keycloak.client.admin.cli.common.AttributeOperation.Type.SET;
+import static org.keycloak.client.admin.cli.util.OutputUtil.MAPPER;
+import static org.keycloak.client.admin.cli.util.ParseUtil.mergeAttributes;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public class MergeAttributesTest {
+
+ @Test
+ public void testMergeAttrs() throws Exception {
+
+ List<AttributeOperation> attrs = new LinkedList<>();
+ attrs.add(new AttributeOperation(SET, "realm", "nurealm"));
+ attrs.add(new AttributeOperation(SET, "enabled", "true"));
+ attrs.add(new AttributeOperation(SET, "revokeRefreshToken", "true"));
+ attrs.add(new AttributeOperation(SET, "accessTokenLifespan", "900"));
+ attrs.add(new AttributeOperation(SET, "smtpServer.host", "localhost"));
+ attrs.add(new AttributeOperation(SET, "extra.key1", "somevalue"));
+ attrs.add(new AttributeOperation(SET, "extra.key2", "[\"somevalue\"]"));
+ attrs.add(new AttributeOperation(SET, "extra.key3[1]", "second item"));
+ attrs.add(new AttributeOperation(SET, "extra.key4", "\"true\""));
+ attrs.add(new AttributeOperation(SET, "extra.key5", "\"1000\""));
+ attrs.add(new AttributeOperation(DELETE, "id"));
+ attrs.add(new AttributeOperation(DELETE, "attributes.\"_browser_header.xFrameOptions\""));
+
+ String localJSON = "{\n" +
+ " \"id\" : \"24e5d572-756a-435b-8b2b-edbd0a7aa93d\",\n" +
+ " \"realm\" : \"demorealm\",\n" +
+ " \"notBefore\" : 0,\n" +
+ " \"revokeRefreshToken\" : false,\n" +
+ " \"accessTokenLifespan\" : 300,\n" +
+ " \"defaultRoles\" : [ \"offline_access\", \"uma_authorization\" ],\n" +
+ " \"smtpServer\" : { },\n" +
+ " \"attributes\" : {\n" +
+ " \"_browser_header.xFrameOptions\" : \"SAMEORIGIN\",\n" +
+ " \"_browser_header.contentSecurityPolicy\" : \"frame-src 'self'\"\n" +
+ " }\n" +
+ "}";
+
+ ObjectNode localNode = MAPPER.readValue(localJSON.getBytes(Charset.forName("utf-8")), ObjectNode.class);
+ CmdStdinContext<JsonNode> ctx = new CmdStdinContext<>();
+ ctx.setResult(localNode);
+
+ ctx = mergeAttributes(ctx, MAPPER.createObjectNode(), attrs);
+ System.out.println(ctx);
+
+ String remoteJSON = "{\n" +
+ " \"id\" : \"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\",\n" +
+ " \"realm\" : \"demorealm\",\n" +
+ " \"notBefore\" : 0,\n" +
+ " \"revokeRefreshToken\" : false,\n" +
+ " \"accessTokenLifespan\" : 300,\n" +
+ " \"defaultRoles\" : [ \"uma_authorization\" ],\n" +
+ " \"remote\" : \"value\",\n" +
+ " \"attributes\" : {\n" +
+ " \"_browser_header.xFrameOptions\" : \"SAMEORIGIN\",\n" +
+ " \"_browser_header.x\" : \"ORIGIN\",\n" +
+ " \"_browser_header.contentSecurityPolicy\" : \"frame-src 'self'\"\n" +
+ " }\n" +
+ "}";
+
+ ObjectNode remoteNode = MAPPER.readValue(remoteJSON.getBytes(Charset.forName("utf-8")), ObjectNode.class);
+ CmdStdinContext<ObjectNode> ctxremote = new CmdStdinContext<>();
+ ctxremote.setResult(remoteNode);
+
+ ReflectionUtil.merge(ctx.getResult(), ctxremote.getResult());
+ System.out.println(ctx);
+
+ //ctx = mergeAttributes(ctx, MAPPER.createObjectNode(), attrs);
+ }
+}
diff --git a/integration/client-cli/admin-cli/src/test/java/org/keycloak/client/admin/cli/util/ReturnFieldsTest.java b/integration/client-cli/admin-cli/src/test/java/org/keycloak/client/admin/cli/util/ReturnFieldsTest.java
new file mode 100644
index 0000000..bd2105b
--- /dev/null
+++ b/integration/client-cli/admin-cli/src/test/java/org/keycloak/client/admin/cli/util/ReturnFieldsTest.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.client.admin.cli.util;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+/**
+ * @author <a href="mailto:marko.strukelj@gmail.com">Marko Strukelj</a>
+ */
+public class ReturnFieldsTest {
+
+ @Test
+ public void testBasic() {
+ String spec = "field1,field2,field3";
+ ReturnFields fspec = new ReturnFields(spec);
+
+ StringBuilder val = new StringBuilder();
+ for (String field : fspec) {
+ if (val.length() > 0)
+ val.append(',');
+ val.append(field);
+ }
+ Assert.assertEquals(spec, val.toString());
+
+ // check catching errors
+
+ String[] specs = {
+ "",
+ null,
+ ",",
+ "field1,",
+ ",field2"
+ };
+
+ for (String filter : specs) {
+ try {
+ fspec = new ReturnFields(filter);
+ Assert.fail("Parsing of fields spec should have failed! : " + filter);
+ } catch (Exception e) {
+ //e.printStackTrace();
+ }
+ }
+ }
+
+ @Test
+ public void testExclude() {
+ ReturnFields spec = new ReturnFields("*,-name,dog(*,-color)");
+
+ Assert.assertTrue(spec.included("foo"));
+ Assert.assertTrue(spec.included("bar"));
+ Assert.assertFalse(spec.included("name"));
+ Assert.assertTrue(spec.included("dog"));
+ Assert.assertTrue(spec.child("dog").included("breed"));
+ Assert.assertFalse(spec.child("dog").included("color"));
+
+ Assert.assertTrue(spec.excluded("name"));
+ Assert.assertFalse(spec.excluded("foo"));
+ Assert.assertFalse(spec.excluded("bar"));
+ Assert.assertTrue(spec.child("dog").excluded("color"));
+ Assert.assertFalse(spec.child("dog").excluded("breed"));
+ }
+
+ @Test
+ public void testNestedWithGlob() {
+ ReturnFields spec = new ReturnFields("name,dog(*)");
+
+ Assert.assertTrue(spec.included("name"));
+ Assert.assertFalse(spec.included("tacos"));
+
+ Assert.assertNotNull(spec.child("dog"));
+ Assert.assertTrue(spec.child("dog").included("dogname"));
+
+ Assert.assertNotNull(spec.child("cat"));
+ Assert.assertFalse(spec.child("cat").included("name"));
+ }
+
+ @Test
+ public void testNested() {
+ String spec = "field1,field2(sub1,sub2(subsub1)),field3";
+ ReturnFields fspec = new ReturnFields(spec);
+
+ String val = traverse(fspec);
+ Assert.assertEquals(spec, val.toString());
+
+
+ // check catching errors
+
+ String[] specs = {
+ "(",
+ ")",
+ "field1,(",
+ "field1,)",
+ "field1,field2(",
+ "field1,field2)",
+ "field1,field2()",
+ "field1,field2(sub1)(",
+ "field1,field2(sub1))",
+ "field1,field2(sub1),"
+ };
+
+ for (String filter : specs) {
+ try {
+ fspec = new ReturnFields(filter);
+ Assert.fail("Parsing of fields spec should have failed! : " + filter);
+ } catch (Exception e) {
+ //e.printStackTrace();
+ }
+ }
+ }
+
+ private String traverse(ReturnFields fspec) {
+ StringBuilder buf = new StringBuilder();
+ for (String field : fspec) {
+ if (buf.length() > 0)
+ buf.append(',');
+ buf.append(field);
+
+ ReturnFields cspec = fspec.child(field);
+ if (cspec != null && cspec != ReturnFields.NONE) {
+ buf.append('(');
+ buf.append(traverse(cspec));
+ buf.append(')');
+ }
+ }
+ return buf.toString();
+ }
+}
diff --git a/integration/client-cli/client-cli-dist/assembly.xml b/integration/client-cli/client-cli-dist/assembly.xml
index ee27cb2..fc74f20 100755
--- a/integration/client-cli/client-cli-dist/assembly.xml
+++ b/integration/client-cli/client-cli-dist/assembly.xml
@@ -36,11 +36,23 @@
<outputDirectory>keycloak-client-tools/bin</outputDirectory>
<filtered>true</filtered>
</file>
+ <file>
+ <source>../admin-cli/src/main/bin/kcadm.sh</source>
+ <outputDirectory>keycloak-client-tools/bin</outputDirectory>
+ <fileMode>0755</fileMode>
+ <filtered>true</filtered>
+ </file>
+ <file>
+ <source>../admin-cli/src/main/bin/kcadm.bat</source>
+ <outputDirectory>keycloak-client-tools/bin</outputDirectory>
+ <filtered>true</filtered>
+ </file>
</files>
<dependencySets>
<dependencySet>
<includes>
<include>org.keycloak:keycloak-client-registration-cli</include>
+ <include>org.keycloak:keycloak-admin-cli</include>
</includes>
<outputDirectory>keycloak-client-tools/bin/client</outputDirectory>
</dependencySet>
diff --git a/integration/client-cli/client-cli-dist/pom.xml b/integration/client-cli/client-cli-dist/pom.xml
index 1b567cb..daabefc 100755
--- a/integration/client-cli/client-cli-dist/pom.xml
+++ b/integration/client-cli/client-cli-dist/pom.xml
@@ -34,6 +34,10 @@
<groupId>org.keycloak</groupId>
<artifactId>keycloak-client-registration-cli</artifactId>
</dependency>
+ <dependency>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-admin-cli</artifactId>
+ </dependency>
</dependencies>
<build>
diff --git a/integration/client-cli/client-registration-cli/pom.xml b/integration/client-cli/client-registration-cli/pom.xml
index dc5c210..63a6673 100755
--- a/integration/client-cli/client-registration-cli/pom.xml
+++ b/integration/client-cli/client-registration-cli/pom.xml
@@ -33,7 +33,6 @@
<dependency>
<groupId>org.jboss.aesh</groupId>
<artifactId>aesh</artifactId>
- <version>0.66.10</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
diff --git a/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/commands/ConfigTruststoreCmd.java b/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/commands/ConfigTruststoreCmd.java
index b8f1f34..ecadf62 100644
--- a/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/commands/ConfigTruststoreCmd.java
+++ b/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/commands/ConfigTruststoreCmd.java
@@ -1,3 +1,19 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
package org.keycloak.client.registration.cli.commands;
import org.jboss.aesh.cl.CommandDefinition;
@@ -153,7 +169,7 @@ public class ConfigTruststoreCmd extends AbstractAuthOptionsCmd implements Comma
public static String usage() {
StringWriter sb = new StringWriter();
PrintWriter out = new PrintWriter(sb);
- out.println("Usage: " + CMD + " config truststore [TRUSTSTORE | --delete] [--trustpass PASSWOD] [ARGUMENTS]");
+ out.println("Usage: " + CMD + " config truststore [TRUSTSTORE | --delete] [--trustpass PASSWORD] [ARGUMENTS]");
out.println();
out.println("Command to configure a global truststore to use when using https to connect to Keycloak server.");
out.println();
@@ -174,7 +190,7 @@ public class ConfigTruststoreCmd extends AbstractAuthOptionsCmd implements Comma
out.println("Specify a truststore - you will be prompted for truststore password every time it is used:");
out.println(" " + PROMPT + " " + CMD + " config truststore " + OS_ARCH.path("~/.keycloak/truststore.jks"));
out.println();
- out.println("Specify a truststore, and password - truststore will automatically without prompting for password:");
+ out.println("Specify a truststore, and password - truststore will automatically be used without prompting for password:");
out.println(" " + PROMPT + " " + CMD + " config truststore --storepass " + OS_ARCH.envVar("PASSWORD") + " " + OS_ARCH.path("~/.keycloak/truststore.jks"));
out.println();
out.println("Remove truststore configuration:");
integration/client-cli/pom.xml 11(+11 -0)
diff --git a/integration/client-cli/pom.xml b/integration/client-cli/pom.xml
index 4759514..20753c9 100644
--- a/integration/client-cli/pom.xml
+++ b/integration/client-cli/pom.xml
@@ -30,8 +30,19 @@
<artifactId>keycloak-client-cli-parent</artifactId>
<packaging>pom</packaging>
+ <dependencyManagement>
+ <dependencies>
+ <dependency>
+ <groupId>org.jboss.aesh</groupId>
+ <artifactId>aesh</artifactId>
+ <version>0.66.10</version>
+ </dependency>
+ </dependencies>
+ </dependencyManagement>
+
<modules>
<module>client-registration-cli</module>
+ <module>admin-cli</module>
<module>client-cli-dist</module>
</modules>
</project>
\ No newline at end of file
diff --git a/model/infinispan/src/main/java/org/keycloak/models/authorization/infinispan/CachedPolicyStore.java b/model/infinispan/src/main/java/org/keycloak/models/authorization/infinispan/CachedPolicyStore.java
index b503bce..e332de0 100644
--- a/model/infinispan/src/main/java/org/keycloak/models/authorization/infinispan/CachedPolicyStore.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/authorization/infinispan/CachedPolicyStore.java
@@ -18,6 +18,17 @@
package org.keycloak.models.authorization.infinispan;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.function.Supplier;
+import java.util.stream.Collectors;
+
import org.infinispan.Cache;
import org.keycloak.authorization.model.Policy;
import org.keycloak.authorization.model.Resource;
@@ -29,17 +40,10 @@ import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.authorization.infinispan.InfinispanStoreFactoryProvider.CacheTransaction;
import org.keycloak.models.authorization.infinispan.entities.CachedPolicy;
+import org.keycloak.models.cache.authorization.CachedStoreFactoryProvider;
import org.keycloak.representations.idm.authorization.DecisionStrategy;
import org.keycloak.representations.idm.authorization.Logic;
-import java.util.ArrayList;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Map.Entry;
-import java.util.Set;
-import java.util.stream.Collectors;
-
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
@@ -47,41 +51,64 @@ public class CachedPolicyStore implements PolicyStore {
private static final String POLICY_ID_CACHE_PREFIX = "policy-id-";
- private final Cache<String, List> cache;
+ private final Cache<String, List<CachedPolicy>> cache;
private final KeycloakSession session;
private final CacheTransaction transaction;
+ private final List<String> cacheKeys;
private StoreFactory storeFactory;
private PolicyStore delegate;
+ private CachedStoreFactoryProvider cachedStoreFactory;
- public CachedPolicyStore(KeycloakSession session, CacheTransaction transaction) {
+ public CachedPolicyStore(KeycloakSession session, CacheTransaction transaction, StoreFactory storeFactory) {
this.session = session;
this.transaction = transaction;
InfinispanConnectionProvider provider = session.getProvider(InfinispanConnectionProvider.class);
this.cache = provider.getCache(InfinispanConnectionProvider.AUTHORIZATION_CACHE_NAME);
+ cacheKeys = new ArrayList<>();
+ cacheKeys.add("findByResource");
+ cacheKeys.add("findByResourceType");
+ cacheKeys.add("findByScopeIds");
+ cacheKeys.add("findByType");
+ this.storeFactory = storeFactory;
}
@Override
public Policy create(String name, String type, ResourceServer resourceServer) {
Policy policy = getDelegate().create(name, type, getStoreFactory().getResourceServerStore().findById(resourceServer.getId()));
+ String id = policy.getId();
+
+ this.transaction.whenRollback(() -> {
+ cache.remove(getCacheKeyForPolicy(id));
+ });
- this.transaction.whenRollback(() -> cache.remove(getCacheKeyForPolicy(policy.getId())));
+ this.transaction.whenCommit(() -> {
+ invalidateCache(resourceServer.getId());
+ });
return createAdapter(new CachedPolicy(policy));
}
@Override
public void delete(String id) {
+ Policy policy = findById(id, null);
+ if (policy == null) {
+ return;
+ }
+ ResourceServer resourceServer = policy.getResourceServer();
getDelegate().delete(id);
- this.transaction.whenCommit(() -> cache.remove(getCacheKeyForPolicy(id)));
+ this.transaction.whenCommit(() -> {
+ cache.remove(getCacheKeyForPolicy(id));
+ invalidateCache(resourceServer.getId());
+ });
}
@Override
- public Policy findById(String id) {
+ public Policy findById(String id, String resourceServerId) {
String cacheKeyForPolicy = getCacheKeyForPolicy(id);
List<CachedPolicy> cached = this.cache.get(cacheKeyForPolicy);
if (cached == null) {
- Policy policy = getDelegate().findById(id);
+ Policy policy = getDelegate().findById(id, resourceServerId);
if (policy != null) {
return createAdapter(updatePolicyCache(policy));
@@ -100,7 +127,7 @@ public class CachedPolicyStore implements PolicyStore {
@Override
public List<Policy> findByResourceServer(String resourceServerId) {
- return getDelegate().findByResourceServer(resourceServerId).stream().map(policy -> findById(policy.getId())).collect(Collectors.toList());
+ return getDelegate().findByResourceServer(resourceServerId);
}
@Override
@@ -109,88 +136,49 @@ public class CachedPolicyStore implements PolicyStore {
}
@Override
- public List<Policy> findByResource(String resourceId) {
- List<Policy> cache = new ArrayList<>();
-
- for (Entry entry : this.cache.entrySet()) {
- String cacheKey = (String) entry.getKey();
-
- if (cacheKey.startsWith(POLICY_ID_CACHE_PREFIX)) {
- List<CachedPolicy> value = (List<CachedPolicy>) entry.getValue();
- CachedPolicy policy = value.get(0);
-
- if (policy.getResourcesIds().contains(resourceId)) {
- cache.add(findById(policy.getId()));
- }
- }
- }
-
- if (cache.isEmpty()) {
- getDelegate().findByResource(resourceId).forEach(policy -> cache.add(findById(updatePolicyCache(policy).getId())));
- }
-
- return cache;
+ public List<Policy> findByResource(String resourceId, String resourceServerId) {
+ return cacheResult(new StringBuilder("findByResource").append(resourceServerId).append(resourceId).toString(), () -> getDelegate().findByResource(resourceId, resourceServerId));
}
@Override
public List<Policy> findByResourceType(String resourceType, String resourceServerId) {
- List<Policy> cache = new ArrayList<>();
-
- for (Entry entry : this.cache.entrySet()) {
- String cacheKey = (String) entry.getKey();
-
- if (cacheKey.startsWith(POLICY_ID_CACHE_PREFIX)) {
- List<CachedPolicy> value = (List<CachedPolicy>) entry.getValue();
- CachedPolicy policy = value.get(0);
-
- if (policy.getResourceServerId().equals(resourceServerId) && policy.getConfig().getOrDefault("defaultResourceType", "").equals(resourceType)) {
- cache.add(findById(policy.getId()));
- }
- }
- }
-
- if (cache.isEmpty()) {
- getDelegate().findByResourceType(resourceType, resourceServerId).forEach(policy -> cache.add(findById(updatePolicyCache(policy).getId())));
- }
-
- return cache;
+ return cacheResult(new StringBuilder("findByResourceType").append(resourceServerId).append(resourceType).toString(), () -> getDelegate().findByResourceType(resourceType, resourceServerId));
}
@Override
public List<Policy> findByScopeIds(List<String> scopeIds, String resourceServerId) {
- List<Policy> cache = new ArrayList<>();
+ List<Policy> policies = new ArrayList<>();
- for (Entry entry : this.cache.entrySet()) {
- String cacheKey = (String) entry.getKey();
-
- if (cacheKey.startsWith(POLICY_ID_CACHE_PREFIX)) {
- List<CachedPolicy> value = (List<CachedPolicy>) entry.getValue();
- CachedPolicy policy = value.get(0);
-
- for (String scopeId : policy.getScopesIds()) {
- if (scopeIds.contains(scopeId)) {
- cache.add(findById(policy.getId()));
- break;
- }
- }
- }
+ for (String scopeId : scopeIds) {
+ policies.addAll(cacheResult(new StringBuilder("findByScopeIds").append(resourceServerId).append(scopeId).toString(), () -> getDelegate().findByScopeIds(Arrays.asList(scopeId), resourceServerId)));
}
- if (cache.isEmpty()) {
- getDelegate().findByScopeIds(scopeIds, resourceServerId).forEach(policy -> cache.add(findById(updatePolicyCache(policy).getId())));
- }
+ return policies;
+ }
- return cache;
+ @Override
+ public List<Policy> findByType(String type, String resourceServerId) {
+ return cacheResult(new StringBuilder("findByType").append(resourceServerId).append(type).toString(), () -> getDelegate().findByType(type, resourceServerId));
}
@Override
- public List<Policy> findByType(String type) {
- return getDelegate().findByType(type).stream().map(policy -> findById(policy.getId())).collect(Collectors.toList());
+ public List<Policy> findDependentPolicies(String id, String resourceServerId) {
+ return getDelegate().findDependentPolicies(id, resourceServerId);
}
@Override
- public List<Policy> findDependentPolicies(String id) {
- return getDelegate().findDependentPolicies(id).stream().map(policy -> findById(policy.getId())).collect(Collectors.toList());
+ public void notifyChange(Object cached) {
+ String resourceServerId;
+
+ if (Resource.class.isInstance(cached)) {
+ resourceServerId = ((Resource) cached).getResourceServer().getId();
+ } else if (Scope.class.isInstance(cached)){
+ resourceServerId = ((Scope) cached).getResourceServer().getId();
+ } else {
+ throw new RuntimeException("Unexpected notification [" + cached + "]");
+ }
+
+ invalidateCache(resourceServerId);
}
private String getCacheKeyForPolicy(String policyId) {
@@ -198,10 +186,6 @@ public class CachedPolicyStore implements PolicyStore {
}
private StoreFactory getStoreFactory() {
- if (this.storeFactory == null) {
- this.storeFactory = this.session.getProvider(StoreFactory.class);
- }
-
return this.storeFactory;
}
@@ -216,6 +200,9 @@ public class CachedPolicyStore implements PolicyStore {
private Policy createAdapter(CachedPolicy cached) {
return new Policy() {
+ private Set<Scope> scopes;
+ private Set<Resource> resources;
+ private Set<Policy> associatedPolicies;
private Policy updated;
@Override
@@ -285,54 +272,56 @@ public class CachedPolicyStore implements PolicyStore {
@Override
public ResourceServer getResourceServer() {
- return getStoreFactory().getResourceServerStore().findById(cached.getResourceServerId());
+ return getCachedStoreFactory().getResourceServerStore().findById(cached.getResourceServerId());
}
@Override
public void addScope(Scope scope) {
- getDelegateForUpdate().addScope(getStoreFactory().getScopeStore().findById(scope.getId()));
+ getDelegateForUpdate().addScope(getStoreFactory().getScopeStore().findById(scope.getId(), cached.getResourceServerId()));
cached.addScope(scope);
}
@Override
public void removeScope(Scope scope) {
- getDelegateForUpdate().removeScope(getStoreFactory().getScopeStore().findById(scope.getId()));
+ getDelegateForUpdate().removeScope(scope);
cached.removeScope(scope);
}
@Override
public void addAssociatedPolicy(Policy associatedPolicy) {
- getDelegateForUpdate().addAssociatedPolicy(getStoreFactory().getPolicyStore().findById(associatedPolicy.getId()));
+ getDelegateForUpdate().addAssociatedPolicy(getStoreFactory().getPolicyStore().findById(associatedPolicy.getId(), cached.getResourceServerId()));
cached.addAssociatedPolicy(associatedPolicy);
}
@Override
public void removeAssociatedPolicy(Policy associatedPolicy) {
- getDelegateForUpdate().removeAssociatedPolicy(getStoreFactory().getPolicyStore().findById(associatedPolicy.getId()));
+ getDelegateForUpdate().removeAssociatedPolicy(getStoreFactory().getPolicyStore().findById(associatedPolicy.getId(), cached.getResourceServerId()));
cached.removeAssociatedPolicy(associatedPolicy);
}
@Override
public void addResource(Resource resource) {
- getDelegateForUpdate().addResource(getStoreFactory().getResourceStore().findById(resource.getId()));
+ getDelegateForUpdate().addResource(getStoreFactory().getResourceStore().findById(resource.getId(), cached.getResourceServerId()));
cached.addResource(resource);
}
@Override
public void removeResource(Resource resource) {
- getDelegateForUpdate().removeResource(getStoreFactory().getResourceStore().findById(resource.getId()));
+ getDelegateForUpdate().removeResource(getStoreFactory().getResourceStore().findById(resource.getId(), cached.getResourceServerId()));
cached.removeResource(resource);
}
@Override
public Set<Policy> getAssociatedPolicies() {
- Set<Policy> associatedPolicies = new HashSet<>();
+ if (associatedPolicies == null) {
+ associatedPolicies = new HashSet<>();
- for (String id : cached.getAssociatedPoliciesIds()) {
- Policy cached = findById(id);
+ for (String id : cached.getAssociatedPoliciesIds()) {
+ Policy policy = findById(id, cached.getResourceServerId());
- if (cached != null) {
- associatedPolicies.add(cached);
+ if (policy != null) {
+ associatedPolicies.add(policy);
+ }
}
}
@@ -341,13 +330,15 @@ public class CachedPolicyStore implements PolicyStore {
@Override
public Set<Resource> getResources() {
- Set<Resource> resources = new HashSet<>();
+ if (resources == null) {
+ resources = new HashSet<>();
- for (String id : cached.getResourcesIds()) {
- Resource cached = getStoreFactory().getResourceStore().findById(id);
+ for (String id : cached.getResourcesIds()) {
+ Resource resource = getCachedStoreFactory().getResourceStore().findById(id, cached.getResourceServerId());
- if (cached != null) {
- resources.add(cached);
+ if (resource != null) {
+ resources.add(resource);
+ }
}
}
@@ -356,13 +347,15 @@ public class CachedPolicyStore implements PolicyStore {
@Override
public Set<Scope> getScopes() {
- Set<Scope> scopes = new HashSet<>();
+ if (scopes == null) {
+ scopes = new HashSet<>();
- for (String id : cached.getScopesIds()) {
- Scope cached = getStoreFactory().getScopeStore().findById(id);
+ for (String id : cached.getScopesIds()) {
+ Scope scope = getCachedStoreFactory().getScopeStore().findById(id, cached.getResourceServerId());
- if (cached != null) {
- scopes.add(cached);
+ if (scope != null) {
+ scopes.add(scope);
+ }
}
}
@@ -392,9 +385,15 @@ public class CachedPolicyStore implements PolicyStore {
private Policy getDelegateForUpdate() {
if (this.updated == null) {
- this.updated = getDelegate().findById(getId());
+ this.updated = getDelegate().findById(getId(), cached.getResourceServerId());
if (this.updated == null) throw new IllegalStateException("Not found in database");
- transaction.whenCommit(() -> cache.remove(getCacheKeyForPolicy(getId())));
+ transaction.whenCommit(() -> {
+ cache.remove(getCacheKeyForPolicy(getId()));
+ invalidateCache(cached.getResourceServerId());
+ });
+ transaction.whenRollback(() -> {
+ cache.remove(getCacheKeyForPolicy(getId()));
+ });
}
return this.updated;
@@ -402,9 +401,16 @@ public class CachedPolicyStore implements PolicyStore {
};
}
+ private CachedStoreFactoryProvider getCachedStoreFactory() {
+ if (cachedStoreFactory == null) {
+ cachedStoreFactory = session.getProvider(CachedStoreFactoryProvider.class);
+ }
+ return cachedStoreFactory;
+ }
+
private CachedPolicy updatePolicyCache(Policy policy) {
CachedPolicy cached = new CachedPolicy(policy);
- List<Policy> cache = new ArrayList<>();
+ List<CachedPolicy> cache = new ArrayList<>();
cache.add(cached);
@@ -413,4 +419,25 @@ public class CachedPolicyStore implements PolicyStore {
return cached;
}
+ private void invalidateCache(String resourceServerId) {
+ cacheKeys.forEach(cacheKey -> cache.keySet().stream().filter(key -> key.startsWith(cacheKey + resourceServerId)).forEach(cache::remove));
+ }
+
+ private List<Policy> cacheResult(String key, Supplier<List<Policy>> provider) {
+ List<CachedPolicy> cached = cache.computeIfAbsent(key, (Function<String, List<CachedPolicy>>) o -> {
+ List<Policy> result = provider.get();
+
+ if (result.isEmpty()) {
+ return Collections.emptyList();
+ }
+
+ return result.stream().map(policy -> new CachedPolicy(policy)).collect(Collectors.toList());
+ });
+
+ if (cached == null) {
+ return Collections.emptyList();
+ }
+
+ return cached.stream().map(cachedPolicy -> createAdapter(cachedPolicy)).collect(Collectors.toList());
+ }
}
\ No newline at end of file
diff --git a/model/infinispan/src/main/java/org/keycloak/models/authorization/infinispan/CachedResourceServerStore.java b/model/infinispan/src/main/java/org/keycloak/models/authorization/infinispan/CachedResourceServerStore.java
index 2685135..ed98500 100644
--- a/model/infinispan/src/main/java/org/keycloak/models/authorization/infinispan/CachedResourceServerStore.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/authorization/infinispan/CachedResourceServerStore.java
@@ -18,6 +18,10 @@
package org.keycloak.models.authorization.infinispan;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
import org.infinispan.Cache;
import org.keycloak.authorization.model.ResourceServer;
import org.keycloak.authorization.store.ResourceServerStore;
@@ -28,16 +32,13 @@ import org.keycloak.models.authorization.infinispan.InfinispanStoreFactoryProvid
import org.keycloak.models.authorization.infinispan.entities.CachedResourceServer;
import org.keycloak.representations.idm.authorization.PolicyEnforcementMode;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map;
-
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
public class CachedResourceServerStore implements ResourceServerStore {
private static final String RS_ID_CACHE_PREFIX = "rs-id-";
+ private static final String RS_CLIENT_ID_CACHE_PREFIX = "rs-client-id-";
private final KeycloakSession session;
private final CacheTransaction transaction;
@@ -45,11 +46,12 @@ public class CachedResourceServerStore implements ResourceServerStore {
private ResourceServerStore delegate;
private final Cache<String, List> cache;
- public CachedResourceServerStore(KeycloakSession session, CacheTransaction transaction) {
+ public CachedResourceServerStore(KeycloakSession session, CacheTransaction transaction, StoreFactory storeFactory) {
this.session = session;
this.transaction = transaction;
InfinispanConnectionProvider provider = session.getProvider(InfinispanConnectionProvider.class);
this.cache = provider.getCache(InfinispanConnectionProvider.AUTHORIZATION_CACHE_NAME);
+ this.storeFactory = storeFactory;
}
@Override
@@ -64,7 +66,14 @@ public class CachedResourceServerStore implements ResourceServerStore {
@Override
public void delete(String id) {
getDelegate().delete(id);
- this.transaction.whenCommit(() -> this.cache.remove(getCacheKeyForResourceServer(id)));
+ this.transaction.whenCommit(() -> {
+ List<CachedResourceServer> servers = cache.remove(getCacheKeyForResourceServer(id));
+
+ if (servers != null) {
+ CachedResourceServer entry = servers.get(0);
+ cache.remove(getCacheKeyForResourceServerClientId(entry.getClientId()));
+ }
+ });
}
@Override
@@ -87,32 +96,31 @@ public class CachedResourceServerStore implements ResourceServerStore {
@Override
public ResourceServer findByClient(String id) {
- for (Map.Entry entry : this.cache.entrySet()) {
- String cacheKey = (String) entry.getKey();
+ String cacheKeyForResourceServer = getCacheKeyForResourceServerClientId(id);
+ List<String> cached = this.cache.get(cacheKeyForResourceServer);
- if (cacheKey.startsWith(RS_ID_CACHE_PREFIX)) {
- List<ResourceServer> cache = (List<ResourceServer>) entry.getValue();
- ResourceServer resourceServer = cache.get(0);
+ if (cached == null) {
+ ResourceServer resourceServer = getDelegate().findByClient(id);
- if (resourceServer.getClientId().equals(id)) {
- return findById(resourceServer.getId());
- }
+ if (resourceServer != null) {
+ cache.put(cacheKeyForResourceServer, Arrays.asList(resourceServer.getId()));
+ return findById(resourceServer.getId());
}
- }
- ResourceServer resourceServer = getDelegate().findByClient(id);
-
- if (resourceServer != null) {
- return findById(updateResourceServerCache(resourceServer).getId());
+ return null;
}
- return null;
+ return findById(cached.get(0));
}
private String getCacheKeyForResourceServer(String id) {
return RS_ID_CACHE_PREFIX + id;
}
+ private String getCacheKeyForResourceServerClientId(String id) {
+ return RS_CLIENT_ID_CACHE_PREFIX + id;
+ }
+
private ResourceServerStore getDelegate() {
if (this.delegate == null) {
this.delegate = getStoreFactory().getResourceServerStore();
@@ -122,10 +130,6 @@ public class CachedResourceServerStore implements ResourceServerStore {
}
private StoreFactory getStoreFactory() {
- if (this.storeFactory == null) {
- this.storeFactory = session.getProvider(StoreFactory.class);
- }
-
return this.storeFactory;
}
private ResourceServer createAdapter(ResourceServer cached) {
diff --git a/model/infinispan/src/main/java/org/keycloak/models/authorization/infinispan/CachedResourceStore.java b/model/infinispan/src/main/java/org/keycloak/models/authorization/infinispan/CachedResourceStore.java
index 8696d8d..92adf6c 100644
--- a/model/infinispan/src/main/java/org/keycloak/models/authorization/infinispan/CachedResourceStore.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/authorization/infinispan/CachedResourceStore.java
@@ -18,6 +18,16 @@
package org.keycloak.models.authorization.infinispan;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.function.Supplier;
+import java.util.stream.Collectors;
+
import org.infinispan.Cache;
import org.keycloak.authorization.model.Resource;
import org.keycloak.authorization.model.ResourceServer;
@@ -28,14 +38,7 @@ import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.authorization.infinispan.InfinispanStoreFactoryProvider.CacheTransaction;
import org.keycloak.models.authorization.infinispan.entities.CachedResource;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
-import java.util.Map.Entry;
-import java.util.Set;
-import java.util.stream.Collectors;
+import org.keycloak.models.cache.authorization.CachedStoreFactoryProvider;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
@@ -43,60 +46,72 @@ import java.util.stream.Collectors;
public class CachedResourceStore implements ResourceStore {
private static final String RESOURCE_ID_CACHE_PREFIX = "rsc-id-";
- private static final String RESOURCE_OWNER_CACHE_PREFIX = "rsc-owner-";
+ private static final String RESOURCE_NAME_CACHE_PREFIX = "rsc-name-";
private final KeycloakSession session;
private final CacheTransaction transaction;
+ private final List<String> cacheKeys;
private StoreFactory storeFactory;
private ResourceStore delegate;
- private final Cache<String, List> cache;
+ private final Cache<String, List<CachedResource>> cache;
- public CachedResourceStore(KeycloakSession session, CacheTransaction transaction) {
+ public CachedResourceStore(KeycloakSession session, CacheTransaction transaction, StoreFactory storeFactory) {
this.session = session;
InfinispanConnectionProvider provider = session.getProvider(InfinispanConnectionProvider.class);
this.cache = provider.getCache(InfinispanConnectionProvider.AUTHORIZATION_CACHE_NAME);
this.transaction = transaction;
+ cacheKeys = new ArrayList<>();
+ cacheKeys.add("findByOwner");
+ cacheKeys.add("findByUri");
+ this.storeFactory = storeFactory;
}
@Override
public Resource create(String name, ResourceServer resourceServer, String owner) {
Resource resource = getDelegate().create(name, getStoreFactory().getResourceServerStore().findById(resourceServer.getId()), owner);
- this.transaction.whenRollback(() -> cache.remove(getCacheKeyForResource(resource.getId())));
+ this.transaction.whenRollback(() -> {
+ cache.remove(getCacheKeyForResource(resource.getId()));
+ });
+
+ this.transaction.whenCommit(() -> {
+ invalidateCache(resourceServer.getId());
+ getCachedStoreFactory().getPolicyStore().notifyChange(resource);
+ });
return createAdapter(new CachedResource(resource));
}
@Override
public void delete(String id) {
- List<CachedResource> removed = this.cache.remove(getCacheKeyForResource(id));
-
- if (removed != null) {
- CachedResource cachedResource = removed.get(0);
- List<String> byOwner = this.cache.get(getResourceOwnerCacheKey(cachedResource.getOwner()));
-
- if (byOwner != null) {
- byOwner.remove(id);
+ Resource resource = findById(id, null);
+ if (resource == null) {
+ return;
+ }
+ ResourceServer resourceServer = resource.getResourceServer();
+ getDelegate().delete(id);
+ this.transaction.whenCommit(() -> {
+ List<CachedResource> resources = cache.remove(getCacheKeyForResource(id));
- if (byOwner.isEmpty()) {
- this.cache.remove(getResourceOwnerCacheKey(cachedResource.getOwner()));
- }
+ if (resources != null) {
+ CachedResource entry = resources.get(0);
+ cache.remove(getCacheKeyForResourceName(entry.getName(), entry.getResourceServerId()));
}
- }
- getDelegate().delete(id);
+ invalidateCache(resourceServer.getId());
+ getCachedStoreFactory().getPolicyStore().notifyChange(resource);
+ });
}
@Override
- public Resource findById(String id) {
+ public Resource findById(String id, String resourceServerId) {
String cacheKeyForResource = getCacheKeyForResource(id);
List<CachedResource> cached = this.cache.get(cacheKeyForResource);
if (cached == null) {
- Resource resource = getDelegate().findById(id);
+ Resource resource = getDelegate().findById(id, resourceServerId);
if (resource != null) {
- updateCachedIds(getResourceOwnerCacheKey(resource.getOwner()), resource, false);
return createAdapter(updateResourceCache(resource));
}
@@ -107,20 +122,18 @@ public class CachedResourceStore implements ResourceStore {
}
@Override
- public List<Resource> findByOwner(String ownerId) {
-
- for (Resource resource : getDelegate().findByOwner(ownerId)) {
- updateCachedIds(getResourceOwnerCacheKey(ownerId), resource, true);
- }
+ public List<Resource> findByOwner(String ownerId, String resourceServerId) {
+ return cacheResult(new StringBuilder("findByOwner").append(resourceServerId).append(ownerId).toString(), () -> getDelegate().findByOwner(ownerId, resourceServerId));
+ }
- return ((List<String>) this.cache.getOrDefault(getResourceOwnerCacheKey(ownerId), Collections.emptyList())).stream().map(this::findById)
- .filter(resource -> resource != null)
- .collect(Collectors.toList());
+ @Override
+ public List<Resource> findByUri(String uri, String resourceServerId) {
+ return cacheResult(new StringBuilder("findByUri").append(resourceServerId).append(uri).toString(), () -> getDelegate().findByUri(uri, resourceServerId));
}
@Override
public List<Resource> findByResourceServer(String resourceServerId) {
- return getDelegate().findByResourceServer(resourceServerId).stream().map(resource -> findById(resource.getId())).collect(Collectors.toList());
+ return getDelegate().findByResourceServer(resourceServerId);
}
@Override
@@ -129,43 +142,42 @@ public class CachedResourceStore implements ResourceStore {
}
@Override
- public List<Resource> findByScope(String... id) {
- return getDelegate().findByScope(id).stream().map(resource -> findById(resource.getId())).collect(Collectors.toList());
+ public List<Resource> findByScope(List<String> id, String resourceServerId) {
+ return getDelegate().findByScope(id, resourceServerId);
}
@Override
public Resource findByName(String name, String resourceServerId) {
- for (Entry entry : this.cache.entrySet()) {
- String cacheKey = (String) entry.getKey();
+ String cacheKeyForResource = getCacheKeyForResourceName(name, resourceServerId);
+ List<CachedResource> cached = this.cache.get(cacheKeyForResource);
- if (cacheKey.startsWith(RESOURCE_ID_CACHE_PREFIX)) {
- List<CachedResource> value = (List<CachedResource>) entry.getValue();
- CachedResource resource = value.get(0);
+ if (cached == null) {
+ Resource resource = getDelegate().findByName(name, resourceServerId);
- if (resource.getResourceServerId().equals(resourceServerId) && resource.getName().equals(name)) {
- return findById(resource.getId());
- }
+ if (resource != null) {
+ cache.put(cacheKeyForResource, Arrays.asList(new CachedResource(resource)));
+ return findById(resource.getId(), resourceServerId);
}
- }
-
- Resource resource = getDelegate().findByName(name, resourceServerId);
- if (resource != null) {
- return findById(updateResourceCache(resource).getId());
+ return null;
}
- return null;
+ return createAdapter(cached.get(0));
}
@Override
- public List<Resource> findByType(String type) {
- return getDelegate().findByType(type).stream().map(resource -> findById(resource.getId())).collect(Collectors.toList());
+ public List<Resource> findByType(String type, String resourceServerId) {
+ return getDelegate().findByType(type, resourceServerId);
}
private String getCacheKeyForResource(String id) {
return RESOURCE_ID_CACHE_PREFIX + id;
}
+ private String getCacheKeyForResourceName(String name, String resourceServerId) {
+ return RESOURCE_NAME_CACHE_PREFIX + name + "-" + resourceServerId;
+ }
+
private ResourceStore getDelegate() {
if (this.delegate == null) {
this.delegate = getStoreFactory().getResourceStore();
@@ -175,10 +187,6 @@ public class CachedResourceStore implements ResourceStore {
}
private StoreFactory getStoreFactory() {
- if (this.storeFactory == null) {
- this.storeFactory = session.getProvider(StoreFactory.class);
- }
-
return this.storeFactory;
}
@@ -228,13 +236,15 @@ public class CachedResourceStore implements ResourceStore {
@Override
public List<Scope> getScopes() {
- List<Scope> scopes = new ArrayList<>();
+ if (scopes == null) {
+ scopes = new ArrayList<>();
- for (String id : cached.getScopesIds()) {
- Scope cached = getStoreFactory().getScopeStore().findById(id);
+ for (String id : cached.getScopesIds()) {
+ Scope scope = getCachedStoreFactory().getScopeStore().findById(id, cached.getResourceServerId());
- if (cached != null) {
- scopes.add(cached);
+ if (scope != null) {
+ scopes.add(scope);
+ }
}
}
@@ -254,7 +264,7 @@ public class CachedResourceStore implements ResourceStore {
@Override
public ResourceServer getResourceServer() {
- return getStoreFactory().getResourceServerStore().findById(cached.getResourceServerId());
+ return getCachedStoreFactory().getResourceServerStore().findById(cached.getResourceServerId());
}
@Override
@@ -264,15 +274,22 @@ public class CachedResourceStore implements ResourceStore {
@Override
public void updateScopes(Set<Scope> scopes) {
- getDelegateForUpdate().updateScopes(scopes.stream().map(scope -> getStoreFactory().getScopeStore().findById(scope.getId())).collect(Collectors.toSet()));
+ getDelegateForUpdate().updateScopes(scopes.stream().map(scope -> getStoreFactory().getScopeStore().findById(scope.getId(), cached.getResourceServerId())).collect(Collectors.toSet()));
cached.updateScopes(scopes);
}
private Resource getDelegateForUpdate() {
if (this.updated == null) {
- this.updated = getDelegate().findById(getId());
+ this.updated = getDelegate().findById(getId(), cached.getResourceServerId());
if (this.updated == null) throw new IllegalStateException("Not found in database");
- transaction.whenCommit(() -> cache.remove(getCacheKeyForResource(getId())));
+ transaction.whenCommit(() -> {
+ cache.remove(getCacheKeyForResource(cached.getId()));
+ invalidateCache(cached.getResourceServerId());
+ getCachedStoreFactory().getPolicyStore().notifyChange(updated);
+ });
+ transaction.whenRollback(() -> {
+ cache.remove(getCacheKeyForResource(cached.getId()));
+ });
}
return this.updated;
@@ -280,6 +297,10 @@ public class CachedResourceStore implements ResourceStore {
};
}
+ private CachedStoreFactoryProvider getCachedStoreFactory() {
+ return session.getProvider(CachedStoreFactoryProvider.class);
+ }
+
private CachedResource updateResourceCache(Resource resource) {
CachedResource cached = new CachedResource(resource);
List cache = new ArrayList<>();
@@ -291,23 +312,25 @@ public class CachedResourceStore implements ResourceStore {
return cached;
}
- private void updateCachedIds(String cacheKey, Resource resource, boolean create) {
- List<String> cached = this.cache.get(cacheKey);
+ private List<Resource> cacheResult(String key, Supplier<List<Resource>> provider) {
+ List<CachedResource> cached = cache.computeIfAbsent(key, (Function<String, List<CachedResource>>) o -> {
+ List<Resource> result = provider.get();
- if (cached == null) {
- if (!create) {
- return;
+ if (result.isEmpty()) {
+ return null;
}
- cached = new ArrayList<>();
- this.cache.put(cacheKey, cached);
- }
- if (cached != null && !cached.contains(resource.getId())) {
- cached.add(resource.getId());
+ return result.stream().map(resource -> new CachedResource(resource)).collect(Collectors.toList());
+ });
+
+ if (cached == null) {
+ return Collections.emptyList();
}
+
+ return cached.stream().map(this::createAdapter).collect(Collectors.toList());
}
- private String getResourceOwnerCacheKey(String ownerId) {
- return RESOURCE_OWNER_CACHE_PREFIX + ownerId;
+ private void invalidateCache(String resourceServerId) {
+ cacheKeys.forEach(cacheKey -> cache.keySet().stream().filter(key -> key.startsWith(cacheKey + resourceServerId)).forEach(cache::remove));
}
}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/authorization/infinispan/CachedScopeStore.java b/model/infinispan/src/main/java/org/keycloak/models/authorization/infinispan/CachedScopeStore.java
index f86a7d1..5be00b4 100644
--- a/model/infinispan/src/main/java/org/keycloak/models/authorization/infinispan/CachedScopeStore.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/authorization/infinispan/CachedScopeStore.java
@@ -18,7 +18,13 @@
package org.keycloak.models.authorization.infinispan;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+
import org.infinispan.Cache;
+import org.keycloak.authorization.model.Resource;
import org.keycloak.authorization.model.ResourceServer;
import org.keycloak.authorization.model.Scope;
import org.keycloak.authorization.store.ScopeStore;
@@ -27,11 +33,7 @@ import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.authorization.infinispan.InfinispanStoreFactoryProvider.CacheTransaction;
import org.keycloak.models.authorization.infinispan.entities.CachedScope;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map;
-import java.util.Map.Entry;
+import org.keycloak.models.cache.authorization.CachedStoreFactoryProvider;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
@@ -39,6 +41,7 @@ import java.util.Map.Entry;
public class CachedScopeStore implements ScopeStore {
private static final String SCOPE_ID_CACHE_PREFIX = "scp-id-";
+ private static final String SCOPE_NAME_CACHE_PREFIX = "scp-name-";
private final Cache<String, List> cache;
private final KeycloakSession session;
@@ -46,11 +49,12 @@ public class CachedScopeStore implements ScopeStore {
private ScopeStore delegate;
private StoreFactory storeFactory;
- public CachedScopeStore(KeycloakSession session, CacheTransaction transaction) {
+ public CachedScopeStore(KeycloakSession session, CacheTransaction transaction, StoreFactory storeFactory) {
this.session = session;
this.transaction = transaction;
InfinispanConnectionProvider provider = session.getProvider(InfinispanConnectionProvider.class);
this.cache = provider.getCache(InfinispanConnectionProvider.AUTHORIZATION_CACHE_NAME);
+ this.storeFactory = storeFactory;
}
@Override
@@ -58,23 +62,39 @@ public class CachedScopeStore implements ScopeStore {
Scope scope = getDelegate().create(name, getStoreFactory().getResourceServerStore().findById(resourceServer.getId()));
this.transaction.whenRollback(() -> cache.remove(getCacheKeyForScope(scope.getId())));
+ this.transaction.whenCommit(() -> {
+ getCachedStoreFactory().getPolicyStore().notifyChange(scope);
+ });
return createAdapter(new CachedScope(scope));
}
@Override
public void delete(String id) {
+ Scope scope = findById(id, null);
+ if (scope == null) {
+ return;
+ }
getDelegate().delete(id);
- this.transaction.whenCommit(() -> cache.remove(getCacheKeyForScope(id)));
+ this.transaction.whenCommit(() -> {
+ List<CachedScope> scopes = cache.remove(getCacheKeyForScope(id));
+
+ if (scopes != null) {
+ CachedScope entry = scopes.get(0);
+ cache.remove(getCacheKeyForScopeName(entry.getName(), entry.getResourceServerId()));
+ }
+
+ getCachedStoreFactory().getPolicyStore().notifyChange(scope);
+ });
}
@Override
- public Scope findById(String id) {
+ public Scope findById(String id, String resourceServerId) {
String cacheKeyForScope = getCacheKeyForScope(id);
List<CachedScope> cached = this.cache.get(cacheKeyForScope);
if (cached == null) {
- Scope scope = getDelegate().findById(id);
+ Scope scope = getDelegate().findById(id, resourceServerId);
if (scope != null) {
return createAdapter(updateScopeCache(scope));
@@ -88,26 +108,21 @@ public class CachedScopeStore implements ScopeStore {
@Override
public Scope findByName(String name, String resourceServerId) {
- for (Entry entry : this.cache.entrySet()) {
- String cacheKey = (String) entry.getKey();
+ String cacheKeyForScope = getCacheKeyForScopeName(name, resourceServerId);
+ List<String> cached = this.cache.get(cacheKeyForScope);
- if (cacheKey.startsWith(SCOPE_ID_CACHE_PREFIX)) {
- List<CachedScope> cache = (List<CachedScope>) entry.getValue();
- CachedScope scope = cache.get(0);
+ if (cached == null) {
+ Scope scope = getDelegate().findByName(name, resourceServerId);
- if (scope.getResourceServerId().equals(resourceServerId) && scope.getName().equals(name)) {
- return findById(scope.getId());
- }
+ if (scope != null) {
+ cache.put(cacheKeyForScope, Arrays.asList(scope.getId()));
+ return findById(scope.getId(), resourceServerId);
}
- }
-
- Scope scope = getDelegate().findByName(name, resourceServerId);
- if (scope != null) {
- return findById(updateScopeCache(scope).getId());
+ return null;
}
- return null;
+ return findById(cached.get(0), resourceServerId);
}
@Override
@@ -124,6 +139,10 @@ public class CachedScopeStore implements ScopeStore {
return SCOPE_ID_CACHE_PREFIX + id;
}
+ private String getCacheKeyForScopeName(String name, String resourceServerId) {
+ return SCOPE_NAME_CACHE_PREFIX + name + "-" + resourceServerId;
+ }
+
private ScopeStore getDelegate() {
if (this.delegate == null) {
this.delegate = getStoreFactory().getScopeStore();
@@ -133,10 +152,6 @@ public class CachedScopeStore implements ScopeStore {
}
private StoreFactory getStoreFactory() {
- if (this.storeFactory == null) {
- this.storeFactory = session.getProvider(StoreFactory.class);
- }
-
return this.storeFactory;
}
@@ -174,14 +189,20 @@ public class CachedScopeStore implements ScopeStore {
@Override
public ResourceServer getResourceServer() {
- return getStoreFactory().getResourceServerStore().findById(cached.getResourceServerId());
+ return getCachedStoreFactory().getResourceServerStore().findById(cached.getResourceServerId());
}
private Scope getDelegateForUpdate() {
if (this.updated == null) {
- this.updated = getDelegate().findById(getId());
+ this.updated = getDelegate().findById(getId(), cached.getResourceServerId());
if (this.updated == null) throw new IllegalStateException("Not found in database");
- transaction.whenCommit(() -> cache.remove(getCacheKeyForScope(getId())));
+ transaction.whenCommit(() -> {
+ cache.remove(getCacheKeyForScope(getId()));
+ getCachedStoreFactory().getPolicyStore().notifyChange(updated);
+ });
+ transaction.whenRollback(() -> {
+ cache.remove(getCacheKeyForScope(cached.getId()));
+ });
}
return this.updated;
@@ -189,6 +210,10 @@ public class CachedScopeStore implements ScopeStore {
};
}
+ private CachedStoreFactoryProvider getCachedStoreFactory() {
+ return session.getProvider(CachedStoreFactoryProvider.class);
+ }
+
private CachedScope updateScopeCache(Scope scope) {
CachedScope cached = new CachedScope(scope);
diff --git a/model/infinispan/src/main/java/org/keycloak/models/authorization/infinispan/InfinispanStoreFactoryProvider.java b/model/infinispan/src/main/java/org/keycloak/models/authorization/infinispan/InfinispanStoreFactoryProvider.java
index 56a385f..ff66da6 100644
--- a/model/infinispan/src/main/java/org/keycloak/models/authorization/infinispan/InfinispanStoreFactoryProvider.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/authorization/infinispan/InfinispanStoreFactoryProvider.java
@@ -22,6 +22,7 @@ import org.keycloak.authorization.store.PolicyStore;
import org.keycloak.authorization.store.ResourceServerStore;
import org.keycloak.authorization.store.ResourceStore;
import org.keycloak.authorization.store.ScopeStore;
+import org.keycloak.authorization.store.StoreFactory;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakTransaction;
import org.keycloak.models.cache.authorization.CachedStoreFactoryProvider;
@@ -36,31 +37,41 @@ public class InfinispanStoreFactoryProvider implements CachedStoreFactoryProvide
private final KeycloakSession session;
private final CacheTransaction transaction;
+ private final StoreFactory storeFactory;
+ private final CachedResourceStore resourceStore;
+ private final CachedScopeStore scopeStore;
+ private final CachedPolicyStore policyStore;
+ private ResourceServerStore resourceServerStore;
InfinispanStoreFactoryProvider(KeycloakSession delegate) {
this.session = delegate;
this.transaction = new CacheTransaction();
this.session.getTransactionManager().enlistAfterCompletion(transaction);
+ storeFactory = this.session.getProvider(StoreFactory.class);
+ resourceStore = new CachedResourceStore(this.session, this.transaction, storeFactory);
+ resourceServerStore = new CachedResourceServerStore(this.session, this.transaction, storeFactory);
+ scopeStore = new CachedScopeStore(this.session, this.transaction, storeFactory);
+ policyStore = new CachedPolicyStore(this.session, this.transaction, storeFactory);
}
@Override
public ResourceStore getResourceStore() {
- return new CachedResourceStore(this.session, this.transaction);
+ return resourceStore;
}
@Override
public ResourceServerStore getResourceServerStore() {
- return new CachedResourceServerStore(this.session, this.transaction);
+ return resourceServerStore;
}
@Override
public ScopeStore getScopeStore() {
- return new CachedScopeStore(this.session, this.transaction);
+ return scopeStore;
}
@Override
public PolicyStore getPolicyStore() {
- return new CachedPolicyStore(this.session, this.transaction);
+ return policyStore;
}
@Override
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedRealm.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedRealm.java
index 48689e6..5e4b579 100755
--- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedRealm.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedRealm.java
@@ -34,9 +34,6 @@ import org.keycloak.models.RealmModel;
import org.keycloak.models.RequiredActionProviderModel;
import org.keycloak.models.RequiredCredentialModel;
-import java.security.PrivateKey;
-import java.security.PublicKey;
-import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
@@ -61,6 +58,8 @@ public class CachedRealm extends AbstractExtendableRevisioned {
protected boolean registrationEmailAsUsername;
protected boolean rememberMe;
protected boolean verifyEmail;
+ protected boolean loginWithEmailAllowed;
+ protected boolean duplicateEmailsAllowed;
protected boolean resetPasswordAllowed;
protected boolean identityFederationEnabled;
protected boolean editUsernameAllowed;
@@ -150,6 +149,8 @@ public class CachedRealm extends AbstractExtendableRevisioned {
registrationEmailAsUsername = model.isRegistrationEmailAsUsername();
rememberMe = model.isRememberMe();
verifyEmail = model.isVerifyEmail();
+ loginWithEmailAllowed = model.isLoginWithEmailAllowed();
+ duplicateEmailsAllowed = model.isDuplicateEmailsAllowed();
resetPasswordAllowed = model.isResetPasswordAllowed();
identityFederationEnabled = model.isIdentityFederationEnabled();
editUsernameAllowed = model.isEditUsernameAllowed();
@@ -340,6 +341,14 @@ public class CachedRealm extends AbstractExtendableRevisioned {
public boolean isVerifyEmail() {
return verifyEmail;
}
+
+ public boolean isLoginWithEmailAllowed() {
+ return loginWithEmailAllowed;
+ }
+
+ public boolean isDuplicateEmailsAllowed() {
+ return duplicateEmailsAllowed;
+ }
public boolean isResetPasswordAllowed() {
return resetPasswordAllowed;
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java
index 2cca447..0b36d67 100755
--- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java
@@ -306,6 +306,30 @@ public class RealmAdapter implements CachedRealmModel {
}
@Override
+ public boolean isLoginWithEmailAllowed() {
+ if (isUpdated()) return updated.isLoginWithEmailAllowed();
+ return cached.isLoginWithEmailAllowed();
+ }
+
+ @Override
+ public void setLoginWithEmailAllowed(boolean loginWithEmailAllowed) {
+ getDelegateForUpdate();
+ updated.setLoginWithEmailAllowed(loginWithEmailAllowed);
+ }
+
+ @Override
+ public boolean isDuplicateEmailsAllowed() {
+ if (isUpdated()) return updated.isDuplicateEmailsAllowed();
+ return cached.isDuplicateEmailsAllowed();
+ }
+
+ @Override
+ public void setDuplicateEmailsAllowed(boolean duplicateEmailsAllowed) {
+ getDelegateForUpdate();
+ updated.setDuplicateEmailsAllowed(duplicateEmailsAllowed);
+ }
+
+ @Override
public boolean isResetPasswordAllowed() {
if (isUpdated()) return updated.isResetPasswordAllowed();
return cached.isResetPasswordAllowed();
diff --git a/model/jpa/src/main/java/org/keycloak/authorization/jpa/entities/PolicyEntity.java b/model/jpa/src/main/java/org/keycloak/authorization/jpa/entities/PolicyEntity.java
index e1ba326..648b1b3 100644
--- a/model/jpa/src/main/java/org/keycloak/authorization/jpa/entities/PolicyEntity.java
+++ b/model/jpa/src/main/java/org/keycloak/authorization/jpa/entities/PolicyEntity.java
@@ -18,11 +18,10 @@
package org.keycloak.authorization.jpa.entities;
-import org.keycloak.authorization.model.Policy;
-import org.keycloak.authorization.model.Resource;
-import org.keycloak.authorization.model.Scope;
-import org.keycloak.representations.idm.authorization.DecisionStrategy;
-import org.keycloak.representations.idm.authorization.Logic;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
import javax.persistence.Access;
import javax.persistence.AccessType;
@@ -34,15 +33,17 @@ import javax.persistence.FetchType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.JoinTable;
-import javax.persistence.ManyToMany;
import javax.persistence.ManyToOne;
import javax.persistence.MapKeyColumn;
+import javax.persistence.OneToMany;
import javax.persistence.Table;
import javax.persistence.UniqueConstraint;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Map;
-import java.util.Set;
+
+import org.keycloak.authorization.model.Policy;
+import org.keycloak.authorization.model.Resource;
+import org.keycloak.authorization.model.Scope;
+import org.keycloak.representations.idm.authorization.DecisionStrategy;
+import org.keycloak.representations.idm.authorization.Logic;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
@@ -79,19 +80,19 @@ public class PolicyEntity implements Policy {
@CollectionTable(name="POLICY_CONFIG", joinColumns={ @JoinColumn(name="POLICY_ID") })
private Map<String, String> config = new HashMap();
- @ManyToOne(optional = false)
+ @ManyToOne(optional = false, fetch = FetchType.LAZY)
@JoinColumn(name = "RESOURCE_SERVER_ID")
private ResourceServerEntity resourceServer;
- @ManyToMany(fetch = FetchType.LAZY, cascade = {})
+ @OneToMany(fetch = FetchType.LAZY, cascade = {})
@JoinTable(name = "ASSOCIATED_POLICY", joinColumns = @JoinColumn(name = "POLICY_ID"), inverseJoinColumns = @JoinColumn(name = "ASSOCIATED_POLICY_ID"))
private Set<PolicyEntity> associatedPolicies = new HashSet<>();
- @ManyToMany(fetch = FetchType.LAZY, cascade = {})
+ @OneToMany(fetch = FetchType.LAZY, cascade = {})
@JoinTable(name = "RESOURCE_POLICY", joinColumns = @JoinColumn(name = "POLICY_ID"), inverseJoinColumns = @JoinColumn(name = "RESOURCE_ID"))
private Set<ResourceEntity> resources = new HashSet<>();
- @ManyToMany(fetch = FetchType.EAGER, cascade = {})
+ @OneToMany(fetch = FetchType.EAGER, cascade = {})
@JoinTable(name = "SCOPE_POLICY", joinColumns = @JoinColumn(name = "POLICY_ID"), inverseJoinColumns = @JoinColumn(name = "SCOPE_ID"))
private Set<ScopeEntity> scopes = new HashSet<>();
diff --git a/model/jpa/src/main/java/org/keycloak/authorization/jpa/entities/ResourceEntity.java b/model/jpa/src/main/java/org/keycloak/authorization/jpa/entities/ResourceEntity.java
index 7cb1a6f..29b5740 100644
--- a/model/jpa/src/main/java/org/keycloak/authorization/jpa/entities/ResourceEntity.java
+++ b/model/jpa/src/main/java/org/keycloak/authorization/jpa/entities/ResourceEntity.java
@@ -67,7 +67,7 @@ public class ResourceEntity implements Resource {
@Column(name = "OWNER")
private String owner;
- @ManyToOne(optional = false)
+ @ManyToOne(optional = false, fetch = FetchType.LAZY)
@JoinColumn(name = "RESOURCE_SERVER_ID")
private ResourceServerEntity resourceServer;
diff --git a/model/jpa/src/main/java/org/keycloak/authorization/jpa/entities/ScopeEntity.java b/model/jpa/src/main/java/org/keycloak/authorization/jpa/entities/ScopeEntity.java
index 99f8b41..9f2c3b8 100644
--- a/model/jpa/src/main/java/org/keycloak/authorization/jpa/entities/ScopeEntity.java
+++ b/model/jpa/src/main/java/org/keycloak/authorization/jpa/entities/ScopeEntity.java
@@ -57,7 +57,7 @@ public class ScopeEntity implements Scope {
@Column(name = "ICON_URI")
private String iconUri;
- @ManyToOne(optional = false)
+ @ManyToOne(optional = false, fetch = FetchType.LAZY)
@JoinColumn(name = "RESOURCE_SERVER_ID")
private ResourceServerEntity resourceServer;
diff --git a/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAPolicyStore.java b/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAPolicyStore.java
index b57cd1e..544018d 100644
--- a/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAPolicyStore.java
+++ b/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAPolicyStore.java
@@ -19,8 +19,10 @@ package org.keycloak.authorization.jpa.store;
import org.keycloak.authorization.jpa.entities.PolicyEntity;
import org.keycloak.authorization.jpa.entities.ResourceServerEntity;
+import org.keycloak.authorization.jpa.entities.ScopeEntity;
import org.keycloak.authorization.model.Policy;
import org.keycloak.authorization.model.ResourceServer;
+import org.keycloak.authorization.model.Scope;
import org.keycloak.authorization.store.PolicyStore;
import org.keycloak.models.utils.KeycloakModelUtils;
@@ -68,17 +70,30 @@ public class JPAPolicyStore implements PolicyStore {
@Override
public void delete(String id) {
- Policy policy = findById(id);
+ Policy policy = entityManager.find(PolicyEntity.class, id);
if (policy != null) {
- getEntityManager().remove(policy);
+ this.entityManager.remove(policy);
}
}
@Override
- public Policy findById(String id) {
- return getEntityManager().find(PolicyEntity.class, id);
+ public Policy findById(String id, String resourceServerId) {
+ if (id == null) {
+ return null;
+ }
+
+ if (resourceServerId == null) {
+ return entityManager.find(PolicyEntity.class, id);
+ }
+
+ Query query = entityManager.createQuery("from PolicyEntity where resourceServer.id = :serverId and id = :id");
+
+ query.setParameter("serverId", resourceServerId);
+ query.setParameter("id", id);
+
+ return entityManager.find(PolicyEntity.class, id);
}
@Override
@@ -142,32 +157,23 @@ public class JPAPolicyStore implements PolicyStore {
}
@Override
- public List<Policy> findByResource(final String resourceId) {
- Query query = getEntityManager().createQuery("select p from PolicyEntity p inner join p.resources r where r.id = :resourceId");
+ public List<Policy> findByResource(final String resourceId, String resourceServerId) {
+ Query query = getEntityManager().createQuery("select p from PolicyEntity p inner join p.resources r where p.resourceServer.id = :serverId and (r.resourceServer.id = :serverId and r.id = :resourceId)");
query.setParameter("resourceId", resourceId);
+ query.setParameter("serverId", resourceServerId);
return query.getResultList();
}
@Override
public List<Policy> findByResourceType(final String resourceType, String resourceServerId) {
- List<Policy> policies = new ArrayList<>();
- Query query = getEntityManager().createQuery("from PolicyEntity where resourceServer.id = :serverId");
+ Query query = getEntityManager().createQuery("select p from PolicyEntity p inner join p.config c where p.resourceServer.id = :serverId and KEY(c) = 'defaultResourceType' and c = :type");
query.setParameter("serverId", resourceServerId);
+ query.setParameter("type", resourceType);
- List<Policy> models = query.getResultList();
-
- for (Policy policy : models) {
- String defaultType = policy.getConfig().get("defaultResourceType");
-
- if (defaultType != null && defaultType.equals(resourceType) && policy.getResources().isEmpty()) {
- policies.add(policy);
- }
- }
-
- return policies;
+ return query.getResultList();
}
@Override
@@ -177,7 +183,7 @@ public class JPAPolicyStore implements PolicyStore {
}
// Use separate subquery to handle DB2 and MSSSQL
- Query query = getEntityManager().createQuery("select pe from PolicyEntity pe where pe.id IN (select p.id from PolicyEntity p inner join p.scopes s where p.resourceServer.id = :serverId and s.id in (:scopeIds) and p.resources is empty group by p.id) order by pe.name");
+ Query query = getEntityManager().createQuery("select pe from PolicyEntity pe where pe.resourceServer.id = :serverId and pe.id IN (select p.id from ScopeEntity s inner join s.policies p where s.resourceServer.id = :serverId and (p.resourceServer.id = :serverId and p.type = 'scope' and s.id in (:scopeIds)))");
query.setParameter("serverId", resourceServerId);
query.setParameter("scopeIds", scopeIds);
@@ -186,19 +192,21 @@ public class JPAPolicyStore implements PolicyStore {
}
@Override
- public List<Policy> findByType(String type) {
- Query query = getEntityManager().createQuery("select p from PolicyEntity p where p.type = :type");
+ public List<Policy> findByType(String type, String resourceServerId) {
+ Query query = getEntityManager().createQuery("select p from PolicyEntity p where p.resourceServer.id = :serverId and p.type = :type");
+ query.setParameter("serverId", resourceServerId);
query.setParameter("type", type);
return query.getResultList();
}
@Override
- public List<Policy> findDependentPolicies(String policyId) {
- Query query = getEntityManager().createQuery("select p from PolicyEntity p inner join p.associatedPolicies ap where ap.id in (:policyId)");
+ public List<Policy> findDependentPolicies(String policyId, String resourceServerId) {
+ Query query = getEntityManager().createQuery("select p from PolicyEntity p inner join p.associatedPolicies ap where p.resourceServer.id = :serverId and (ap.resourceServer.id = :serverId and ap.id = :policyId)");
- query.setParameter("policyId", Arrays.asList(policyId));
+ query.setParameter("serverId", resourceServerId);
+ query.setParameter("policyId", policyId);
return query.getResultList();
}
diff --git a/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAResourceStore.java b/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAResourceStore.java
index 6d00bb6..0b04388 100644
--- a/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAResourceStore.java
+++ b/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAResourceStore.java
@@ -66,7 +66,7 @@ public class JPAResourceStore implements ResourceStore {
@Override
public void delete(String id) {
- Resource resource = findById(id);
+ Resource resource = entityManager.find(ResourceEntity.class, id);
resource.getScopes().clear();
@@ -76,19 +76,39 @@ public class JPAResourceStore implements ResourceStore {
}
@Override
- public Resource findById(String id) {
+ public Resource findById(String id, String resourceServerId) {
if (id == null) {
return null;
}
+ if (resourceServerId == null) {
+ return entityManager.find(ResourceEntity.class, id);
+ }
+
+ Query query = entityManager.createQuery("from ResourceEntity where resourceServer.id = :serverId and id = :id");
+
+ query.setParameter("serverId", resourceServerId);
+ query.setParameter("id", id);
+
return entityManager.find(ResourceEntity.class, id);
}
@Override
- public List<Resource> findByOwner(String ownerId) {
- Query query = entityManager.createQuery("from ResourceEntity where owner = :ownerId");
+ public List<Resource> findByOwner(String ownerId, String resourceServerId) {
+ Query query = entityManager.createQuery("from ResourceEntity where resourceServer.id = :serverId and owner = :ownerId");
query.setParameter("ownerId", ownerId);
+ query.setParameter("serverId", resourceServerId);
+
+ return query.getResultList();
+ }
+
+ @Override
+ public List<Resource> findByUri(String uri, String resourceServerId) {
+ Query query = entityManager.createQuery("from ResourceEntity where resourceServer.id = :serverId and uri = :uri");
+
+ query.setParameter("uri", uri);
+ query.setParameter("serverId", resourceServerId);
return query.getResultList();
}
@@ -112,7 +132,9 @@ public class JPAResourceStore implements ResourceStore {
predicates.add(builder.equal(root.get("resourceServer").get("id"), resourceServerId));
attributes.forEach((name, value) -> {
- if ("scope".equals(name)) {
+ if ("id".equals(name)) {
+ predicates.add(root.get(name).in(value));
+ } else if ("scope".equals(name)) {
predicates.add(root.join("scopes").get("id").in(value));
} else {
predicates.add(builder.like(builder.lower(root.get(name)), "%" + value[0].toLowerCase() + "%"));
@@ -134,10 +156,11 @@ public class JPAResourceStore implements ResourceStore {
}
@Override
- public List<Resource> findByScope(String... id) {
- Query query = entityManager.createQuery("select r from ResourceEntity r inner join r.scopes s where s.id in (:scopeIds)");
+ public List<Resource> findByScope(List<String> id, String resourceServerId) {
+ Query query = entityManager.createQuery("select r from ResourceEntity r inner join r.scopes s where r.resourceServer.id = :serverId and (s.resourceServer.id = :serverId and s.id in (:scopeIds))");
- query.setParameter("scopeIds", Arrays.asList(id));
+ query.setParameter("scopeIds", id);
+ query.setParameter("serverId", resourceServerId);
return query.getResultList();
}
@@ -159,10 +182,11 @@ public class JPAResourceStore implements ResourceStore {
}
@Override
- public List<Resource> findByType(String type) {
- Query query = entityManager.createQuery("from ResourceEntity where type = :type");
+ public List<Resource> findByType(String type, String resourceServerId) {
+ Query query = entityManager.createQuery("from ResourceEntity r where r.resourceServer.id = :serverId and type = :type");
query.setParameter("type", type);
+ query.setParameter("serverId", resourceServerId);
return query.getResultList();
}
diff --git a/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAScopeStore.java b/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAScopeStore.java
index d468314..031eb4a 100644
--- a/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAScopeStore.java
+++ b/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAScopeStore.java
@@ -17,12 +17,9 @@
*/
package org.keycloak.authorization.jpa.store;
-import org.keycloak.authorization.jpa.entities.ResourceServerEntity;
-import org.keycloak.authorization.jpa.entities.ScopeEntity;
-import org.keycloak.authorization.model.ResourceServer;
-import org.keycloak.authorization.model.Scope;
-import org.keycloak.authorization.store.ScopeStore;
-import org.keycloak.models.utils.KeycloakModelUtils;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
import javax.persistence.EntityManager;
import javax.persistence.NoResultException;
@@ -31,9 +28,13 @@ import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map;
+
+import org.keycloak.authorization.jpa.entities.ResourceServerEntity;
+import org.keycloak.authorization.jpa.entities.ScopeEntity;
+import org.keycloak.authorization.model.ResourceServer;
+import org.keycloak.authorization.model.Scope;
+import org.keycloak.authorization.store.ScopeStore;
+import org.keycloak.models.utils.KeycloakModelUtils;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
@@ -61,12 +62,30 @@ public class JPAScopeStore implements ScopeStore {
@Override
public void delete(String id) {
- this.entityManager.remove(findById(id));
+ Scope scope = entityManager.find(ScopeEntity.class, id);
+
+ if (scope != null) {
+ this.entityManager.remove(scope);
+ }
}
@Override
- public Scope findById(String id) {
+ public Scope findById(String id, String resourceServerId) {
+ if (id == null) {
+ return null;
+ }
+
+ if (resourceServerId == null) {
+ return entityManager.find(ScopeEntity.class, id);
+ }
+
+ Query query = entityManager.createQuery("from ScopeEntity where resourceServer.id = :serverId and id = :id");
+
+ query.setParameter("serverId", resourceServerId);
+ query.setParameter("id", id);
+
return entityManager.find(ScopeEntity.class, id);
+
}
@Override
@@ -74,8 +93,8 @@ public class JPAScopeStore implements ScopeStore {
try {
Query query = entityManager.createQuery("select s from ScopeEntity s inner join s.resourceServer rs where rs.id = :resourceServerId and name = :name");
- query.setParameter("name", name);
query.setParameter("resourceServerId", resourceServerId);
+ query.setParameter("name", name);
return (Scope) query.getSingleResult();
} catch (NoResultException nre) {
@@ -102,7 +121,11 @@ public class JPAScopeStore implements ScopeStore {
predicates.add(builder.equal(root.get("resourceServer").get("id"), resourceServerId));
attributes.forEach((name, value) -> {
- predicates.add(builder.like(builder.lower(root.get(name)), "%" + value[0].toLowerCase() + "%"));
+ if ("id".equals(name)) {
+ predicates.add(root.get(name).in(value));
+ } else {
+ predicates.add(builder.like(builder.lower(root.get(name)), "%" + value[0].toLowerCase() + "%"));
+ }
});
querybuilder.where(predicates.toArray(new Predicate[predicates.size()])).orderBy(builder.asc(root.get("name")));
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmEntity.java
index eef1d91..cc62c8a 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmEntity.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmEntity.java
@@ -73,6 +73,10 @@ public class RealmEntity {
protected boolean verifyEmail;
@Column(name="RESET_PASSWORD_ALLOWED")
protected boolean resetPasswordAllowed;
+ @Column(name="LOGIN_WITH_EMAIL_ALLOWED")
+ protected boolean loginWithEmailAllowed;
+ @Column(name="DUPLICATE_EMAILS_ALLOWED")
+ protected boolean duplicateEmailsAllowed;
@Column(name="REMEMBER_ME")
protected boolean rememberMe;
@@ -287,6 +291,22 @@ public class RealmEntity {
public void setVerifyEmail(boolean verifyEmail) {
this.verifyEmail = verifyEmail;
}
+
+ public boolean isLoginWithEmailAllowed() {
+ return loginWithEmailAllowed;
+ }
+
+ public void setLoginWithEmailAllowed(boolean loginWithEmailAllowed) {
+ this.loginWithEmailAllowed = loginWithEmailAllowed;
+ }
+
+ public boolean isDuplicateEmailsAllowed() {
+ return duplicateEmailsAllowed;
+ }
+
+ public void setDuplicateEmailsAllowed(boolean duplicateEmailsAllowed) {
+ this.duplicateEmailsAllowed = duplicateEmailsAllowed;
+ }
public boolean isResetPasswordAllowed() {
return resetPasswordAllowed;
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserEntity.java
index 9b5ff19..6f2f3ff 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserEntity.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserEntity.java
@@ -78,7 +78,7 @@ public class UserEntity {
@Column(name = "EMAIL_VERIFIED")
protected boolean emailVerified;
- // Hack just to workaround the fact that on MS-SQL you can't have unique constraint with multiple NULL values TODO: Find better solution (like unique index with 'where' but that's proprietary)
+ // This is necessary to be able to dynamically switch unique email constraints on and off in the realm settings
@Column(name = "EMAIL_CONSTRAINT")
protected String emailConstraint = KeycloakModelUtils.generateId();
@@ -144,9 +144,9 @@ public class UserEntity {
return email;
}
- public void setEmail(String email) {
+ public void setEmail(String email, boolean allowDuplicate) {
this.email = email;
- this.emailConstraint = email != null ? email : KeycloakModelUtils.generateId();
+ this.emailConstraint = email == null || allowDuplicate ? KeycloakModelUtils.generateId() : email;
}
public boolean isEnabled() {
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java
index 8e1d2eb..dbe367f 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java
@@ -480,7 +480,12 @@ public class JpaUserProvider implements UserProvider, UserCredentialStore {
query.setParameter("email", email.toLowerCase());
query.setParameter("realmId", realm.getId());
List<UserEntity> results = query.getResultList();
- return results.isEmpty() ? null : new UserAdapter(session, realm, em, results.get(0));
+
+ if (results.isEmpty()) return null;
+
+ ensureEmailConstraint(results, realm);
+
+ return new UserAdapter(session, realm, em, results.get(0));
}
@Override
@@ -880,7 +885,25 @@ public class JpaUserProvider implements UserProvider, UserCredentialStore {
return toModel(results.get(0));
}
-
-
-
+ // Could override this to provide a custom behavior.
+ protected void ensureEmailConstraint(List<UserEntity> users, RealmModel realm) {
+ UserEntity user = users.get(0);
+
+ if (users.size() > 1) {
+ // Realm settings have been changed from allowing duplicate emails to not allowing them
+ // but duplicates haven't been removed.
+ throw new ModelDuplicateException("Multiple users with email '" + user.getEmail() + "' exist in Keycloak.");
+ }
+
+ if (realm.isDuplicateEmailsAllowed()) {
+ return;
+ }
+
+ if (user.getEmail() != null && !user.getEmail().equals(user.getEmailConstraint())) {
+ // Realm settings have been changed from allowing duplicate emails to not allowing them.
+ // We need to update the email constraint to reflect this change in the user entities.
+ user.setEmailConstraint(user.getEmail());
+ em.persist(user);
+ }
+ }
}
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java
index f11ba90..32f34a5 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java
@@ -169,6 +169,7 @@ public class RealmAdapter implements RealmModel, JpaModel<RealmEntity> {
@Override
public void setRegistrationEmailAsUsername(boolean registrationEmailAsUsername) {
realm.setRegistrationEmailAsUsername(registrationEmailAsUsername);
+ if (registrationEmailAsUsername) realm.setDuplicateEmailsAllowed(false);
em.flush();
}
@@ -347,6 +348,33 @@ public class RealmAdapter implements RealmModel, JpaModel<RealmEntity> {
realm.setVerifyEmail(verifyEmail);
em.flush();
}
+
+ @Override
+ public boolean isLoginWithEmailAllowed() {
+ return realm.isLoginWithEmailAllowed();
+ }
+
+ @Override
+ public void setLoginWithEmailAllowed(boolean loginWithEmailAllowed) {
+ realm.setLoginWithEmailAllowed(loginWithEmailAllowed);
+ if (loginWithEmailAllowed) realm.setDuplicateEmailsAllowed(false);
+ em.flush();
+ }
+
+ @Override
+ public boolean isDuplicateEmailsAllowed() {
+ return realm.isDuplicateEmailsAllowed();
+ }
+
+ @Override
+ public void setDuplicateEmailsAllowed(boolean duplicateEmailsAllowed) {
+ realm.setDuplicateEmailsAllowed(duplicateEmailsAllowed);
+ if (duplicateEmailsAllowed) {
+ realm.setLoginWithEmailAllowed(false);
+ realm.setRegistrationEmailAsUsername(false);
+ }
+ em.flush();
+ }
@Override
public boolean isResetPasswordAllowed() {
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/session/PersistentUserSessionEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/session/PersistentUserSessionEntity.java
index a2f0143..af47a13 100644
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/session/PersistentUserSessionEntity.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/session/PersistentUserSessionEntity.java
@@ -50,7 +50,7 @@ public class PersistentUserSessionEntity {
@Column(name = "REALM_ID", length = 36)
protected String realmId;
- @Column(name="USER_ID", length = 36)
+ @Column(name="USER_ID", length = 255)
protected String userId;
@Column(name = "LAST_SESSION_REFRESH")
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/UserAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/UserAdapter.java
index a95548b..a80dec9 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/UserAdapter.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/UserAdapter.java
@@ -276,7 +276,7 @@ public class UserAdapter implements UserModel, JpaModel<UserEntity> {
@Override
public void setEmail(String email) {
email = KeycloakModelUtils.toLowerCaseSafe(email);
- user.setEmail(email);
+ user.setEmail(email, realm.isDuplicateEmailsAllowed());
}
@Override
diff --git a/model/jpa/src/main/resources/META-INF/jpa-changelog-2.5.0.xml b/model/jpa/src/main/resources/META-INF/jpa-changelog-2.5.0.xml
index fb5ef22..04e6694 100755
--- a/model/jpa/src/main/resources/META-INF/jpa-changelog-2.5.0.xml
+++ b/model/jpa/src/main/resources/META-INF/jpa-changelog-2.5.0.xml
@@ -20,6 +20,9 @@
<changeSet author="bburke@redhat.com" id="2.5.0">
<customChange class="org.keycloak.connections.jpa.updater.liquibase.custom.MigrateUserFedToComponent"/>
+
+ <addUniqueConstraint columnNames="NAME,PARENT_GROUP,REALM_ID" constraintName="SIBLING_NAMES" tableName="KEYCLOAK_GROUP"/>
+
<modifyDataType tableName="OFFLINE_USER_SESSION" columnName="USER_ID" newDataType="VARCHAR(255)"/>
</changeSet>
@@ -59,7 +62,7 @@
<modifyDataType tableName="KEYCLOAK_ROLE" columnName="NAME" newDataType="NVARCHAR(255)"/>
<modifyDataType tableName="KEYCLOAK_ROLE" columnName="DESCRIPTION" newDataType="NVARCHAR(255)"/>
</changeSet>
-
+
<changeSet author="hmlnarik@redhat.com" id="2.5.0-unicode-other-dbs">
<preConditions onSqlOutput="TEST" onFail="MARK_RAN">
<not>
@@ -97,5 +100,16 @@
<addUniqueConstraint columnNames="NAME,CLIENT_REALM_CONSTRAINT" constraintName="UK_J3RWUVD56ONTGSUHOGM184WW2-2" tableName="KEYCLOAK_ROLE"/>
<modifyDataType tableName="KEYCLOAK_ROLE" columnName="DESCRIPTION" newDataType="NVARCHAR(255)"/>
</changeSet>
+
+ <changeSet author="slawomir@dabek.name" id="2.5.0-duplicate-email-support">
+ <addColumn tableName="REALM">
+ <column name="LOGIN_WITH_EMAIL_ALLOWED" type="BOOLEAN" defaultValueBoolean="true">
+ <constraints nullable="false"/>
+ </column>
+ <column name="DUPLICATE_EMAILS_ALLOWED" type="BOOLEAN" defaultValueBoolean="false">
+ <constraints nullable="false"/>
+ </column>
+ </addColumn>
+ </changeSet>
</databaseChangeLog>
\ No newline at end of file
diff --git a/model/mongo/src/main/java/org/keycloak/authorization/mongo/adapter/PolicyAdapter.java b/model/mongo/src/main/java/org/keycloak/authorization/mongo/adapter/PolicyAdapter.java
index 2b28f16..928bba9 100644
--- a/model/mongo/src/main/java/org/keycloak/authorization/mongo/adapter/PolicyAdapter.java
+++ b/model/mongo/src/main/java/org/keycloak/authorization/mongo/adapter/PolicyAdapter.java
@@ -124,21 +124,21 @@ public class PolicyAdapter extends AbstractMongoAdapter<PolicyEntity> implements
@Override
public Set<Policy> getAssociatedPolicies() {
return getMongoEntity().getAssociatedPolicies().stream()
- .map((Function<String, Policy>) id -> authorizationProvider.getStoreFactory().getPolicyStore().findById(id))
+ .map((Function<String, Policy>) id -> authorizationProvider.getStoreFactory().getPolicyStore().findById(id, getMongoEntity().getResourceServerId()))
.collect(Collectors.toSet());
}
@Override
public Set<Resource> getResources() {
return getMongoEntity().getResources().stream()
- .map((Function<String, Resource>) id -> authorizationProvider.getStoreFactory().getResourceStore().findById(id))
+ .map((Function<String, Resource>) id -> authorizationProvider.getStoreFactory().getResourceStore().findById(id, getMongoEntity().getResourceServerId()))
.collect(Collectors.toSet());
}
@Override
public Set<Scope> getScopes() {
return getMongoEntity().getScopes().stream()
- .map((Function<String, Scope>) id -> authorizationProvider.getStoreFactory().getScopeStore().findById(id))
+ .map((Function<String, Scope>) id -> authorizationProvider.getStoreFactory().getScopeStore().findById(id, getMongoEntity().getResourceServerId()))
.collect(Collectors.toSet());
}
diff --git a/model/mongo/src/main/java/org/keycloak/authorization/mongo/adapter/ResourceAdapter.java b/model/mongo/src/main/java/org/keycloak/authorization/mongo/adapter/ResourceAdapter.java
index 7c67f6e..8138a24 100644
--- a/model/mongo/src/main/java/org/keycloak/authorization/mongo/adapter/ResourceAdapter.java
+++ b/model/mongo/src/main/java/org/keycloak/authorization/mongo/adapter/ResourceAdapter.java
@@ -68,7 +68,7 @@ public class ResourceAdapter extends AbstractMongoAdapter<ResourceEntity> implem
@Override
public List<Scope> getScopes() {
return getMongoEntity().getScopes().stream()
- .map(id -> authorizationProvider.getStoreFactory().getScopeStore().findById(id))
+ .map(id -> authorizationProvider.getStoreFactory().getScopeStore().findById(id, getResourceServer().getId()))
.collect(toList());
}
diff --git a/model/mongo/src/main/java/org/keycloak/authorization/mongo/store/MongoAuthorizationStoreFactory.java b/model/mongo/src/main/java/org/keycloak/authorization/mongo/store/MongoAuthorizationStoreFactory.java
index 9a484ad..df4cfc9 100644
--- a/model/mongo/src/main/java/org/keycloak/authorization/mongo/store/MongoAuthorizationStoreFactory.java
+++ b/model/mongo/src/main/java/org/keycloak/authorization/mongo/store/MongoAuthorizationStoreFactory.java
@@ -32,8 +32,7 @@ public class MongoAuthorizationStoreFactory implements AuthorizationStoreFactory
@Override
public StoreFactory create(KeycloakSession session) {
MongoConnectionProvider connection = session.getProvider(MongoConnectionProvider.class);
- AuthorizationProvider provider = session.getProvider(AuthorizationProvider.class);
- return new MongoStoreFactory(connection.getInvocationContext(), provider);
+ return new MongoStoreFactory(connection.getInvocationContext(), session);
}
@Override
diff --git a/model/mongo/src/main/java/org/keycloak/authorization/mongo/store/MongoPolicyStore.java b/model/mongo/src/main/java/org/keycloak/authorization/mongo/store/MongoPolicyStore.java
index 04a3d9a..c7227f6 100644
--- a/model/mongo/src/main/java/org/keycloak/authorization/mongo/store/MongoPolicyStore.java
+++ b/model/mongo/src/main/java/org/keycloak/authorization/mongo/store/MongoPolicyStore.java
@@ -70,7 +70,7 @@ public class MongoPolicyStore implements PolicyStore {
}
@Override
- public Policy findById(String id) {
+ public Policy findById(String id, String resourceServerId) {
PolicyEntity entity = getMongoStore().loadEntity(PolicyEntity.class, id, getInvocationContext());
if (entity == null) {
@@ -89,7 +89,7 @@ public class MongoPolicyStore implements PolicyStore {
.get();
return getMongoStore().loadEntities(PolicyEntity.class, query, getInvocationContext()).stream()
- .map(policyEntity -> findById(policyEntity.getId())).findFirst().orElse(null);
+ .map(policyEntity -> findById(policyEntity.getId(), resourceServerId)).findFirst().orElse(null);
}
@Override
@@ -99,7 +99,7 @@ public class MongoPolicyStore implements PolicyStore {
.get();
return getMongoStore().loadEntities(PolicyEntity.class, query, getInvocationContext()).stream()
- .map(policyEntity -> findById(policyEntity.getId()))
+ .map(policyEntity -> findById(policyEntity.getId(), resourceServerId))
.collect(toList());
}
@@ -125,17 +125,18 @@ public class MongoPolicyStore implements PolicyStore {
DBObject sort = new BasicDBObject("name", 1);
return getMongoStore().loadEntities(PolicyEntity.class, queryBuilder.get(), sort, firstResult, maxResult, invocationContext).stream()
- .map(policy -> findById(policy.getId())).collect(toList());
+ .map(policy -> findById(policy.getId(), resourceServerId)).collect(toList());
}
@Override
- public List<Policy> findByResource(String resourceId) {
+ public List<Policy> findByResource(String resourceId, String resourceServerId) {
DBObject query = new QueryBuilder()
+ .and("resourceServerId").is(resourceServerId)
.and("resources").is(resourceId)
.get();
return getMongoStore().loadEntities(PolicyEntity.class, query, getInvocationContext()).stream()
- .map(policyEntity -> findById(policyEntity.getId()))
+ .map(policyEntity -> findById(policyEntity.getId(), resourceServerId))
.collect(toList());
}
@@ -150,7 +151,7 @@ public class MongoPolicyStore implements PolicyStore {
String defaultResourceType = policyEntity.getConfig().get("defaultResourceType");
return defaultResourceType != null && defaultResourceType.equals(resourceType);
})
- .map(policyEntity -> findById(policyEntity.getId()))
+ .map(policyEntity -> findById(policyEntity.getId(), resourceServerId))
.collect(toList());
}
@@ -162,29 +163,31 @@ public class MongoPolicyStore implements PolicyStore {
.get();
return getMongoStore().loadEntities(PolicyEntity.class, query, getInvocationContext()).stream()
- .map(policyEntity -> findById(policyEntity.getId()))
+ .map(policyEntity -> findById(policyEntity.getId(), resourceServerId))
.collect(toList());
}
@Override
- public List<Policy> findByType(String type) {
+ public List<Policy> findByType(String type, String resourceServerId) {
DBObject query = new QueryBuilder()
+ .and("resourceServerId").is(resourceServerId)
.and("type").is(type)
.get();
return getMongoStore().loadEntities(PolicyEntity.class, query, getInvocationContext()).stream()
- .map(policyEntity -> findById(policyEntity.getId()))
+ .map(policyEntity -> findById(policyEntity.getId(), resourceServerId))
.collect(toList());
}
@Override
- public List<Policy> findDependentPolicies(String policyId) {
+ public List<Policy> findDependentPolicies(String policyId, String resourceServerId) {
DBObject query = new QueryBuilder()
+ .and("resourceServerId").is(resourceServerId)
.and("associatedPolicies").is(policyId)
.get();
return getMongoStore().loadEntities(PolicyEntity.class, query, getInvocationContext()).stream()
- .map(policyEntity -> findById(policyEntity.getId()))
+ .map(policyEntity -> findById(policyEntity.getId(), resourceServerId))
.collect(toList());
}
diff --git a/model/mongo/src/main/java/org/keycloak/authorization/mongo/store/MongoResourceStore.java b/model/mongo/src/main/java/org/keycloak/authorization/mongo/store/MongoResourceStore.java
index a85de72..dcbf3ec 100644
--- a/model/mongo/src/main/java/org/keycloak/authorization/mongo/store/MongoResourceStore.java
+++ b/model/mongo/src/main/java/org/keycloak/authorization/mongo/store/MongoResourceStore.java
@@ -70,7 +70,7 @@ public class MongoResourceStore implements ResourceStore {
}
@Override
- public Resource findById(String id) {
+ public Resource findById(String id, String resourceServerId) {
ResourceEntity entity = getMongoStore().loadEntity(ResourceEntity.class, id, getInvocationContext());
if (entity == null) {
@@ -81,13 +81,25 @@ public class MongoResourceStore implements ResourceStore {
}
@Override
- public List<Resource> findByOwner(String ownerId) {
+ public List<Resource> findByOwner(String ownerId, String resourceServerId) {
DBObject query = new QueryBuilder()
+ .and("resourceServerId").is(resourceServerId)
.and("owner").is(ownerId)
.get();
return getMongoStore().loadEntities(ResourceEntity.class, query, getInvocationContext()).stream()
- .map(scope -> findById(scope.getId())).collect(toList());
+ .map(scope -> findById(scope.getId(), resourceServerId)).collect(toList());
+ }
+
+ @Override
+ public List<Resource> findByUri(String uri, String resourceServerId) {
+ DBObject query = new QueryBuilder()
+ .and("resourceServerId").is(resourceServerId)
+ .and("uri").is(uri)
+ .get();
+
+ return getMongoStore().loadEntities(ResourceEntity.class, query, getInvocationContext()).stream()
+ .map(scope -> findById(scope.getId(), resourceServerId)).collect(toList());
}
@Override
@@ -97,7 +109,7 @@ public class MongoResourceStore implements ResourceStore {
.get();
return getMongoStore().loadEntities(ResourceEntity.class, query, getInvocationContext()).stream()
- .map(scope -> findById(scope.getId())).collect(toList());
+ .map(scope -> findById(scope.getId(), resourceServerId)).collect(toList());
}
@Override
@@ -116,39 +128,41 @@ public class MongoResourceStore implements ResourceStore {
DBObject sort = new BasicDBObject("name", 1);
return getMongoStore().loadEntities(ResourceEntity.class, queryBuilder.get(), sort, firstResult, maxResult, invocationContext).stream()
- .map(scope -> findById(scope.getId())).collect(toList());
+ .map(scope -> findById(scope.getId(), resourceServerId)).collect(toList());
}
@Override
- public List<Resource> findByScope(String... id) {
+ public List<Resource> findByScope(List<String> id, String resourceServerId) {
DBObject query = new QueryBuilder()
+ .and("resourceServerId").is(resourceServerId)
.and("scopes").in(id)
.get();
return getMongoStore().loadEntities(ResourceEntity.class, query, getInvocationContext()).stream()
- .map(policyEntity -> findById(policyEntity.getId()))
+ .map(policyEntity -> findById(policyEntity.getId(), resourceServerId))
.collect(toList());
}
@Override
public Resource findByName(String name, String resourceServerId) {
DBObject query = new QueryBuilder()
- .and("name").is(name)
.and("resourceServerId").is(resourceServerId)
+ .and("name").is(name)
.get();
return getMongoStore().loadEntities(ResourceEntity.class, query, getInvocationContext()).stream()
- .map(policyEntity -> findById(policyEntity.getId())).findFirst().orElse(null);
+ .map(policyEntity -> findById(policyEntity.getId(), resourceServerId)).findFirst().orElse(null);
}
@Override
- public List<Resource> findByType(String type) {
+ public List<Resource> findByType(String type, String resourceServerId) {
DBObject query = new QueryBuilder()
+ .and("resourceServerId").is(resourceServerId)
.and("type").is(type)
.get();
return getMongoStore().loadEntities(ResourceEntity.class, query, getInvocationContext()).stream()
- .map(policyEntity -> findById(policyEntity.getId()))
+ .map(policyEntity -> findById(policyEntity.getId(), resourceServerId))
.collect(toList());
}
diff --git a/model/mongo/src/main/java/org/keycloak/authorization/mongo/store/MongoScopeStore.java b/model/mongo/src/main/java/org/keycloak/authorization/mongo/store/MongoScopeStore.java
index 4b7edd6..04decb2 100644
--- a/model/mongo/src/main/java/org/keycloak/authorization/mongo/store/MongoScopeStore.java
+++ b/model/mongo/src/main/java/org/keycloak/authorization/mongo/store/MongoScopeStore.java
@@ -69,7 +69,7 @@ public class MongoScopeStore implements ScopeStore {
}
@Override
- public Scope findById(String id) {
+ public Scope findById(String id, String resourceServerId) {
ScopeEntity entity = getMongoStore().loadEntity(ScopeEntity.class, id, getInvocationContext());
if (entity == null) {
@@ -87,7 +87,7 @@ public class MongoScopeStore implements ScopeStore {
.get();
return getMongoStore().loadEntities(ScopeEntity.class, query, getInvocationContext()).stream()
- .map(scope -> findById(scope.getId())).findFirst().orElse(null);
+ .map(scope -> findById(scope.getId(), scope.getResourceServerId())).findFirst().orElse(null);
}
@Override
@@ -97,7 +97,7 @@ public class MongoScopeStore implements ScopeStore {
.get();
return getMongoStore().loadEntities(ScopeEntity.class, query, getInvocationContext()).stream()
- .map(policyEntity -> findById(policyEntity.getId()))
+ .map(scope -> findById(scope.getId(), scope.getResourceServerId()))
.collect(toList());
}
@@ -113,7 +113,7 @@ public class MongoScopeStore implements ScopeStore {
DBObject sort = new BasicDBObject("name", 1);
return getMongoStore().loadEntities(ScopeEntity.class, queryBuilder.get(), sort, firstResult, maxResult, invocationContext).stream()
- .map(scope -> findById(scope.getId())).collect(toList());
+ .map(scope -> findById(scope.getId(), scope.getResourceServerId())).collect(toList());
}
private MongoStoreInvocationContext getInvocationContext() {
diff --git a/model/mongo/src/main/java/org/keycloak/authorization/mongo/store/MongoStoreFactory.java b/model/mongo/src/main/java/org/keycloak/authorization/mongo/store/MongoStoreFactory.java
index 7a94ba5..398b3d4 100644
--- a/model/mongo/src/main/java/org/keycloak/authorization/mongo/store/MongoStoreFactory.java
+++ b/model/mongo/src/main/java/org/keycloak/authorization/mongo/store/MongoStoreFactory.java
@@ -25,6 +25,7 @@ import org.keycloak.authorization.store.ResourceStore;
import org.keycloak.authorization.store.ScopeStore;
import org.keycloak.authorization.store.StoreFactory;
import org.keycloak.connections.mongo.api.context.MongoStoreInvocationContext;
+import org.keycloak.models.KeycloakSession;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
@@ -32,31 +33,35 @@ import org.keycloak.connections.mongo.api.context.MongoStoreInvocationContext;
public class MongoStoreFactory implements StoreFactory {
private final MongoStoreInvocationContext invocationContext;
- private final AuthorizationProvider authorizationProvider;
+ private final KeycloakSession session;
- public MongoStoreFactory(MongoStoreInvocationContext invocationContext, AuthorizationProvider authorizationProvider) {
+ public MongoStoreFactory(MongoStoreInvocationContext invocationContext, KeycloakSession session) {
this.invocationContext = invocationContext;
- this.authorizationProvider = authorizationProvider;
+ this.session = session;
}
@Override
public PolicyStore getPolicyStore() {
- return new MongoPolicyStore(this.invocationContext, this.authorizationProvider);
+ return new MongoPolicyStore(this.invocationContext, getAuthorizationProvider());
}
@Override
public ResourceServerStore getResourceServerStore() {
- return new MongoResourceServerStore(this.invocationContext, this.authorizationProvider);
+ return new MongoResourceServerStore(this.invocationContext, getAuthorizationProvider());
}
@Override
public ResourceStore getResourceStore() {
- return new MongoResourceStore(this.invocationContext, this.authorizationProvider);
+ return new MongoResourceStore(this.invocationContext, getAuthorizationProvider());
}
@Override
public ScopeStore getScopeStore() {
- return new MongoScopeStore(this.invocationContext, this.authorizationProvider);
+ return new MongoScopeStore(this.invocationContext, getAuthorizationProvider());
+ }
+
+ private AuthorizationProvider getAuthorizationProvider() {
+ return session.getProvider(AuthorizationProvider.class);
}
@Override
diff --git a/model/mongo/src/main/java/org/keycloak/connections/mongo/updater/impl/updates/Update2_5_0.java b/model/mongo/src/main/java/org/keycloak/connections/mongo/updater/impl/updates/Update2_5_0.java
index a46947b..c95617b 100644
--- a/model/mongo/src/main/java/org/keycloak/connections/mongo/updater/impl/updates/Update2_5_0.java
+++ b/model/mongo/src/main/java/org/keycloak/connections/mongo/updater/impl/updates/Update2_5_0.java
@@ -17,8 +17,10 @@
package org.keycloak.connections.mongo.updater.impl.updates;
+import com.mongodb.BasicDBObject;
+import com.mongodb.DBCollection;
+import com.mongodb.DBCursor;
import org.keycloak.models.KeycloakSession;
-import org.keycloak.models.LDAPConstants;
import org.keycloak.provider.ProviderFactory;
import org.keycloak.storage.UserStorageProvider;
@@ -40,6 +42,16 @@ public class Update2_5_0 extends AbstractMigrateUserFedToComponent {
for (ProviderFactory factory : factories) {
portUserFedToComponent(factory.getId());
}
+
+ DBCollection realms = db.getCollection("realms");
+ try (DBCursor realmsCursor = realms.find()) {
+ while (realmsCursor.hasNext()) {
+ BasicDBObject realm = (BasicDBObject) realmsCursor.next();
+ realm.append("loginWithEmailAllowed", true);
+ realm.append("duplicateEmailsAllowed", false);
+ realms.save(realm);
+ }
+ }
}
}
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserProvider.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserProvider.java
index dd4d7a6..9100631 100755
--- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserProvider.java
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserProvider.java
@@ -60,6 +60,7 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;
+import org.keycloak.models.mongo.keycloak.entities.UserEntity;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
@@ -111,13 +112,13 @@ public class MongoUserProvider implements UserProvider, UserCredentialStore {
.and("email").is(email.toLowerCase())
.and("realmId").is(realm.getId())
.get();
- MongoUserEntity user = getMongoStore().loadSingleEntity(MongoUserEntity.class, query, invocationContext);
+ List<MongoUserEntity> users = getMongoStore().loadEntities(MongoUserEntity.class, query, invocationContext);
- if (user == null) {
- return null;
- } else {
- return new UserAdapter(session, realm, user, invocationContext);
- }
+ if (users.isEmpty()) return null;
+
+ ensureEmailConstraint(users, realm);
+
+ return new UserAdapter(session, realm, users.get(0), invocationContext);
}
@Override
@@ -817,4 +818,26 @@ public class MongoUserProvider implements UserProvider, UserCredentialStore {
if (update) getMongoStore().updateEntity(mongoUser, invocationContext);
return credModel;
}
+
+ // Could override this to provide a custom behavior.
+ protected void ensureEmailConstraint(List<MongoUserEntity> users, RealmModel realm) {
+ MongoUserEntity user = users.get(0);
+
+ if (users.size() > 1) {
+ // Realm settings have been changed from allowing duplicate emails to not allowing them
+ // but duplicates haven't been removed.
+ throw new ModelDuplicateException("Multiple users with email '" + user.getEmail() + "' exist in Keycloak.");
+ }
+
+ if (realm.isDuplicateEmailsAllowed()) {
+ return;
+ }
+
+ if (user.getEmail() != null && user.getEmailIndex() == null) {
+ // Realm settings have been changed from allowing duplicate emails to not allowing them.
+ // We need to update the email index to reflect this change in the user entities.
+ user.setEmail(user.getEmail(), false);
+ getMongoStore().updateEntity(user, invocationContext);
+ }
+ }
}
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java
index cf8fd56..62d3be8 100755
--- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java
@@ -157,12 +157,15 @@ public class RealmAdapter extends AbstractMongoAdapter<MongoRealmEntity> impleme
updateRealm();
}
+ @Override
public boolean isRegistrationEmailAsUsername() {
return realm.isRegistrationEmailAsUsername();
}
+ @Override
public void setRegistrationEmailAsUsername(boolean registrationEmailAsUsername) {
realm.setRegistrationEmailAsUsername(registrationEmailAsUsername);
+ if (registrationEmailAsUsername) realm.setDuplicateEmailsAllowed(false);
updateRealm();
}
@@ -266,6 +269,33 @@ public class RealmAdapter extends AbstractMongoAdapter<MongoRealmEntity> impleme
realm.setVerifyEmail(verifyEmail);
updateRealm();
}
+
+ @Override
+ public boolean isLoginWithEmailAllowed() {
+ return realm.isLoginWithEmailAllowed();
+ }
+
+ @Override
+ public void setLoginWithEmailAllowed(boolean loginWithEmailAllowed) {
+ realm.setLoginWithEmailAllowed(loginWithEmailAllowed);
+ if (loginWithEmailAllowed) realm.setDuplicateEmailsAllowed(false);
+ updateRealm();
+ }
+
+ @Override
+ public boolean isDuplicateEmailsAllowed() {
+ return realm.isDuplicateEmailsAllowed();
+ }
+
+ @Override
+ public void setDuplicateEmailsAllowed(boolean duplicateEmailsAllowed) {
+ realm.setDuplicateEmailsAllowed(duplicateEmailsAllowed);
+ if (duplicateEmailsAllowed) {
+ realm.setLoginWithEmailAllowed(false);
+ realm.setRegistrationEmailAsUsername(false);
+ }
+ updateRealm();
+ }
@Override
public boolean isResetPasswordAllowed() {
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/UserAdapter.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/UserAdapter.java
index e5440cc..9282df0 100755
--- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/UserAdapter.java
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/UserAdapter.java
@@ -124,8 +124,7 @@ public class UserAdapter extends AbstractMongoAdapter<MongoUserEntity> implement
@Override
public void setEmail(String email) {
email = KeycloakModelUtils.toLowerCaseSafe(email);
-
- user.setEmail(email);
+ user.setEmail(email, realm.isDuplicateEmailsAllowed());
updateUser();
}
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/MongoUserEntity.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/MongoUserEntity.java
index ae9d5a6..909391b 100755
--- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/MongoUserEntity.java
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/MongoUserEntity.java
@@ -29,13 +29,6 @@ import org.keycloak.connections.mongo.api.context.MongoStoreInvocationContext;
@MongoCollection(collectionName = "users")
public class MongoUserEntity extends UserEntity implements MongoIdentifiableEntity {
- public String getEmailIndex() {
- return getEmail() != null ? getRealmId() + "//" + getEmail() : null;
- }
-
- public void setEmailIndex(String ignored) {
- }
-
@Override
public void afterRemove(MongoStoreInvocationContext context) {
// Remove all consents of this user
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/RealmEntity.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/RealmEntity.java
index 7151216..07df0a8 100755
--- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/RealmEntity.java
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/RealmEntity.java
@@ -37,6 +37,8 @@ public class RealmEntity extends AbstractIdentifiableEntity {
protected boolean registrationEmailAsUsername;
private boolean rememberMe;
private boolean verifyEmail;
+ private boolean loginWithEmailAllowed;
+ private boolean duplicateEmailsAllowed;
private boolean resetPasswordAllowed;
private String passwordPolicy;
@@ -186,6 +188,22 @@ public class RealmEntity extends AbstractIdentifiableEntity {
public void setVerifyEmail(boolean verifyEmail) {
this.verifyEmail = verifyEmail;
}
+
+ public boolean isLoginWithEmailAllowed() {
+ return loginWithEmailAllowed;
+ }
+
+ public void setLoginWithEmailAllowed(boolean loginWithEmailAllowed) {
+ this.loginWithEmailAllowed = loginWithEmailAllowed;
+ }
+
+ public boolean isDuplicateEmailsAllowed() {
+ return duplicateEmailsAllowed;
+ }
+
+ public void setDuplicateEmailsAllowed(boolean duplicateEmailsAllowed) {
+ this.duplicateEmailsAllowed = duplicateEmailsAllowed;
+ }
public boolean isResetPasswordAllowed() {
return resetPasswordAllowed;
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/UserEntity.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/UserEntity.java
index 56e8a88..2dd5395 100755
--- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/UserEntity.java
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/UserEntity.java
@@ -31,6 +31,7 @@ public class UserEntity extends AbstractIdentifiableEntity {
private String firstName;
private String lastName;
private String email;
+ private String emailIndex;
private boolean emailVerified;
private boolean enabled;
@@ -82,11 +83,25 @@ public class UserEntity extends AbstractIdentifiableEntity {
public String getEmail() {
return email;
}
-
+
+ @Deprecated // called upon deserialization only
public void setEmail(String email) {
this.email = email;
}
+ public void setEmail(String email, boolean allowDuplicate) {
+ this.email = email;
+ this.emailIndex = email == null || allowDuplicate ? null : getRealmId() + "//" + email;
+ }
+
+ public void setEmailIndex(String index) {
+ this.emailIndex = index;
+ }
+
+ public String getEmailIndex() {
+ return emailIndex;
+ }
+
public boolean isEmailVerified() {
return emailVerified;
}
pom.xml 5(+5 -0)
diff --git a/pom.xml b/pom.xml
index 23c277d..5a9a3f2 100755
--- a/pom.xml
+++ b/pom.xml
@@ -1323,6 +1323,11 @@
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
+ <artifactId>keycloak-admin-cli</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.keycloak</groupId>
<artifactId>keycloak-client-cli-dist</artifactId>
<version>${project.version}</version>
<type>zip</type>
proxy/pom.xml 2(+1 -1)
diff --git a/proxy/pom.xml b/proxy/pom.xml
index 2009021..f1f5f11 100755
--- a/proxy/pom.xml
+++ b/proxy/pom.xml
@@ -23,7 +23,7 @@
<version>2.5.0.Final-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
- <name>Model Parent</name>
+ <name>Keycloak Proxy</name>
<description/>
<modelVersion>4.0.0</modelVersion>
diff --git a/saml-core/src/main/java/org/keycloak/saml/BaseSAML2BindingBuilder.java b/saml-core/src/main/java/org/keycloak/saml/BaseSAML2BindingBuilder.java
index ca1f82e..86b3ecb 100755
--- a/saml-core/src/main/java/org/keycloak/saml/BaseSAML2BindingBuilder.java
+++ b/saml-core/src/main/java/org/keycloak/saml/BaseSAML2BindingBuilder.java
@@ -18,6 +18,7 @@
package org.keycloak.saml;
import org.jboss.logging.Logger;
+
import org.keycloak.common.util.KeycloakUriBuilder;
import org.keycloak.saml.common.constants.GeneralConstants;
import org.keycloak.saml.common.constants.JBossSAMLConstants;
@@ -29,6 +30,7 @@ import org.keycloak.saml.processing.core.saml.v2.util.DocumentUtil;
import org.keycloak.saml.processing.core.util.XMLEncryptionUtil;
import org.keycloak.saml.processing.web.util.PostBindingUtil;
import org.keycloak.saml.processing.web.util.RedirectBindingUtil;
+
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
@@ -38,7 +40,6 @@ import javax.crypto.spec.SecretKeySpec;
import javax.xml.crypto.dsig.CanonicalizationMethod;
import javax.xml.namespace.QName;
import java.io.IOException;
-import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.security.InvalidKeyException;
import java.security.KeyPair;
@@ -155,8 +156,8 @@ public class BaseSAML2BindingBuilder<T extends BaseSAML2BindingBuilder> {
}
public String encoded() throws ProcessingException, ConfigurationException, IOException {
- byte[] responseBytes = DocumentUtil.getDocumentAsString(document).getBytes("UTF-8");
- return PostBindingUtil.base64Encode(new String(responseBytes));
+ byte[] responseBytes = DocumentUtil.getDocumentAsString(document).getBytes(GeneralConstants.SAML_CHARSET);
+ return PostBindingUtil.base64Encode(new String(responseBytes, GeneralConstants.SAML_CHARSET));
}
public Document getDocument() {
return document;
@@ -300,8 +301,8 @@ public class BaseSAML2BindingBuilder<T extends BaseSAML2BindingBuilder> {
public String buildHtmlPostResponse(Document responseDoc, String actionUrl, boolean asRequest) throws ProcessingException, ConfigurationException, IOException {
- byte[] responseBytes = org.keycloak.saml.common.util.DocumentUtil.getDocumentAsString(responseDoc).getBytes("UTF-8");
- String samlResponse = PostBindingUtil.base64Encode(new String(responseBytes));
+ byte[] responseBytes = org.keycloak.saml.common.util.DocumentUtil.getDocumentAsString(responseDoc).getBytes(GeneralConstants.SAML_CHARSET);
+ String samlResponse = PostBindingUtil.base64Encode(new String(responseBytes, GeneralConstants.SAML_CHARSET));
return buildHtml(samlResponse, actionUrl, asRequest);
}
@@ -315,26 +316,26 @@ public class BaseSAML2BindingBuilder<T extends BaseSAML2BindingBuilder> {
key = GeneralConstants.SAML_REQUEST_KEY;
}
- builder.append("<HTML>");
- builder.append("<HEAD>");
+ builder.append("<HTML>")
+ .append("<HEAD>")
- builder.append("<TITLE>SAML HTTP Post Binding</TITLE>");
- builder.append("</HEAD>");
- builder.append("<BODY Onload=\"document.forms[0].submit()\">");
+ .append("<TITLE>SAML HTTP Post Binding</TITLE>")
+ .append("</HEAD>")
+ .append("<BODY Onload=\"document.forms[0].submit()\">")
- builder.append("<FORM METHOD=\"POST\" ACTION=\"" + actionUrl + "\">");
- builder.append("<INPUT TYPE=\"HIDDEN\" NAME=\"" + key + "\"" + " VALUE=\"" + samlResponse + "\"/>");
+ .append("<FORM METHOD=\"POST\" ACTION=\"").append(actionUrl).append("\">")
+ .append("<INPUT TYPE=\"HIDDEN\" NAME=\"").append(key).append("\"").append(" VALUE=\"").append(samlResponse).append("\"/>");
if (isNotNull(relayState)) {
- builder.append("<INPUT TYPE=\"HIDDEN\" NAME=\"RelayState\" " + "VALUE=\"" + escapeAttribute(relayState) + "\"/>");
+ builder.append("<INPUT TYPE=\"HIDDEN\" NAME=\"RelayState\" " + "VALUE=\"").append(escapeAttribute(relayState)).append("\"/>");
}
- builder.append("<NOSCRIPT>");
- builder.append("<P>JavaScript is disabled. We strongly recommend to enable it. Click the button below to continue.</P>");
- builder.append("<INPUT TYPE=\"SUBMIT\" VALUE=\"CONTINUE\" />");
- builder.append("</NOSCRIPT>");
+ builder.append("<NOSCRIPT>")
+ .append("<P>JavaScript is disabled. We strongly recommend to enable it. Click the button below to continue.</P>")
+ .append("<INPUT TYPE=\"SUBMIT\" VALUE=\"CONTINUE\" />")
+ .append("</NOSCRIPT>")
- builder.append("</FORM></BODY></HTML>");
+ .append("</FORM></BODY></HTML>");
return builder.toString();
}
@@ -342,7 +343,7 @@ public class BaseSAML2BindingBuilder<T extends BaseSAML2BindingBuilder> {
public String base64Encoded(Document document) throws ConfigurationException, ProcessingException, IOException {
String documentAsString = DocumentUtil.getDocumentAsString(document);
logger.debugv("saml document: {0}", documentAsString);
- byte[] responseBytes = documentAsString.getBytes("UTF-8");
+ byte[] responseBytes = documentAsString.getBytes(GeneralConstants.SAML_CHARSET);
return RedirectBindingUtil.deflateBase64URLEncode(responseBytes);
}
@@ -364,9 +365,9 @@ public class BaseSAML2BindingBuilder<T extends BaseSAML2BindingBuilder> {
byte[] sig = new byte[0];
try {
signature.initSign(signingKeyPair.getPrivate());
- signature.update(rawQuery.getBytes("UTF-8"));
+ signature.update(rawQuery.getBytes(GeneralConstants.SAML_CHARSET));
sig = signature.sign();
- } catch (InvalidKeyException | UnsupportedEncodingException | SignatureException e) {
+ } catch (InvalidKeyException | SignatureException e) {
throw new ProcessingException(e);
}
String encodedSig = RedirectBindingUtil.base64URLEncode(sig);
diff --git a/saml-core/src/main/java/org/keycloak/saml/common/util/DocumentUtil.java b/saml-core/src/main/java/org/keycloak/saml/common/util/DocumentUtil.java
index b46220b..d9dd4e0 100755
--- a/saml-core/src/main/java/org/keycloak/saml/common/util/DocumentUtil.java
+++ b/saml-core/src/main/java/org/keycloak/saml/common/util/DocumentUtil.java
@@ -376,7 +376,7 @@ public class DocumentUtil {
throw logger.processingError(e);
}
- return new String(baos.toByteArray());
+ return new String(baos.toByteArray(), GeneralConstants.SAML_CHARSET);
}
/**
diff --git a/saml-core/src/main/java/org/keycloak/saml/common/util/StaxUtil.java b/saml-core/src/main/java/org/keycloak/saml/common/util/StaxUtil.java
index 76ea66d..3c5cd38 100755
--- a/saml-core/src/main/java/org/keycloak/saml/common/util/StaxUtil.java
+++ b/saml-core/src/main/java/org/keycloak/saml/common/util/StaxUtil.java
@@ -75,7 +75,7 @@ public class StaxUtil {
public static XMLEventWriter getXMLEventWriter(final OutputStream outStream) throws ProcessingException {
XMLOutputFactory xmlOutputFactory = getXMLOutputFactory();
try {
- return xmlOutputFactory.createXMLEventWriter(outStream, "UTF-8");
+ return xmlOutputFactory.createXMLEventWriter(outStream, GeneralConstants.SAML_CHARSET_NAME);
} catch (XMLStreamException e) {
throw logger.processingError(e);
}
@@ -93,7 +93,7 @@ public class StaxUtil {
public static XMLStreamWriter getXMLStreamWriter(final OutputStream outStream) throws ProcessingException {
XMLOutputFactory xmlOutputFactory = getXMLOutputFactory();
try {
- return xmlOutputFactory.createXMLStreamWriter(outStream, "UTF-8");
+ return xmlOutputFactory.createXMLStreamWriter(outStream, GeneralConstants.SAML_CHARSET_NAME);
} catch (XMLStreamException e) {
throw logger.processingError(e);
}
diff --git a/saml-core/src/main/java/org/keycloak/saml/processing/api/saml/v2/request/SAML2Request.java b/saml-core/src/main/java/org/keycloak/saml/processing/api/saml/v2/request/SAML2Request.java
index 77c15ee..3b4b5db 100755
--- a/saml-core/src/main/java/org/keycloak/saml/processing/api/saml/v2/request/SAML2Request.java
+++ b/saml-core/src/main/java/org/keycloak/saml/processing/api/saml/v2/request/SAML2Request.java
@@ -25,6 +25,7 @@ import org.keycloak.dom.saml.v2.protocol.RequestAbstractType;
import org.keycloak.dom.saml.v2.protocol.ResponseType;
import org.keycloak.saml.common.PicketLinkLogger;
import org.keycloak.saml.common.PicketLinkLoggerFactory;
+import org.keycloak.saml.common.constants.GeneralConstants;
import org.keycloak.saml.common.constants.JBossSAMLConstants;
import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
import org.keycloak.saml.common.exceptions.ConfigurationException;
@@ -39,6 +40,7 @@ import org.keycloak.saml.processing.core.saml.v2.util.XMLTimeUtil;
import org.keycloak.saml.processing.core.saml.v2.writers.SAMLRequestWriter;
import org.keycloak.saml.processing.core.saml.v2.writers.SAMLResponseWriter;
import org.keycloak.saml.processing.core.util.JAXPValidationUtil;
+
import org.w3c.dom.Document;
import javax.xml.datatype.XMLGregorianCalendar;
@@ -274,7 +276,7 @@ public class SAML2Request {
writer.write((LogoutRequestType) rat);
}
- return DocumentUtil.getDocument(new String(bos.toByteArray()));
+ return DocumentUtil.getDocument(new String(bos.toByteArray(), GeneralConstants.SAML_CHARSET));
}
/**
diff --git a/saml-core/src/main/java/org/keycloak/saml/processing/api/util/DeflateUtil.java b/saml-core/src/main/java/org/keycloak/saml/processing/api/util/DeflateUtil.java
index f18656f..58923f8 100755
--- a/saml-core/src/main/java/org/keycloak/saml/processing/api/util/DeflateUtil.java
+++ b/saml-core/src/main/java/org/keycloak/saml/processing/api/util/DeflateUtil.java
@@ -16,6 +16,8 @@
*/
package org.keycloak.saml.processing.api.util;
+import org.keycloak.saml.common.constants.GeneralConstants;
+
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
@@ -62,7 +64,7 @@ public class DeflateUtil {
* @throws IOException
*/
public static byte[] encode(String message) throws IOException {
- return encode(message.getBytes());
+ return encode(message.getBytes(GeneralConstants.SAML_CHARSET));
}
/**
diff --git a/saml-core/src/main/java/org/keycloak/saml/processing/core/parsers/saml/metadata/SAMLEntityDescriptorParser.java b/saml-core/src/main/java/org/keycloak/saml/processing/core/parsers/saml/metadata/SAMLEntityDescriptorParser.java
index a0520ab..2af29a3 100755
--- a/saml-core/src/main/java/org/keycloak/saml/processing/core/parsers/saml/metadata/SAMLEntityDescriptorParser.java
+++ b/saml-core/src/main/java/org/keycloak/saml/processing/core/parsers/saml/metadata/SAMLEntityDescriptorParser.java
@@ -37,6 +37,7 @@ import org.keycloak.dom.saml.v2.metadata.SPSSODescriptorType;
import org.keycloak.dom.xmlsec.w3.xmlenc.EncryptionMethodType;
import org.keycloak.saml.common.PicketLinkLogger;
import org.keycloak.saml.common.PicketLinkLoggerFactory;
+import org.keycloak.saml.common.constants.GeneralConstants;
import org.keycloak.saml.common.constants.JBossSAMLConstants;
import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
import org.keycloak.saml.common.exceptions.ParsingException;
@@ -44,6 +45,7 @@ import org.keycloak.saml.common.parsers.ParserNamespaceSupport;
import org.keycloak.saml.common.util.StaxParserUtil;
import org.keycloak.saml.processing.core.parsers.util.SAMLParserUtil;
import org.keycloak.saml.processing.core.saml.v2.util.XMLTimeUtil;
+
import org.w3c.dom.Element;
import javax.xml.namespace.QName;
@@ -400,7 +402,8 @@ public class SAMLEntityDescriptorParser extends AbstractDescriptorParser impleme
startElement = StaxParserUtil.getNextStartElement(xmlEventReader);
LocalizedNameType localName = getLocalizedName(xmlEventReader, startElement);
org.addOrganizationDisplayName(localName);
- } else if (JBossSAMLConstants.ORGANIZATION_URL.get().equals(localPart)) {
+ } else if (JBossSAMLConstants.ORGANIZATION_URL.get().equals(localPart) ||
+ (JBossSAMLConstants.ORGANIZATION_URL_ALT.get().equals(localPart))) {
startElement = StaxParserUtil.getNextStartElement(xmlEventReader);
Attribute lang = startElement.getAttributeByName(new QName(JBossSAMLURIConstants.XML.get(), "lang"));
String langVal = StaxParserUtil.getAttributeValue(lang);
@@ -475,7 +478,7 @@ public class SAMLEntityDescriptorParser extends AbstractDescriptorParser impleme
keySize = BigInteger.valueOf(Long.valueOf(StaxParserUtil.getElementText(xmlEventReader)));
} else if ("OAEPparams".equals(localPart)) {
startElement = StaxParserUtil.getNextStartElement(xmlEventReader);
- OAEPparams = StaxParserUtil.getElementText(xmlEventReader).getBytes();
+ OAEPparams = StaxParserUtil.getElementText(xmlEventReader).getBytes(GeneralConstants.SAML_CHARSET);
} else {
throw logger.parserUnknownTag(localPart, startElement.getLocation());
}
diff --git a/saml-core/src/main/java/org/keycloak/saml/processing/core/parsers/util/SAML11ParserUtil.java b/saml-core/src/main/java/org/keycloak/saml/processing/core/parsers/util/SAML11ParserUtil.java
index 1a5a204..1cbfa7a 100755
--- a/saml-core/src/main/java/org/keycloak/saml/processing/core/parsers/util/SAML11ParserUtil.java
+++ b/saml-core/src/main/java/org/keycloak/saml/processing/core/parsers/util/SAML11ParserUtil.java
@@ -41,6 +41,7 @@ import org.keycloak.dom.xmlsec.w3.xmldsig.X509CertificateType;
import org.keycloak.dom.xmlsec.w3.xmldsig.X509DataType;
import org.keycloak.saml.common.PicketLinkLogger;
import org.keycloak.saml.common.PicketLinkLoggerFactory;
+import org.keycloak.saml.common.constants.GeneralConstants;
import org.keycloak.saml.common.constants.JBossSAMLConstants;
import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
import org.keycloak.saml.common.constants.WSTrustConstants;
@@ -50,6 +51,7 @@ import org.keycloak.saml.processing.core.parsers.saml.SAML11SubjectParser;
import org.keycloak.saml.processing.core.saml.v1.SAML11Constants;
import org.keycloak.saml.processing.core.saml.v2.util.SignatureUtil;
import org.keycloak.saml.processing.core.saml.v2.util.XMLTimeUtil;
+
import org.w3c.dom.Element;
import javax.xml.namespace.QName;
@@ -561,7 +563,7 @@ public class SAML11ParserUtil {
X509CertificateType cert = new X509CertificateType();
String certValue = StaxParserUtil.getElementText(xmlEventReader);
- cert.setEncodedCertificate(certValue.getBytes());
+ cert.setEncodedCertificate(certValue.getBytes(GeneralConstants.SAML_CHARSET));
x509.add(cert);
EndElement endElement = StaxParserUtil.getNextEndElement(xmlEventReader);
@@ -614,11 +616,11 @@ public class SAML11ParserUtil {
if (tag.equals(WSTrustConstants.XMLDSig.MODULUS)) {
startElement = StaxParserUtil.getNextStartElement(xmlEventReader);
String text = StaxParserUtil.getElementText(xmlEventReader);
- rsaKeyValue.setModulus(text.getBytes());
+ rsaKeyValue.setModulus(text.getBytes(GeneralConstants.SAML_CHARSET));
} else if (tag.equals(WSTrustConstants.XMLDSig.EXPONENT)) {
startElement = StaxParserUtil.getNextStartElement(xmlEventReader);
String text = StaxParserUtil.getElementText(xmlEventReader);
- rsaKeyValue.setExponent(text.getBytes());
+ rsaKeyValue.setExponent(text.getBytes(GeneralConstants.SAML_CHARSET));
} else
throw logger.parserUnknownTag(tag, startElement.getLocation());
}
diff --git a/saml-core/src/main/java/org/keycloak/saml/processing/core/parsers/util/SAMLParserUtil.java b/saml-core/src/main/java/org/keycloak/saml/processing/core/parsers/util/SAMLParserUtil.java
index c2337b2..b1ed278 100755
--- a/saml-core/src/main/java/org/keycloak/saml/processing/core/parsers/util/SAMLParserUtil.java
+++ b/saml-core/src/main/java/org/keycloak/saml/processing/core/parsers/util/SAMLParserUtil.java
@@ -34,6 +34,7 @@ import org.keycloak.dom.xmlsec.w3.xmldsig.X509CertificateType;
import org.keycloak.dom.xmlsec.w3.xmldsig.X509DataType;
import org.keycloak.saml.common.PicketLinkLogger;
import org.keycloak.saml.common.PicketLinkLoggerFactory;
+import org.keycloak.saml.common.constants.GeneralConstants;
import org.keycloak.saml.common.constants.JBossSAMLConstants;
import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
import org.keycloak.saml.common.constants.WSTrustConstants;
@@ -42,6 +43,7 @@ import org.keycloak.saml.common.util.StaxParserUtil;
import org.keycloak.saml.common.util.StringUtil;
import org.keycloak.saml.processing.core.saml.v2.util.SignatureUtil;
import org.keycloak.saml.processing.core.saml.v2.util.XMLTimeUtil;
+
import org.w3c.dom.Element;
import javax.xml.datatype.XMLGregorianCalendar;
@@ -98,7 +100,7 @@ public class SAMLParserUtil {
X509CertificateType cert = new X509CertificateType();
String certValue = StaxParserUtil.getElementText(xmlEventReader);
- cert.setEncodedCertificate(certValue.getBytes());
+ cert.setEncodedCertificate(certValue.getBytes(GeneralConstants.SAML_CHARSET));
x509.add(cert);
EndElement endElement = StaxParserUtil.getNextEndElement(xmlEventReader);
@@ -151,11 +153,11 @@ public class SAMLParserUtil {
if (tag.equals(WSTrustConstants.XMLDSig.MODULUS)) {
startElement = StaxParserUtil.getNextStartElement(xmlEventReader);
String text = StaxParserUtil.getElementText(xmlEventReader);
- rsaKeyValue.setModulus(text.getBytes());
+ rsaKeyValue.setModulus(text.getBytes(GeneralConstants.SAML_CHARSET));
} else if (tag.equals(WSTrustConstants.XMLDSig.EXPONENT)) {
startElement = StaxParserUtil.getNextStartElement(xmlEventReader);
String text = StaxParserUtil.getElementText(xmlEventReader);
- rsaKeyValue.setExponent(text.getBytes());
+ rsaKeyValue.setExponent(text.getBytes(GeneralConstants.SAML_CHARSET));
} else
throw logger.parserUnknownTag(tag, startElement.getLocation());
}
diff --git a/saml-core/src/main/java/org/keycloak/saml/processing/core/saml/v2/util/AssertionUtil.java b/saml-core/src/main/java/org/keycloak/saml/processing/core/saml/v2/util/AssertionUtil.java
index ed941a0..89d6680 100755
--- a/saml-core/src/main/java/org/keycloak/saml/processing/core/saml/v2/util/AssertionUtil.java
+++ b/saml-core/src/main/java/org/keycloak/saml/processing/core/saml/v2/util/AssertionUtil.java
@@ -49,6 +49,7 @@ import org.keycloak.saml.processing.core.parsers.saml.SAMLParser;
import org.keycloak.saml.processing.core.saml.v2.writers.SAMLAssertionWriter;
import org.keycloak.saml.processing.core.util.JAXPValidationUtil;
import org.keycloak.saml.processing.core.util.XMLEncryptionUtil;
+
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
@@ -62,7 +63,9 @@ import java.security.PublicKey;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
+
import org.keycloak.rotation.HardcodedKeyLocator;
+import org.keycloak.saml.common.constants.GeneralConstants;
/**
* Utility to deal with assertions
@@ -87,7 +90,7 @@ public class AssertionUtil {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
SAMLAssertionWriter writer = new SAMLAssertionWriter(StaxUtil.getXMLStreamWriter(baos));
writer.write(assertion);
- return new String(baos.toByteArray());
+ return new String(baos.toByteArray(), GeneralConstants.SAML_CHARSET);
}
/**
diff --git a/saml-core/src/main/java/org/keycloak/saml/processing/core/saml/v2/util/SignatureUtil.java b/saml-core/src/main/java/org/keycloak/saml/processing/core/saml/v2/util/SignatureUtil.java
index 352144d..1ba4d74 100755
--- a/saml-core/src/main/java/org/keycloak/saml/processing/core/saml/v2/util/SignatureUtil.java
+++ b/saml-core/src/main/java/org/keycloak/saml/processing/core/saml/v2/util/SignatureUtil.java
@@ -22,11 +22,13 @@ import org.keycloak.dom.xmlsec.w3.xmldsig.RSAKeyValueType;
import org.keycloak.dom.xmlsec.w3.xmldsig.SignatureType;
import org.keycloak.saml.common.PicketLinkLogger;
import org.keycloak.saml.common.PicketLinkLoggerFactory;
+import org.keycloak.saml.common.constants.GeneralConstants;
import org.keycloak.saml.common.constants.JBossSAMLConstants;
import org.keycloak.saml.common.constants.WSTrustConstants;
import org.keycloak.saml.common.exceptions.ParsingException;
import org.keycloak.saml.common.util.Base64;
import org.keycloak.saml.processing.core.constants.PicketLinkFederationConstants;
+
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
@@ -106,7 +108,7 @@ public class SignatureUtil {
String algo = signingKey.getAlgorithm();
Signature sig = getSignature(algo);
sig.initSign(signingKey);
- sig.update(stringToBeSigned.getBytes());
+ sig.update(stringToBeSigned.getBytes(GeneralConstants.SAML_CHARSET));
return sig.sign();
}
@@ -191,7 +193,7 @@ public class SignatureUtil {
Element childElement = (Element) node;
String tag = childElement.getLocalName();
- byte[] text = childElement.getTextContent().getBytes();
+ byte[] text = childElement.getTextContent().getBytes(GeneralConstants.SAML_CHARSET);
if (WSTrustConstants.XMLDSig.P.equals(tag)) {
dsa.setP(text);
@@ -232,7 +234,7 @@ public class SignatureUtil {
Element childElement = (Element) node;
String tag = childElement.getLocalName();
- byte[] text = childElement.getTextContent().getBytes();
+ byte[] text = childElement.getTextContent().getBytes(GeneralConstants.SAML_CHARSET);
if (WSTrustConstants.XMLDSig.MODULUS.equals(tag)) {
rsa.setModulus(text);
@@ -262,8 +264,8 @@ public class SignatureUtil {
byte[] exponent = pubKey.getPublicExponent().toByteArray();
RSAKeyValueType rsaKeyValue = new RSAKeyValueType();
- rsaKeyValue.setModulus(Base64.encodeBytes(modulus).getBytes());
- rsaKeyValue.setExponent(Base64.encodeBytes(exponent).getBytes());
+ rsaKeyValue.setModulus(Base64.encodeBytes(modulus).getBytes(GeneralConstants.SAML_CHARSET));
+ rsaKeyValue.setExponent(Base64.encodeBytes(exponent).getBytes(GeneralConstants.SAML_CHARSET));
return rsaKeyValue;
} else if (key instanceof DSAPublicKey) {
DSAPublicKey pubKey = (DSAPublicKey) key;
@@ -273,10 +275,10 @@ public class SignatureUtil {
byte[] Y = pubKey.getY().toByteArray();
DSAKeyValueType dsaKeyValue = new DSAKeyValueType();
- dsaKeyValue.setP(Base64.encodeBytes(P).getBytes());
- dsaKeyValue.setQ(Base64.encodeBytes(Q).getBytes());
- dsaKeyValue.setG(Base64.encodeBytes(G).getBytes());
- dsaKeyValue.setY(Base64.encodeBytes(Y).getBytes());
+ dsaKeyValue.setP(Base64.encodeBytes(P).getBytes(GeneralConstants.SAML_CHARSET));
+ dsaKeyValue.setQ(Base64.encodeBytes(Q).getBytes(GeneralConstants.SAML_CHARSET));
+ dsaKeyValue.setG(Base64.encodeBytes(G).getBytes(GeneralConstants.SAML_CHARSET));
+ dsaKeyValue.setY(Base64.encodeBytes(Y).getBytes(GeneralConstants.SAML_CHARSET));
return dsaKeyValue;
}
throw logger.unsupportedType(key.toString());
diff --git a/saml-core/src/main/java/org/keycloak/saml/processing/core/saml/v2/util/StaxWriterUtil.java b/saml-core/src/main/java/org/keycloak/saml/processing/core/saml/v2/util/StaxWriterUtil.java
index 5c83183..3473743 100755
--- a/saml-core/src/main/java/org/keycloak/saml/processing/core/saml/v2/util/StaxWriterUtil.java
+++ b/saml-core/src/main/java/org/keycloak/saml/processing/core/saml/v2/util/StaxWriterUtil.java
@@ -27,9 +27,11 @@ import org.keycloak.dom.xmlsec.w3.xmldsig.X509DataType;
import org.keycloak.saml.common.ErrorCodes;
import org.keycloak.saml.common.PicketLinkLogger;
import org.keycloak.saml.common.PicketLinkLoggerFactory;
+import org.keycloak.saml.common.constants.GeneralConstants;
import org.keycloak.saml.common.constants.WSTrustConstants;
import org.keycloak.saml.common.exceptions.ProcessingException;
import org.keycloak.saml.common.util.StaxUtil;
+
import org.w3c.dom.Element;
import javax.xml.stream.XMLStreamWriter;
@@ -77,7 +79,7 @@ public class StaxWriterUtil {
X509CertificateType cert = (X509CertificateType) obj;
StaxUtil.writeStartElement(writer, WSTrustConstants.XMLDSig.DSIG_PREFIX, WSTrustConstants.XMLDSig.X509CERT,
WSTrustConstants.XMLDSig.DSIG_NS);
- StaxUtil.writeCharacters(writer, new String(cert.getEncodedCertificate()));
+ StaxUtil.writeCharacters(writer, new String(cert.getEncodedCertificate(), GeneralConstants.SAML_CHARSET));
StaxUtil.writeEndElement(writer);
}
StaxUtil.writeEndElement(writer);
@@ -105,13 +107,13 @@ public class StaxWriterUtil {
// write the rsa key modulus.
byte[] modulus = type.getModulus();
StaxUtil.writeStartElement(writer, prefix, WSTrustConstants.XMLDSig.MODULUS, WSTrustConstants.DSIG_NS);
- StaxUtil.writeCharacters(writer, new String(modulus));
+ StaxUtil.writeCharacters(writer, new String(modulus, GeneralConstants.SAML_CHARSET));
StaxUtil.writeEndElement(writer);
// write the rsa key exponent.
byte[] exponent = type.getExponent();
StaxUtil.writeStartElement(writer, prefix, WSTrustConstants.XMLDSig.EXPONENT, WSTrustConstants.DSIG_NS);
- StaxUtil.writeCharacters(writer, new String(exponent));
+ StaxUtil.writeCharacters(writer, new String(exponent, GeneralConstants.SAML_CHARSET));
StaxUtil.writeEndElement(writer);
StaxUtil.writeEndElement(writer);
@@ -126,37 +128,37 @@ public class StaxWriterUtil {
byte[] p = type.getP();
if (p != null) {
StaxUtil.writeStartElement(writer, prefix, WSTrustConstants.XMLDSig.P, WSTrustConstants.DSIG_NS);
- StaxUtil.writeCharacters(writer, new String(p));
+ StaxUtil.writeCharacters(writer, new String(p, GeneralConstants.SAML_CHARSET));
StaxUtil.writeEndElement(writer);
}
byte[] q = type.getQ();
if (q != null) {
StaxUtil.writeStartElement(writer, prefix, WSTrustConstants.XMLDSig.Q, WSTrustConstants.DSIG_NS);
- StaxUtil.writeCharacters(writer, new String(q));
+ StaxUtil.writeCharacters(writer, new String(q, GeneralConstants.SAML_CHARSET));
StaxUtil.writeEndElement(writer);
}
byte[] g = type.getG();
if (g != null) {
StaxUtil.writeStartElement(writer, prefix, WSTrustConstants.XMLDSig.G, WSTrustConstants.DSIG_NS);
- StaxUtil.writeCharacters(writer, new String(g));
+ StaxUtil.writeCharacters(writer, new String(g, GeneralConstants.SAML_CHARSET));
StaxUtil.writeEndElement(writer);
}
byte[] y = type.getY();
if (y != null) {
StaxUtil.writeStartElement(writer, prefix, WSTrustConstants.XMLDSig.Y, WSTrustConstants.DSIG_NS);
- StaxUtil.writeCharacters(writer, new String(y));
+ StaxUtil.writeCharacters(writer, new String(y, GeneralConstants.SAML_CHARSET));
StaxUtil.writeEndElement(writer);
}
byte[] seed = type.getSeed();
if (seed != null) {
StaxUtil.writeStartElement(writer, prefix, WSTrustConstants.XMLDSig.SEED, WSTrustConstants.DSIG_NS);
- StaxUtil.writeCharacters(writer, new String(seed));
+ StaxUtil.writeCharacters(writer, new String(seed, GeneralConstants.SAML_CHARSET));
StaxUtil.writeEndElement(writer);
}
byte[] pgen = type.getPgenCounter();
if (pgen != null) {
StaxUtil.writeStartElement(writer, prefix, WSTrustConstants.XMLDSig.PGEN_COUNTER, WSTrustConstants.DSIG_NS);
- StaxUtil.writeCharacters(writer, new String(pgen));
+ StaxUtil.writeCharacters(writer, new String(pgen, GeneralConstants.SAML_CHARSET));
StaxUtil.writeEndElement(writer);
}
diff --git a/saml-core/src/main/java/org/keycloak/saml/processing/core/util/JAXBUtil.java b/saml-core/src/main/java/org/keycloak/saml/processing/core/util/JAXBUtil.java
index c7b3b79..be255e2 100755
--- a/saml-core/src/main/java/org/keycloak/saml/processing/core/util/JAXBUtil.java
+++ b/saml-core/src/main/java/org/keycloak/saml/processing/core/util/JAXBUtil.java
@@ -18,6 +18,8 @@ package org.keycloak.saml.processing.core.util;
import org.keycloak.saml.common.PicketLinkLogger;
import org.keycloak.saml.common.PicketLinkLoggerFactory;
+import org.keycloak.saml.common.constants.GeneralConstants;
+
import org.xml.sax.ErrorHandler;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
@@ -89,7 +91,7 @@ public class JAXBUtil {
JAXBContext jc = getJAXBContext(pkgName);
Marshaller marshaller = jc.createMarshaller();
- marshaller.setProperty(Marshaller.JAXB_ENCODING, "UTF-8");
+ marshaller.setProperty(Marshaller.JAXB_ENCODING, GeneralConstants.SAML_CHARSET_NAME);
marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.FALSE); // Breaks signatures
return marshaller;
}
diff --git a/saml-core/src/main/java/org/keycloak/saml/processing/core/util/XMLSignatureUtil.java b/saml-core/src/main/java/org/keycloak/saml/processing/core/util/XMLSignatureUtil.java
index 6ad6513..53228c9 100755
--- a/saml-core/src/main/java/org/keycloak/saml/processing/core/util/XMLSignatureUtil.java
+++ b/saml-core/src/main/java/org/keycloak/saml/processing/core/util/XMLSignatureUtil.java
@@ -22,6 +22,7 @@ import org.keycloak.dom.xmlsec.w3.xmldsig.RSAKeyValueType;
import org.keycloak.dom.xmlsec.w3.xmldsig.SignatureType;
import org.keycloak.saml.common.PicketLinkLogger;
import org.keycloak.saml.common.PicketLinkLoggerFactory;
+import org.keycloak.saml.common.constants.GeneralConstants;
import org.keycloak.saml.common.constants.JBossSAMLConstants;
import org.keycloak.saml.common.constants.WSTrustConstants;
import org.keycloak.saml.common.exceptions.ParsingException;
@@ -572,7 +573,7 @@ public class XMLSignatureUtil {
try {
CertificateFactory cf = CertificateFactory.getInstance("X.509");
- ByteArrayInputStream bais = new ByteArrayInputStream(derFormattedString.getBytes());
+ ByteArrayInputStream bais = new ByteArrayInputStream(derFormattedString.getBytes(GeneralConstants.SAML_CHARSET));
while (bais.available() > 0) {
cert = (X509Certificate) cf.generateCertificate(bais);
@@ -603,7 +604,7 @@ public class XMLSignatureUtil {
Element childElement = (Element) node;
String tag = childElement.getLocalName();
- byte[] text = childElement.getTextContent().getBytes();
+ byte[] text = childElement.getTextContent().getBytes(GeneralConstants.SAML_CHARSET);
if (WSTrustConstants.XMLDSig.P.equals(tag)) {
dsa.setP(text);
@@ -644,7 +645,7 @@ public class XMLSignatureUtil {
Element childElement = (Element) node;
String tag = childElement.getLocalName();
- byte[] text = childElement.getTextContent().getBytes();
+ byte[] text = childElement.getTextContent().getBytes(GeneralConstants.SAML_CHARSET);
if (WSTrustConstants.XMLDSig.MODULUS.equals(tag)) {
rsa.setModulus(text);
@@ -674,8 +675,8 @@ public class XMLSignatureUtil {
byte[] exponent = pubKey.getPublicExponent().toByteArray();
RSAKeyValueType rsaKeyValue = new RSAKeyValueType();
- rsaKeyValue.setModulus(Base64.encodeBytes(modulus).getBytes());
- rsaKeyValue.setExponent(Base64.encodeBytes(exponent).getBytes());
+ rsaKeyValue.setModulus(Base64.encodeBytes(modulus).getBytes(GeneralConstants.SAML_CHARSET));
+ rsaKeyValue.setExponent(Base64.encodeBytes(exponent).getBytes(GeneralConstants.SAML_CHARSET));
return rsaKeyValue;
} else if (key instanceof DSAPublicKey) {
DSAPublicKey pubKey = (DSAPublicKey) key;
@@ -685,10 +686,10 @@ public class XMLSignatureUtil {
byte[] Y = pubKey.getY().toByteArray();
DSAKeyValueType dsaKeyValue = new DSAKeyValueType();
- dsaKeyValue.setP(Base64.encodeBytes(P).getBytes());
- dsaKeyValue.setQ(Base64.encodeBytes(Q).getBytes());
- dsaKeyValue.setG(Base64.encodeBytes(G).getBytes());
- dsaKeyValue.setY(Base64.encodeBytes(Y).getBytes());
+ dsaKeyValue.setP(Base64.encodeBytes(P).getBytes(GeneralConstants.SAML_CHARSET));
+ dsaKeyValue.setQ(Base64.encodeBytes(Q).getBytes(GeneralConstants.SAML_CHARSET));
+ dsaKeyValue.setG(Base64.encodeBytes(G).getBytes(GeneralConstants.SAML_CHARSET));
+ dsaKeyValue.setY(Base64.encodeBytes(Y).getBytes(GeneralConstants.SAML_CHARSET));
return dsaKeyValue;
}
throw logger.unsupportedType(key.toString());
diff --git a/saml-core/src/main/java/org/keycloak/saml/processing/web/util/PostBindingUtil.java b/saml-core/src/main/java/org/keycloak/saml/processing/web/util/PostBindingUtil.java
index eaf338a..57c5191 100755
--- a/saml-core/src/main/java/org/keycloak/saml/processing/web/util/PostBindingUtil.java
+++ b/saml-core/src/main/java/org/keycloak/saml/processing/web/util/PostBindingUtil.java
@@ -18,6 +18,7 @@ package org.keycloak.saml.processing.web.util;
import org.keycloak.saml.common.PicketLinkLogger;
import org.keycloak.saml.common.PicketLinkLoggerFactory;
+import org.keycloak.saml.common.constants.GeneralConstants;
import org.keycloak.saml.common.util.Base64;
import java.io.ByteArrayInputStream;
@@ -42,7 +43,7 @@ public class PostBindingUtil {
* @return
*/
public static String base64Encode(String stringToEncode) throws IOException {
- return Base64.encodeBytes(stringToEncode.getBytes("UTF-8"), Base64.DONT_BREAK_LINES);
+ return Base64.encodeBytes(stringToEncode.getBytes(GeneralConstants.SAML_CHARSET), Base64.DONT_BREAK_LINES);
}
/**
diff --git a/saml-core/src/main/java/org/keycloak/saml/processing/web/util/RedirectBindingSignatureUtil.java b/saml-core/src/main/java/org/keycloak/saml/processing/web/util/RedirectBindingSignatureUtil.java
index c559f2b..1e2087a 100755
--- a/saml-core/src/main/java/org/keycloak/saml/processing/web/util/RedirectBindingSignatureUtil.java
+++ b/saml-core/src/main/java/org/keycloak/saml/processing/web/util/RedirectBindingSignatureUtil.java
@@ -79,7 +79,7 @@ public class RedirectBindingSignatureUtil {
String urlEncodedRelayState = null;
if (isNotNull(relayState))
- urlEncodedRelayState = URLEncoder.encode(relayState, "UTF-8");
+ urlEncodedRelayState = URLEncoder.encode(relayState, GeneralConstants.SAML_CHARSET_NAME);
byte[] sigValue = computeSignature(GeneralConstants.SAML_REQUEST_KEY, urlEncodedRequest, urlEncodedRelayState,
signingKey);
@@ -113,7 +113,7 @@ public class RedirectBindingSignatureUtil {
String urlEncodedRelayState = null;
if (isNotNull(relayState))
- urlEncodedRelayState = URLEncoder.encode(relayState, "UTF-8");
+ urlEncodedRelayState = URLEncoder.encode(relayState, GeneralConstants.SAML_CHARSET_NAME);
byte[] sigValue = computeSignature(GeneralConstants.SAML_RESPONSE_KEY, urlEncodedResponse, urlEncodedRelayState,
signingKey);
@@ -234,7 +234,7 @@ public class RedirectBindingSignatureUtil {
addParameter(sb, GeneralConstants.SAML_SIG_ALG_REQUEST_KEY,
RedirectBindingSignatureUtil.getTokenValue(queryString, GeneralConstants.SAML_SIG_ALG_REQUEST_KEY));
- return SignatureUtil.validate(sb.toString().getBytes("UTF-8"), sigValue, validatingKey);
+ return SignatureUtil.validate(sb.toString().getBytes(GeneralConstants.SAML_CHARSET), sigValue, validatingKey);
}
private static boolean isRequestQueryString(String queryString) {
@@ -257,7 +257,7 @@ public class RedirectBindingSignatureUtil {
String algo = signingKey.getAlgorithm();
String sigAlg = SignatureUtil.getXMLSignatureAlgorithmURI(algo);
- sigAlg = URLEncoder.encode(sigAlg, "UTF-8");
+ sigAlg = URLEncoder.encode(sigAlg, GeneralConstants.SAML_CHARSET_NAME);
addParameter(sb, GeneralConstants.SAML_SIG_ALG_REQUEST_KEY, sigAlg);
@@ -291,7 +291,7 @@ public class RedirectBindingSignatureUtil {
// SigAlg
String sigAlg = SignatureUtil.getXMLSignatureAlgorithmURI(sigAlgo);
- sigAlg = URLEncoder.encode(sigAlg, "UTF-8");
+ sigAlg = URLEncoder.encode(sigAlg, GeneralConstants.SAML_CHARSET_NAME);
addParameter(sb, GeneralConstants.SAML_SIG_ALG_REQUEST_KEY, sigAlg);
diff --git a/saml-core/src/main/java/org/keycloak/saml/processing/web/util/RedirectBindingUtil.java b/saml-core/src/main/java/org/keycloak/saml/processing/web/util/RedirectBindingUtil.java
index 7b22767..587113c 100755
--- a/saml-core/src/main/java/org/keycloak/saml/processing/web/util/RedirectBindingUtil.java
+++ b/saml-core/src/main/java/org/keycloak/saml/processing/web/util/RedirectBindingUtil.java
@@ -16,6 +16,7 @@
*/
package org.keycloak.saml.processing.web.util;
+import org.keycloak.saml.common.constants.GeneralConstants;
import org.keycloak.saml.common.util.Base64;
import org.keycloak.saml.common.util.StringUtil;
import org.keycloak.saml.processing.api.util.DeflateUtil;
@@ -43,7 +44,7 @@ public class RedirectBindingUtil {
* @throws IOException
*/
public static String urlEncode(String str) throws IOException {
- return URLEncoder.encode(str, "UTF-8");
+ return URLEncoder.encode(str, GeneralConstants.SAML_CHARSET_NAME);
}
/**
@@ -56,7 +57,7 @@ public class RedirectBindingUtil {
* @throws IOException
*/
public static String urlDecode(String str) throws IOException {
- return URLDecoder.decode(str, "UTF-8");
+ return URLDecoder.decode(str, GeneralConstants.SAML_CHARSET_NAME);
}
/**
@@ -97,7 +98,7 @@ public class RedirectBindingUtil {
* @throws IOException
*/
public static String deflateBase64URLEncode(String stringToEncode) throws IOException {
- return deflateBase64URLEncode(stringToEncode.getBytes("UTF-8"));
+ return deflateBase64URLEncode(stringToEncode.getBytes(GeneralConstants.SAML_CHARSET));
}
/**
diff --git a/saml-core/src/main/java/org/keycloak/saml/SAMLRequestParser.java b/saml-core/src/main/java/org/keycloak/saml/SAMLRequestParser.java
index 5f1257b..00160e6 100755
--- a/saml-core/src/main/java/org/keycloak/saml/SAMLRequestParser.java
+++ b/saml-core/src/main/java/org/keycloak/saml/SAMLRequestParser.java
@@ -18,9 +18,11 @@
package org.keycloak.saml;
import org.jboss.logging.Logger;
+
import org.keycloak.common.util.StreamUtil;
import org.keycloak.saml.common.PicketLinkLogger;
import org.keycloak.saml.common.PicketLinkLoggerFactory;
+import org.keycloak.saml.common.constants.GeneralConstants;
import org.keycloak.saml.processing.api.saml.v2.request.SAML2Request;
import org.keycloak.saml.processing.api.saml.v2.response.SAML2Response;
import org.keycloak.saml.processing.core.saml.v2.common.SAMLDocumentHolder;
@@ -45,13 +47,13 @@ public class SAMLRequestParser {
if (log.isDebugEnabled()) {
String message = null;
try {
- message = StreamUtil.readString(is);
+ message = StreamUtil.readString(is, GeneralConstants.SAML_CHARSET);
} catch (IOException e) {
throw new RuntimeException(e);
}
log.debug("SAML Redirect Binding");
log.debug(message);
- is = new ByteArrayInputStream(message.getBytes());
+ is = new ByteArrayInputStream(message.getBytes(GeneralConstants.SAML_CHARSET));
}
SAML2Request saml2Request = new SAML2Request();
@@ -69,7 +71,7 @@ public class SAMLRequestParser {
InputStream is;
byte[] samlBytes = PostBindingUtil.base64Decode(samlMessage);
if (log.isDebugEnabled()) {
- String str = new String(samlBytes);
+ String str = new String(samlBytes, GeneralConstants.SAML_CHARSET);
log.debug("SAML POST Binding");
log.debug(str);
}
@@ -92,7 +94,7 @@ public class SAMLRequestParser {
public static SAMLDocumentHolder parseResponseDocument(byte[] samlBytes) {
if (log.isDebugEnabled()) {
- String str = new String(samlBytes);
+ String str = new String(samlBytes, GeneralConstants.SAML_CHARSET);
log.debug(str);
}
InputStream is = new ByteArrayInputStream(samlBytes);
@@ -111,13 +113,13 @@ public class SAMLRequestParser {
if (log.isDebugEnabled()) {
String message = null;
try {
- message = StreamUtil.readString(is);
+ message = StreamUtil.readString(is, GeneralConstants.SAML_CHARSET);
} catch (IOException e) {
throw new RuntimeException(e);
}
log.debug("SAML Redirect Binding");
log.debug(message);
- is = new ByteArrayInputStream(message.getBytes());
+ is = new ByteArrayInputStream(message.getBytes(GeneralConstants.SAML_CHARSET));
}
SAML2Response response = new SAML2Response();
diff --git a/saml-core/src/test/java/org/keycloak/saml/processing/core/parsers/saml/SAMLParserTest.java b/saml-core/src/test/java/org/keycloak/saml/processing/core/parsers/saml/SAMLParserTest.java
index ad150e9..51854fc 100644
--- a/saml-core/src/test/java/org/keycloak/saml/processing/core/parsers/saml/SAMLParserTest.java
+++ b/saml-core/src/test/java/org/keycloak/saml/processing/core/parsers/saml/SAMLParserTest.java
@@ -16,12 +16,20 @@
*/
package org.keycloak.saml.processing.core.parsers.saml;
+import org.keycloak.dom.saml.v2.assertion.AssertionType;
+import org.keycloak.dom.saml.v2.assertion.NameIDType;
+import org.keycloak.dom.saml.v2.metadata.EntityDescriptorType;
+
import java.io.InputStream;
import org.junit.Test;
+
import static org.junit.Assert.*;
import static org.hamcrest.CoreMatchers.*;
+
import org.keycloak.dom.saml.v2.protocol.LogoutRequestType;
import org.keycloak.dom.saml.v2.protocol.ResponseType;
+
+import org.junit.Before;
import org.w3c.dom.Element;
/**
@@ -33,75 +41,118 @@ import org.w3c.dom.Element;
*/
public class SAMLParserTest {
+ private SAMLParser parser;
+
+ @Before
+ public void initParser() {
+ this.parser = new SAMLParser();
+ }
+
@Test
public void testSaml20EncryptedAssertionsSignedReceivedWithRedirectBinding() throws Exception {
- InputStream st = SAMLParserTest.class.getResourceAsStream("saml20-encrypted-signed-redirect-response.xml");
- SAMLParser parser = new SAMLParser();
-
- Object parsedObject = parser.parse(st);
- assertThat(parsedObject, instanceOf(ResponseType.class));
-
- ResponseType resp = (ResponseType) parsedObject;
- assertThat(resp.getSignature(), nullValue());
- assertThat(resp.getConsent(), nullValue());
- assertThat(resp.getIssuer(), not(nullValue()));
- assertThat(resp.getIssuer().getValue(), is("http://localhost:8081/auth/realms/saml-demo"));
-
- assertThat(resp.getExtensions(), not(nullValue()));
- assertThat(resp.getExtensions().getAny().size(), is(1));
- assertThat(resp.getExtensions().getAny().get(0), instanceOf(Element.class));
- Element el = (Element) resp.getExtensions().getAny().get(0);
- assertThat(el.getLocalName(), is("KeyInfo"));
- assertThat(el.getNamespaceURI(), is("urn:keycloak:ext:key:1.0"));
- assertThat(el.hasAttribute("MessageSigningKeyId"), is(true));
- assertThat(el.getAttribute("MessageSigningKeyId"), is("FJ86GcF3jTbNLOco4NvZkUCIUmfYCqoqtOQeMfbhNlE"));
-
- assertThat(resp.getAssertions(), not(nullValue()));
- assertThat(resp.getAssertions().size(), is(1));
+ try (InputStream st = SAMLParserTest.class.getResourceAsStream("saml20-encrypted-signed-redirect-response.xml")) {
+ Object parsedObject = parser.parse(st);
+ assertThat(parsedObject, instanceOf(ResponseType.class));
+
+ ResponseType resp = (ResponseType) parsedObject;
+ assertThat(resp.getSignature(), nullValue());
+ assertThat(resp.getConsent(), nullValue());
+ assertThat(resp.getIssuer(), not(nullValue()));
+ assertThat(resp.getIssuer().getValue(), is("http://localhost:8081/auth/realms/saml-demo"));
+
+ assertThat(resp.getExtensions(), not(nullValue()));
+ assertThat(resp.getExtensions().getAny().size(), is(1));
+ assertThat(resp.getExtensions().getAny().get(0), instanceOf(Element.class));
+ Element el = (Element) resp.getExtensions().getAny().get(0);
+ assertThat(el.getLocalName(), is("KeyInfo"));
+ assertThat(el.getNamespaceURI(), is("urn:keycloak:ext:key:1.0"));
+ assertThat(el.hasAttribute("MessageSigningKeyId"), is(true));
+ assertThat(el.getAttribute("MessageSigningKeyId"), is("FJ86GcF3jTbNLOco4NvZkUCIUmfYCqoqtOQeMfbhNlE"));
+
+ assertThat(resp.getAssertions(), not(nullValue()));
+ assertThat(resp.getAssertions().size(), is(1));
+ }
}
@Test
public void testSaml20EncryptedAssertionsSignedTwoExtensionsReceivedWithRedirectBinding() throws Exception {
Element el;
- InputStream st = SAMLParserTest.class.getResourceAsStream("saml20-encrypted-signed-redirect-response-two-extensions.xml");
- SAMLParser parser = new SAMLParser();
-
- Object parsedObject = parser.parse(st);
- assertThat(parsedObject, instanceOf(ResponseType.class));
-
- ResponseType resp = (ResponseType) parsedObject;
- assertThat(resp.getSignature(), nullValue());
- assertThat(resp.getConsent(), nullValue());
- assertThat(resp.getIssuer(), not(nullValue()));
- assertThat(resp.getIssuer().getValue(), is("http://localhost:8081/auth/realms/saml-demo"));
-
- assertThat(resp.getExtensions(), not(nullValue()));
- assertThat(resp.getExtensions().getAny().size(), is(2));
- assertThat(resp.getExtensions().getAny().get(0), instanceOf(Element.class));
- el = (Element) resp.getExtensions().getAny().get(0);
- assertThat(el.getLocalName(), is("KeyInfo"));
- assertThat(el.getNamespaceURI(), is("urn:keycloak:ext:key:1.0"));
- assertThat(el.hasAttribute("MessageSigningKeyId"), is(true));
- assertThat(el.getAttribute("MessageSigningKeyId"), is("FJ86GcF3jTbNLOco4NvZkUCIUmfYCqoqtOQeMfbhNlE"));
- assertThat(resp.getExtensions().getAny().get(1), instanceOf(Element.class));
- el = (Element) resp.getExtensions().getAny().get(1);
- assertThat(el.getLocalName(), is("ever"));
- assertThat(el.getNamespaceURI(), is("urn:keycloak:ext:what:1.0"));
- assertThat(el.hasAttribute("what"), is(true));
- assertThat(el.getAttribute("what"), is("ever"));
-
- assertThat(resp.getAssertions(), not(nullValue()));
- assertThat(resp.getAssertions().size(), is(1));
+ try (InputStream st = SAMLParserTest.class.getResourceAsStream("saml20-encrypted-signed-redirect-response-two-extensions.xml")) {
+ Object parsedObject = parser.parse(st);
+ assertThat(parsedObject, instanceOf(ResponseType.class));
+
+ ResponseType resp = (ResponseType) parsedObject;
+ assertThat(resp.getSignature(), nullValue());
+ assertThat(resp.getConsent(), nullValue());
+ assertThat(resp.getIssuer(), not(nullValue()));
+ assertThat(resp.getIssuer().getValue(), is("http://localhost:8081/auth/realms/saml-demo"));
+
+ assertThat(resp.getExtensions(), not(nullValue()));
+ assertThat(resp.getExtensions().getAny().size(), is(2));
+ assertThat(resp.getExtensions().getAny().get(0), instanceOf(Element.class));
+ el = (Element) resp.getExtensions().getAny().get(0);
+ assertThat(el.getLocalName(), is("KeyInfo"));
+ assertThat(el.getNamespaceURI(), is("urn:keycloak:ext:key:1.0"));
+ assertThat(el.hasAttribute("MessageSigningKeyId"), is(true));
+ assertThat(el.getAttribute("MessageSigningKeyId"), is("FJ86GcF3jTbNLOco4NvZkUCIUmfYCqoqtOQeMfbhNlE"));
+ assertThat(resp.getExtensions().getAny().get(1), instanceOf(Element.class));
+ el = (Element) resp.getExtensions().getAny().get(1);
+ assertThat(el.getLocalName(), is("ever"));
+ assertThat(el.getNamespaceURI(), is("urn:keycloak:ext:what:1.0"));
+ assertThat(el.hasAttribute("what"), is(true));
+ assertThat(el.getAttribute("what"), is("ever"));
+
+ assertThat(resp.getAssertions(), not(nullValue()));
+ assertThat(resp.getAssertions().size(), is(1));
+ }
}
@Test
- public void testSaml20PostLogoutRequest() throws Exception {
- InputStream st = SAMLParserTest.class.getResourceAsStream("saml20-signed-logout-request.xml");
- SAMLParser parser = new SAMLParser();
+ public void testSaml20AuthnResponseNonAsciiNameDefaultUtf8() throws Exception {
+ try (InputStream st = SAMLParserTest.class.getResourceAsStream("KEYCLOAK-3971-utf-8-no-header-authnresponse.xml")) {
+ Object parsedObject = parser.parse(st);
+ assertThat(parsedObject, instanceOf(ResponseType.class));
+
+ ResponseType rt = (ResponseType) parsedObject;
+ assertThat(rt.getAssertions().size(), is(1));
+ final AssertionType assertion = rt.getAssertions().get(0).getAssertion();
+ assertThat(assertion.getSubject().getSubType().getBaseID(), instanceOf(NameIDType.class));
- Object parsedObject = parser.parse(st);
- assertThat(parsedObject, instanceOf(LogoutRequestType.class));
+ NameIDType nameId = (NameIDType) assertion.getSubject().getSubType().getBaseID();
+ assertThat(nameId.getValue(), is("roàåאבčéèíñòøöùüßåäöü汉字"));
+ }
+ }
+ @Test
+ public void testSaml20AuthnResponseNonAsciiNameDefaultLatin2() throws Exception {
+ try (InputStream st = SAMLParserTest.class.getResourceAsStream("KEYCLOAK-3971-8859-2-in-header-authnresponse.xml")) {
+ Object parsedObject = parser.parse(st);
+ assertThat(parsedObject, instanceOf(ResponseType.class));
+
+ ResponseType rt = (ResponseType) parsedObject;
+ assertThat(rt.getAssertions().size(), is(1));
+ final AssertionType assertion = rt.getAssertions().get(0).getAssertion();
+ assertThat(assertion.getSubject().getSubType().getBaseID(), instanceOf(NameIDType.class));
+
+ NameIDType nameId = (NameIDType) assertion.getSubject().getSubType().getBaseID();
+ assertThat(nameId.getValue(), is("ročéíöüßäöü"));
+ }
+ }
+
+ @Test
+ public void testSaml20PostLogoutRequest() throws Exception {
+ try (InputStream st = SAMLParserTest.class.getResourceAsStream("saml20-signed-logout-request.xml")) {
+ Object parsedObject = parser.parse(st);
+ assertThat(parsedObject, instanceOf(LogoutRequestType.class));
+ }
+ }
+
+ @Test
+ public void testOrganizationDetailsMetadata() throws Exception {
+ try (InputStream st = SAMLParserTest.class.getResourceAsStream("KEYCLOAK-4040-sharefile-metadata.xml")) {
+ Object parsedObject = parser.parse(st);
+ assertThat(parsedObject, instanceOf(EntityDescriptorType.class));
+ }
}
}
diff --git a/saml-core/src/test/resources/org/keycloak/saml/processing/core/parsers/saml/KEYCLOAK-3971-8859-2-in-header-authnresponse.xml b/saml-core/src/test/resources/org/keycloak/saml/processing/core/parsers/saml/KEYCLOAK-3971-8859-2-in-header-authnresponse.xml
new file mode 100644
index 0000000..b4bc850
--- /dev/null
+++ b/saml-core/src/test/resources/org/keycloak/saml/processing/core/parsers/saml/KEYCLOAK-3971-8859-2-in-header-authnresponse.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="ISO-8859-2"?>
+<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" Destination="http://localhost:8080/sales-post-sig/saml" ID="ID_670a76c7-8506-4081-80b3-5ef16df98af8" InResponseTo="ID_cc0ff6f7-b481-4c98-9a79-481d50958290" IssueInstant="2016-12-15T12:42:49.788Z" Version="2.0">
+ <saml:Issuer>http://localhost:11080/auth/realms/saml-demo</saml:Issuer>
+ <samlp:Status>
+ <samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/>
+ </samlp:Status>
+ <saml:Assertion xmlns="urn:oasis:names:tc:SAML:2.0:assertion" ID="ID_cf2e2b00-dfb0-4163-b8b5-f2d85586a235" IssueInstant="2016-12-15T12:42:49.787Z" Version="2.0">
+ <saml:Issuer>http://localhost:11080/auth/realms/saml-demo</saml:Issuer>
+ <saml:Subject>
+ <saml:NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified">ro���������</saml:NameID>
+ <saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
+ <saml:SubjectConfirmationData InResponseTo="ID_cc0ff6f7-b481-4c98-9a79-481d50958290" NotOnOrAfter="2016-12-15T12:47:47.787Z" Recipient="http://localhost:8080/sales-post-sig/saml"/>
+ </saml:SubjectConfirmation>
+ </saml:Subject>
+ <saml:Conditions NotBefore="2016-12-15T12:42:47.787Z" NotOnOrAfter="2016-12-15T12:43:47.787Z">
+ <saml:AudienceRestriction>
+ <saml:Audience>http://localhost:8080/sales-post-sig/</saml:Audience>
+ </saml:AudienceRestriction>
+ </saml:Conditions>
+ <saml:AuthnStatement AuthnInstant="2016-12-15T12:42:49.788Z" SessionIndex="fb5d5a23-aa34-4528-a29a-6aad8c0ef0e8">
+ <saml:AuthnContext>
+ <saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:unspecified</saml:AuthnContextClassRef>
+ </saml:AuthnContext>
+ </saml:AuthnStatement>
+ <saml:AttributeStatement>
+ <saml:Attribute Name="Role" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic">
+ <saml:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:string">view-profile</saml:AttributeValue>
+ </saml:Attribute>
+ </saml:AttributeStatement>
+ </saml:Assertion>
+</samlp:Response>
\ No newline at end of file
diff --git a/saml-core/src/test/resources/org/keycloak/saml/processing/core/parsers/saml/KEYCLOAK-3971-utf-8-no-header-authnresponse.xml b/saml-core/src/test/resources/org/keycloak/saml/processing/core/parsers/saml/KEYCLOAK-3971-utf-8-no-header-authnresponse.xml
new file mode 100644
index 0000000..52dbb5c
--- /dev/null
+++ b/saml-core/src/test/resources/org/keycloak/saml/processing/core/parsers/saml/KEYCLOAK-3971-utf-8-no-header-authnresponse.xml
@@ -0,0 +1,30 @@
+<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" Destination="http://localhost:8080/sales-post-sig/saml" ID="ID_670a76c7-8506-4081-80b3-5ef16df98af8" InResponseTo="ID_cc0ff6f7-b481-4c98-9a79-481d50958290" IssueInstant="2016-12-15T12:42:49.788Z" Version="2.0">
+ <saml:Issuer>http://localhost:11080/auth/realms/saml-demo</saml:Issuer>
+ <samlp:Status>
+ <samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/>
+ </samlp:Status>
+ <saml:Assertion xmlns="urn:oasis:names:tc:SAML:2.0:assertion" ID="ID_cf2e2b00-dfb0-4163-b8b5-f2d85586a235" IssueInstant="2016-12-15T12:42:49.787Z" Version="2.0">
+ <saml:Issuer>http://localhost:11080/auth/realms/saml-demo</saml:Issuer>
+ <saml:Subject>
+ <saml:NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified">roàåאבčéèíñòøöùüßåäöü汉字</saml:NameID>
+ <saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
+ <saml:SubjectConfirmationData InResponseTo="ID_cc0ff6f7-b481-4c98-9a79-481d50958290" NotOnOrAfter="2016-12-15T12:47:47.787Z" Recipient="http://localhost:8080/sales-post-sig/saml"/>
+ </saml:SubjectConfirmation>
+ </saml:Subject>
+ <saml:Conditions NotBefore="2016-12-15T12:42:47.787Z" NotOnOrAfter="2016-12-15T12:43:47.787Z">
+ <saml:AudienceRestriction>
+ <saml:Audience>http://localhost:8080/sales-post-sig/</saml:Audience>
+ </saml:AudienceRestriction>
+ </saml:Conditions>
+ <saml:AuthnStatement AuthnInstant="2016-12-15T12:42:49.788Z" SessionIndex="fb5d5a23-aa34-4528-a29a-6aad8c0ef0e8">
+ <saml:AuthnContext>
+ <saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:unspecified</saml:AuthnContextClassRef>
+ </saml:AuthnContext>
+ </saml:AuthnStatement>
+ <saml:AttributeStatement>
+ <saml:Attribute Name="Role" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic">
+ <saml:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:string">view-profile</saml:AttributeValue>
+ </saml:Attribute>
+ </saml:AttributeStatement>
+ </saml:Assertion>
+</samlp:Response>
\ No newline at end of file
diff --git a/saml-core/src/test/resources/org/keycloak/saml/processing/core/parsers/saml/KEYCLOAK-4040-sharefile-metadata.xml b/saml-core/src/test/resources/org/keycloak/saml/processing/core/parsers/saml/KEYCLOAK-4040-sharefile-metadata.xml
new file mode 100644
index 0000000..7fe7824
--- /dev/null
+++ b/saml-core/src/test/resources/org/keycloak/saml/processing/core/parsers/saml/KEYCLOAK-4040-sharefile-metadata.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<md:EntityDescriptor entityID="https://pradeepkumar74.sharefile.com/saml/info" ID="_a0263555950f54cb56ead771a6f3516e" xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata">
+ <md:SPSSODescriptor AuthnRequestsSigned="False" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
+ <md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress</md:NameIDFormat>
+ <md:AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://pradeepkumar74.sharefile.com/saml/acs" index="1" isDefault="true" />
+ <md:AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://pradeepkumar74.sf-api.com/sf/v3/Sessions/Acs" index="2" isDefault="false" />
+ </md:SPSSODescriptor>
+ <Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
+ <SignedInfo>
+ <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" />
+ <SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1" />
+ <Reference URI="#https://pradeepkumar74.sharefile.com/saml/info">
+ <Transforms>
+ <Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature" />
+ <Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" />
+ </Transforms>
+ <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1" />
+ <DigestValue>Ldu0001hripzSq7zbIMTEKnQCOU=</DigestValue>
+ </Reference>
+ </SignedInfo>
+ <SignatureValue>J6IRDZ9RmdtpuwTlEK9HjqtbyeSA2Vz9mXF8yYRLIM0qMxRIvPLiIk02UuzCzEJ1I1xT4pcFuUfrdgDG6r9yf2iS+lV7jd0+DdXTHQ4VbQAZRC3Xd8wJ2RnnbZ3gwbIBBYurnWpKI0OCm0MnGvqV75n5Q6iF5jKA8Y4cFp60HHHnCH4QzpVTV5LjSg91eJA1X+99Xga+sK8Z+ln9wBzsrevz6ZfMt24rOMtb64wfAitz+HiD542Ta2TrzKQTnx+EPcr8xBwC62Gl+lIeE3DwKxtNk8pM8mq42D2b5UVKzjfL+PsYZ8XXBwwnwxFs40uxiI/ivq6KuQ/INt4Z5wmjGw==</SignatureValue>
+ <KeyInfo>
+ <X509Data>
+ <X509Certificate>MIIFUjCCBDqgAwIBAgIDBbH0MA0GCSqGSIb3DQEBBQUAMGExCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1HZW9UcnVzdCBJbmMuMR0wGwYDVQQLExREb21haW4gVmFsaWRhdGVkIFNTTDEbMBkGA1UEAxMSR2VvVHJ1c3QgRFYgU1NMIENBMB4XDTEyMTIxODAyMjcxOVoXDTE2MTIyMDAwNTYwMFowgckxKTAnBgNVBAUTIHlMbC1HNjFUWWtiaHBaL1JMTnNIYU8vTmJyWEVQOXc1MRMwEQYDVQQLEwpHVDQ4MjA0OTU4MTEwLwYDVQQLEyhTZWUgd3d3Lmdlb3RydXN0LmNvbS9yZXNvdXJjZXMvY3BzIChjKTEyMTcwNQYDVQQLEy5Eb21haW4gQ29udHJvbCBWYWxpZGF0ZWQgLSBRdWlja1NTTChSKSBQcmVtaXVtMRswGQYDVQQDExJzYW1sLnNoYXJlZmlsZS5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDPUZyhTw3RmP7Y7v06aHgTNuv/Fm0PbGWbGlZEwqr8TGabocPbnb8iTBWAL2ECXMbx+VrpaHiSOVxqC2Y/vDXOs+1r0CzRKeMC6oQPsXZbieW6HxOAv3UVShxc9nfWI6+immo/o3BYI5WKcOaeZieVlDq7a7ctfSUJXHEBhpaSJNhghb+cUZtp1/EXs8/LyVQ31coo1q726WjCvFVB8OUU2u6BQLcbJF5aG3qh5CkNyivwM3NtNAyHhSXRmwyE+Yv5YNo5QAtUagCGYmS2saEJj8FxhXsNRtfW5B6vVhgmNreTcHCcWTpFGhjvferPjsjaIQAs3P2zx/pW/GSCXHy1AgMBAAGjggGoMIIBpDAfBgNVHSMEGDAWgBSM9NmTCke8AKBKzkt1bqC2sLJ+/DAOBgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMB0GA1UdEQQWMBSCEnNhbWwuc2hhcmVmaWxlLmNvbTBBBgNVHR8EOjA4MDagNKAyhjBodHRwOi8vZ3Rzc2xkdi1jcmwuZ2VvdHJ1c3QuY29tL2NybHMvZ3Rzc2xkdi5jcmwwHQYDVR0OBBYEFIDTam2PfOzLpQoclHMwfsUtSi3kMAwGA1UdEwEB/wQCMAAwdQYIKwYBBQUHAQEEaTBnMCwGCCsGAQUFBzABhiBodHRwOi8vZ3Rzc2xkdi1vY3NwLmdlb3RydXN0LmNvbTA3BggrBgEFBQcwAoYraHR0cDovL2d0c3NsZHYtYWlhLmdlb3RydXN0LmNvbS9ndHNzbGR2LmNydDBMBgNVHSAERTBDMEEGCmCGSAGG+EUBBzYwMzAxBggrBgEFBQcCARYlaHR0cDovL3d3dy5nZW90cnVzdC5jb20vcmVzb3VyY2VzL2NwczANBgkqhkiG9w0BAQUFAAOCAQEAU0I6sMe1ZgJ27pdu9qhQLMIgt0w7CuEbLfsSZZdo5TXEj15SGQwU2A0F6o5ivdAvMWTCISJsjHdqCkvB6ZOdMHIfSqA9ARLqX7wLKYfM8X/4RM3koHfqHOvxXBLqCLj2mn34oZrMU5CVI6rqbMoU4D61io7DVswR7Dss0rCh1b1o52ZEBjy5w9oJhRTEFwL7ekf6tR9UioyxQ37pGfD8qOpX1hj5gqcZ5+qUSVNjOjeh+9e9OO5Y/ns3jjHK5ieZPdYeLLOp+D6qzAnOERgvKvkPyRIHZA9tAjxj5KIEzQUopmbP7oH4Ovo6YXT+iIuMVvX3dDu00ExOSZjEeDzo/w==</X509Certificate>
+ </X509Data>
+ </KeyInfo>
+ </Signature>
+ <md:Organization>
+ <md:OrganizationName xml:lang="en">ShareFile.com</md:OrganizationName>
+ <md:OrganizationDisplayName xml:lang="en">ShareFile.com, a division of Citrix Systems, Inc</md:OrganizationDisplayName>
+ <md:OrganizationUrl xml:lang="en">https://www.sharefile.com</md:OrganizationUrl>
+ </md:Organization>
+</md:EntityDescriptor>
\ No newline at end of file
diff --git a/saml-core-api/src/main/java/org/keycloak/saml/common/constants/GeneralConstants.java b/saml-core-api/src/main/java/org/keycloak/saml/common/constants/GeneralConstants.java
index accac66..c203c78 100755
--- a/saml-core-api/src/main/java/org/keycloak/saml/common/constants/GeneralConstants.java
+++ b/saml-core-api/src/main/java/org/keycloak/saml/common/constants/GeneralConstants.java
@@ -16,6 +16,7 @@
*/
package org.keycloak.saml.common.constants;
+import java.nio.charset.Charset;
/**
* Constants
@@ -147,4 +148,7 @@ public interface GeneralConstants {
String BASE64_ENCODE_WSTRUST_SECRET_KEY = "picketlink.wstrust.base64_encode_wstrust_secret_key";
String HTTP_HEADER_X_REQUESTED_WITH = "X-Requested-With";
+
+ public static final String SAML_CHARSET_NAME = System.getProperty("keycloak.saml.saml_message_charset", "UTF-8");
+ public static final Charset SAML_CHARSET = Charset.forName(SAML_CHARSET_NAME);
}
diff --git a/saml-core-api/src/main/java/org/keycloak/saml/common/constants/JBossSAMLConstants.java b/saml-core-api/src/main/java/org/keycloak/saml/common/constants/JBossSAMLConstants.java
index c6c56ac..d0b8b85 100755
--- a/saml-core-api/src/main/java/org/keycloak/saml/common/constants/JBossSAMLConstants.java
+++ b/saml-core-api/src/main/java/org/keycloak/saml/common/constants/JBossSAMLConstants.java
@@ -48,7 +48,8 @@ public enum JBossSAMLConstants {
"NameFormat"), NAMEID("NameID"), NAMEID_FORMAT("NameIDFormat"), NAMEID_MAPPING_SERVICE("NameIDMappingService"), NAMEID_POLICY(
"NameIDPolicy"), NAME_QUALIFIER("NameQualifier"), NOT_BEFORE("NotBefore"), NOT_ON_OR_AFTER("NotOnOrAfter"), ORGANIZATION(
"Organization"), ORGANIZATION_NAME("OrganizationName"), ORGANIZATION_DISPLAY_NAME("OrganizationDisplayName"), ORGANIZATION_URL(
- "OrganizationURL"), PDP_DESCRIPTOR("PDPDescriptor"), PROTOCOL_BINDING("ProtocolBinding"), PROTOCOL_SUPPORT_ENUMERATION(
+ "OrganizationURL"), ORGANIZATION_URL_ALT(
+ "OrganizationUrl"), PDP_DESCRIPTOR("PDPDescriptor"), PROTOCOL_BINDING("ProtocolBinding"), PROTOCOL_SUPPORT_ENUMERATION(
"protocolSupportEnumeration"), PROVIDER_NAME("ProviderName"), REQUESTED_AUTHN_CONTEXT("RequestedAuthnContext"), REASON(
"Reason"), RECIPIENT("Recipient"), REQUEST("Request"), REQUESTED_ATTRIBUTE("RequestedAttribute"), REQUEST_ABSTRACT(
"RequestAbstract"), RESPONSE("Response"), RESPONSE_LOCATION("ResponseLocation"), RETURN_CONTEXT("ReturnContext"), SESSION_INDEX(
diff --git a/server-spi/src/main/java/org/keycloak/keys/HmacKeyMetadata.java b/server-spi/src/main/java/org/keycloak/keys/HmacKeyMetadata.java
new file mode 100644
index 0000000..0942819
--- /dev/null
+++ b/server-spi/src/main/java/org/keycloak/keys/HmacKeyMetadata.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.keys;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class HmacKeyMetadata extends KeyMetadata {
+
+}
diff --git a/server-spi/src/main/java/org/keycloak/keys/KeyMetadata.java b/server-spi/src/main/java/org/keycloak/keys/KeyMetadata.java
index b2d0a58..c9adebd 100644
--- a/server-spi/src/main/java/org/keycloak/keys/KeyMetadata.java
+++ b/server-spi/src/main/java/org/keycloak/keys/KeyMetadata.java
@@ -17,22 +17,15 @@
package org.keycloak.keys;
-import java.security.PublicKey;
-import java.security.cert.Certificate;
-
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
-public class KeyMetadata {
+public abstract class KeyMetadata {
public enum Status {
ACTIVE, PASSIVE, DISABLED
}
- public enum Type {
- RSA
- }
-
private String providerId;
private long providerPriority;
@@ -40,11 +33,6 @@ public class KeyMetadata {
private Status status;
- private Type type;
-
- private PublicKey publicKey;
- private Certificate certificate;
-
public String getProviderId() {
return providerId;
}
@@ -77,28 +65,4 @@ public class KeyMetadata {
this.status = status;
}
- public Type getType() {
- return type;
- }
-
- public void setType(Type type) {
- this.type = type;
- }
-
- public PublicKey getPublicKey() {
- return publicKey;
- }
-
- public void setPublicKey(PublicKey publicKey) {
- this.publicKey = publicKey;
- }
-
- public Certificate getCertificate() {
- return certificate;
- }
-
- public void setCertificate(Certificate certificate) {
- this.certificate = certificate;
- }
-
}
diff --git a/server-spi/src/main/java/org/keycloak/keys/RsaKeyMetadata.java b/server-spi/src/main/java/org/keycloak/keys/RsaKeyMetadata.java
new file mode 100644
index 0000000..a8c0648
--- /dev/null
+++ b/server-spi/src/main/java/org/keycloak/keys/RsaKeyMetadata.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.keys;
+
+import java.security.PublicKey;
+import java.security.cert.Certificate;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class RsaKeyMetadata extends KeyMetadata {
+
+ private PublicKey publicKey;
+ private Certificate certificate;
+
+ public PublicKey getPublicKey() {
+ return publicKey;
+ }
+
+ public void setPublicKey(PublicKey publicKey) {
+ this.publicKey = publicKey;
+ }
+
+ public Certificate getCertificate() {
+ return certificate;
+ }
+
+ public void setCertificate(Certificate certificate) {
+ this.certificate = certificate;
+ }
+
+}
diff --git a/server-spi/src/main/java/org/keycloak/models/KeyManager.java b/server-spi/src/main/java/org/keycloak/models/KeyManager.java
index 757e9a9..3916468 100644
--- a/server-spi/src/main/java/org/keycloak/models/KeyManager.java
+++ b/server-spi/src/main/java/org/keycloak/models/KeyManager.java
@@ -17,8 +17,10 @@
package org.keycloak.models;
-import org.keycloak.keys.KeyMetadata;
+import org.keycloak.keys.HmacKeyMetadata;
+import org.keycloak.keys.RsaKeyMetadata;
+import javax.crypto.SecretKey;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.cert.Certificate;
@@ -30,21 +32,27 @@ import java.util.List;
*/
public interface KeyManager {
- ActiveKey getActiveKey(RealmModel realm);
+ ActiveRsaKey getActiveRsaKey(RealmModel realm);
- PublicKey getPublicKey(RealmModel realm, String kid);
+ PublicKey getRsaPublicKey(RealmModel realm, String kid);
- Certificate getCertificate(RealmModel realm, String kid);
+ Certificate getRsaCertificate(RealmModel realm, String kid);
- List<KeyMetadata> getKeys(RealmModel realm, boolean includeDisabled);
+ List<RsaKeyMetadata> getRsaKeys(RealmModel realm, boolean includeDisabled);
- class ActiveKey {
+ ActiveHmacKey getActiveHmacKey(RealmModel realm);
+
+ SecretKey getHmacSecretKey(RealmModel realm, String kid);
+
+ List<HmacKeyMetadata> getHmacKeys(RealmModel realm, boolean includeDisabled);
+
+ class ActiveRsaKey {
private final String kid;
private final PrivateKey privateKey;
private final PublicKey publicKey;
private final X509Certificate certificate;
- public ActiveKey(String kid, PrivateKey privateKey, PublicKey publicKey, X509Certificate certificate) {
+ public ActiveRsaKey(String kid, PrivateKey privateKey, PublicKey publicKey, X509Certificate certificate) {
this.kid = kid;
this.privateKey = privateKey;
this.publicKey = publicKey;
@@ -68,4 +76,22 @@ public interface KeyManager {
}
}
+ class ActiveHmacKey {
+ private final String kid;
+ private final SecretKey secretKey;
+
+ public ActiveHmacKey(String kid, SecretKey secretKey) {
+ this.kid = kid;
+ this.secretKey = secretKey;
+ }
+
+ public String getKid() {
+ return kid;
+ }
+
+ public SecretKey getSecretKey() {
+ return secretKey;
+ }
+ }
+
}
diff --git a/server-spi/src/main/java/org/keycloak/models/RealmModel.java b/server-spi/src/main/java/org/keycloak/models/RealmModel.java
index 3149f50..7640aad 100755
--- a/server-spi/src/main/java/org/keycloak/models/RealmModel.java
+++ b/server-spi/src/main/java/org/keycloak/models/RealmModel.java
@@ -23,10 +23,6 @@ import org.keycloak.provider.ProviderEvent;
import org.keycloak.storage.UserStorageProvider;
import org.keycloak.storage.UserStorageProviderModel;
-import java.security.Key;
-import java.security.PrivateKey;
-import java.security.PublicKey;
-import java.security.cert.X509Certificate;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
@@ -149,6 +145,14 @@ public interface RealmModel extends RoleContainerModel {
boolean isVerifyEmail();
void setVerifyEmail(boolean verifyEmail);
+
+ boolean isLoginWithEmailAllowed();
+
+ void setLoginWithEmailAllowed(boolean loginWithEmailAllowed);
+
+ boolean isDuplicateEmailsAllowed();
+
+ void setDuplicateEmailsAllowed(boolean duplicateEmailsAllowed);
boolean isResetPasswordAllowed();
diff --git a/server-spi/src/main/java/org/keycloak/provider/Provider.java b/server-spi/src/main/java/org/keycloak/provider/Provider.java
index 6997b26..ab6216d 100644
--- a/server-spi/src/main/java/org/keycloak/provider/Provider.java
+++ b/server-spi/src/main/java/org/keycloak/provider/Provider.java
@@ -22,6 +22,6 @@ package org.keycloak.provider;
*/
public interface Provider {
- public void close();
+ void close();
}
diff --git a/server-spi/src/main/java/org/keycloak/storage/user/UserQueryProvider.java b/server-spi/src/main/java/org/keycloak/storage/user/UserQueryProvider.java
index eb179c9..8143dc4 100644
--- a/server-spi/src/main/java/org/keycloak/storage/user/UserQueryProvider.java
+++ b/server-spi/src/main/java/org/keycloak/storage/user/UserQueryProvider.java
@@ -85,7 +85,7 @@ public interface UserQueryProvider {
List<UserModel> searchForUser(Map<String, String> params, RealmModel realm);
/**
- * Search for user by parameter. Valid parameters are:
+ * Search for user by parameter. Valid parameters are:
* "first" - first name
* "last" - last name
* "email" - email
diff --git a/server-spi-private/src/main/java/org/keycloak/authorization/AuthorizationProvider.java b/server-spi-private/src/main/java/org/keycloak/authorization/AuthorizationProvider.java
index fb7c91b..3aafae3 100644
--- a/server-spi-private/src/main/java/org/keycloak/authorization/AuthorizationProvider.java
+++ b/server-spi-private/src/main/java/org/keycloak/authorization/AuthorizationProvider.java
@@ -18,6 +18,12 @@
package org.keycloak.authorization;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Executor;
+import java.util.function.Supplier;
+
import org.keycloak.authorization.permission.evaluator.Evaluators;
import org.keycloak.authorization.policy.evaluation.DefaultPolicyEvaluator;
import org.keycloak.authorization.policy.provider.PolicyProvider;
@@ -26,11 +32,6 @@ import org.keycloak.authorization.store.StoreFactory;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.provider.Provider;
-import org.keycloak.provider.ProviderFactory;
-
-import java.util.List;
-import java.util.concurrent.Executor;
-import java.util.stream.Collectors;
/**
* <p>The main contract here is the creation of {@link org.keycloak.authorization.permission.evaluator.PermissionEvaluator} instances. Usually
@@ -62,22 +63,22 @@ public final class AuthorizationProvider implements Provider {
private final DefaultPolicyEvaluator policyEvaluator;
private final Executor scheduller;
- private final StoreFactory storeFactory;
- private final List<PolicyProviderFactory> policyProviderFactories;
+ private final Supplier<StoreFactory> storeFactory;
+ private final Map<String, PolicyProviderFactory> policyProviderFactories;
private final KeycloakSession keycloakSession;
private final RealmModel realm;
- public AuthorizationProvider(KeycloakSession session, RealmModel realm, StoreFactory storeFactory, Executor scheduller) {
+ public AuthorizationProvider(KeycloakSession session, RealmModel realm, Supplier<StoreFactory> storeFactory, Map<String, PolicyProviderFactory> policyProviderFactories, Executor scheduller) {
this.keycloakSession = session;
this.realm = realm;
this.storeFactory = storeFactory;
this.scheduller = scheduller;
- this.policyProviderFactories = configurePolicyProviderFactories(session);
- this.policyEvaluator = new DefaultPolicyEvaluator(this, this.policyProviderFactories);
+ this.policyProviderFactories = policyProviderFactories;
+ this.policyEvaluator = new DefaultPolicyEvaluator(this);
}
- public AuthorizationProvider(KeycloakSession session, RealmModel realm, StoreFactory storeFactory) {
- this(session, realm, storeFactory, Runnable::run);
+ public AuthorizationProvider(KeycloakSession session, RealmModel realm, StoreFactory storeFactory, Map<String, PolicyProviderFactory> policyProviderFactories) {
+ this(session, realm, () -> storeFactory, policyProviderFactories, Runnable::run);
}
/**
@@ -87,7 +88,7 @@ public final class AuthorizationProvider implements Provider {
* @return a {@link Evaluators} instance
*/
public Evaluators evaluators() {
- return new Evaluators(this.policyProviderFactories, this.policyEvaluator, this.scheduller);
+ return new Evaluators(this.policyEvaluator, this.scheduller);
}
/**
@@ -96,7 +97,7 @@ public final class AuthorizationProvider implements Provider {
* @return the {@link StoreFactory}
*/
public StoreFactory getStoreFactory() {
- return this.storeFactory;
+ return this.storeFactory.get();
}
/**
@@ -104,8 +105,8 @@ public final class AuthorizationProvider implements Provider {
*
* @return a {@link List} containing all registered {@link PolicyProviderFactory}
*/
- public List<PolicyProviderFactory> getProviderFactories() {
- return this.policyProviderFactories;
+ public Collection<PolicyProviderFactory> getProviderFactories() {
+ return this.policyProviderFactories.values();
}
/**
@@ -116,7 +117,24 @@ public final class AuthorizationProvider implements Provider {
* @return a {@link PolicyProviderFactory} with the given <code>type</code>
*/
public <F extends PolicyProviderFactory> F getProviderFactory(String type) {
- return (F) getProviderFactories().stream().filter(policyProviderFactory -> policyProviderFactory.getId().equals(type)).findFirst().orElse(null);
+ return (F) policyProviderFactories.get(type);
+ }
+
+ /**
+ * Returns a {@link PolicyProviderFactory} given a <code>type</code>.
+ *
+ * @param type the type of the policy provider
+ * @param <F> the expected type of the provider
+ * @return a {@link PolicyProvider} with the given <code>type</code>
+ */
+ public <P extends PolicyProvider> P getProvider(String type) {
+ PolicyProviderFactory policyProviderFactory = policyProviderFactories.get(type);
+
+ if (policyProviderFactory == null) {
+ return null;
+ }
+
+ return (P) policyProviderFactory.create(this);
}
public KeycloakSession getKeycloakSession() {
@@ -127,16 +145,6 @@ public final class AuthorizationProvider implements Provider {
return realm;
}
- private List<PolicyProviderFactory> configurePolicyProviderFactories(KeycloakSession session) {
- List<ProviderFactory> providerFactories = session.getKeycloakSessionFactory().getProviderFactories(PolicyProvider.class);
-
- if (providerFactories.isEmpty()) {
- throw new RuntimeException("Could not find any policy provider.");
- }
-
- return providerFactories.stream().map(providerFactory -> (PolicyProviderFactory) providerFactory).collect(Collectors.toList());
- }
-
@Override
public void close() {
diff --git a/server-spi-private/src/main/java/org/keycloak/authorization/permission/evaluator/Evaluators.java b/server-spi-private/src/main/java/org/keycloak/authorization/permission/evaluator/Evaluators.java
index e26ad1c..ed1aa89 100644
--- a/server-spi-private/src/main/java/org/keycloak/authorization/permission/evaluator/Evaluators.java
+++ b/server-spi-private/src/main/java/org/keycloak/authorization/permission/evaluator/Evaluators.java
@@ -18,13 +18,12 @@
package org.keycloak.authorization.permission.evaluator;
+import java.util.List;
+import java.util.concurrent.Executor;
+
import org.keycloak.authorization.permission.ResourcePermission;
import org.keycloak.authorization.policy.evaluation.DefaultPolicyEvaluator;
import org.keycloak.authorization.policy.evaluation.EvaluationContext;
-import org.keycloak.authorization.policy.provider.PolicyProviderFactory;
-
-import java.util.List;
-import java.util.concurrent.Executor;
/**
* A factory for the different {@link PermissionEvaluator} implementations.
@@ -33,12 +32,10 @@ import java.util.concurrent.Executor;
*/
public final class Evaluators {
- private final List<PolicyProviderFactory> policyProviderFactories;
private final DefaultPolicyEvaluator policyEvaluator;
private final Executor scheduler;
- public Evaluators(List<PolicyProviderFactory> policyProviderFactories, DefaultPolicyEvaluator policyEvaluator, Executor scheduler) {
- this.policyProviderFactories = policyProviderFactories;
+ public Evaluators(DefaultPolicyEvaluator policyEvaluator, Executor scheduler) {
this.policyEvaluator = policyEvaluator;
this.scheduler = scheduler;
}
diff --git a/server-spi-private/src/main/java/org/keycloak/authorization/permission/evaluator/ScheduledPermissionEvaluator.java b/server-spi-private/src/main/java/org/keycloak/authorization/permission/evaluator/ScheduledPermissionEvaluator.java
index 13e08e4..4630507 100644
--- a/server-spi-private/src/main/java/org/keycloak/authorization/permission/evaluator/ScheduledPermissionEvaluator.java
+++ b/server-spi-private/src/main/java/org/keycloak/authorization/permission/evaluator/ScheduledPermissionEvaluator.java
@@ -17,11 +17,10 @@
*/
package org.keycloak.authorization.permission.evaluator;
-import org.keycloak.authorization.Decision;
-
-import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
+import org.keycloak.authorization.Decision;
+
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
* @see PermissionEvaluator
@@ -38,6 +37,6 @@ class ScheduledPermissionEvaluator implements PermissionEvaluator {
@Override
public void evaluate(Decision decision) {
- CompletableFuture.runAsync(() -> publisher.evaluate(decision), scheduler);
+ publisher.evaluate(decision);
}
}
diff --git a/server-spi-private/src/main/java/org/keycloak/authorization/policy/evaluation/DecisionResultCollector.java b/server-spi-private/src/main/java/org/keycloak/authorization/policy/evaluation/DecisionResultCollector.java
index abd3f93..2866360 100644
--- a/server-spi-private/src/main/java/org/keycloak/authorization/policy/evaluation/DecisionResultCollector.java
+++ b/server-spi-private/src/main/java/org/keycloak/authorization/policy/evaluation/DecisionResultCollector.java
@@ -37,8 +37,10 @@ public abstract class DecisionResultCollector implements Decision<DefaultEvaluat
@Override
public void onDecision(DefaultEvaluation evaluation) {
- if (evaluation.getParentPolicy() != null) {
- results.computeIfAbsent(evaluation.getPermission(), Result::new).policy(evaluation.getParentPolicy()).policy(evaluation.getPolicy()).setStatus(evaluation.getEffect());
+ Policy parentPolicy = evaluation.getParentPolicy();
+
+ if (parentPolicy != null) {
+ results.computeIfAbsent(evaluation.getPermission(), Result::new).policy(parentPolicy).policy(evaluation.getPolicy()).setStatus(evaluation.getEffect());
} else {
results.computeIfAbsent(evaluation.getPermission(), Result::new).setStatus(evaluation.getEffect());
}
diff --git a/server-spi-private/src/main/java/org/keycloak/authorization/policy/evaluation/DefaultEvaluation.java b/server-spi-private/src/main/java/org/keycloak/authorization/policy/evaluation/DefaultEvaluation.java
index 0bd5b6c..74a2e07 100644
--- a/server-spi-private/src/main/java/org/keycloak/authorization/policy/evaluation/DefaultEvaluation.java
+++ b/server-spi-private/src/main/java/org/keycloak/authorization/policy/evaluation/DefaultEvaluation.java
@@ -18,6 +18,7 @@
package org.keycloak.authorization.policy.evaluation;
+import org.keycloak.authorization.AuthorizationProvider;
import org.keycloak.authorization.Decision;
import org.keycloak.authorization.Decision.Effect;
import org.keycloak.authorization.model.Policy;
@@ -34,37 +35,29 @@ public class DefaultEvaluation implements Evaluation {
private final Decision decision;
private final Policy policy;
private final Policy parentPolicy;
+ private final AuthorizationProvider authorizationProvider;
private Effect effect;
- public DefaultEvaluation(ResourcePermission permission, EvaluationContext executionContext, Policy parentPolicy, Policy policy, Decision decision) {
+ public DefaultEvaluation(ResourcePermission permission, EvaluationContext executionContext, Policy parentPolicy, Policy policy, Decision decision, AuthorizationProvider authorizationProvider) {
this.permission = permission;
this.executionContext = executionContext;
this.parentPolicy = parentPolicy;
this.policy = policy;
this.decision = decision;
+ this.authorizationProvider = authorizationProvider;
}
- /**
- * Returns the {@link ResourcePermission} to be evaluated.
- *
- * @return the permission to be evaluated
- */
+ @Override
public ResourcePermission getPermission() {
return this.permission;
}
- /**
- * Returns the {@link org.keycloak.authorization.permission.evaluator.PermissionEvaluator}. Which provides access to the whole evaluation runtime context.
- *
- * @return the evaluation context
- */
+ @Override
public EvaluationContext getContext() {
return this.executionContext;
}
- /**
- * Grants all the requested permissions to the caller.
- */
+ @Override
public void grant() {
if (policy != null && Logic.NEGATIVE.equals(policy.getLogic())) {
this.effect = Effect.DENY;
@@ -75,6 +68,7 @@ public class DefaultEvaluation implements Evaluation {
this.decision.onDecision(this);
}
+ @Override
public void deny() {
if (policy != null && Logic.NEGATIVE.equals(policy.getLogic())) {
this.effect = Effect.PERMIT;
@@ -85,10 +79,16 @@ public class DefaultEvaluation implements Evaluation {
this.decision.onDecision(this);
}
+ @Override
public Policy getPolicy() {
return this.policy;
}
+ @Override
+ public AuthorizationProvider getAuthorizationProvider() {
+ return authorizationProvider;
+ }
+
public Policy getParentPolicy() {
return this.parentPolicy;
}
diff --git a/server-spi-private/src/main/java/org/keycloak/authorization/policy/evaluation/DefaultPolicyEvaluator.java b/server-spi-private/src/main/java/org/keycloak/authorization/policy/evaluation/DefaultPolicyEvaluator.java
index 207c89e..b74f845 100644
--- a/server-spi-private/src/main/java/org/keycloak/authorization/policy/evaluation/DefaultPolicyEvaluator.java
+++ b/server-spi-private/src/main/java/org/keycloak/authorization/policy/evaluation/DefaultPolicyEvaluator.java
@@ -18,6 +18,14 @@
package org.keycloak.authorization.policy.evaluation;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+import java.util.stream.Collectors;
+
import org.keycloak.authorization.AuthorizationProvider;
import org.keycloak.authorization.Decision;
import org.keycloak.authorization.model.Policy;
@@ -26,105 +34,95 @@ import org.keycloak.authorization.model.ResourceServer;
import org.keycloak.authorization.model.Scope;
import org.keycloak.authorization.permission.ResourcePermission;
import org.keycloak.authorization.policy.provider.PolicyProvider;
-import org.keycloak.authorization.policy.provider.PolicyProviderFactory;
import org.keycloak.authorization.store.PolicyStore;
import org.keycloak.authorization.store.StoreFactory;
import org.keycloak.representations.idm.authorization.PolicyEnforcementMode;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.concurrent.atomic.AtomicInteger;
-import java.util.function.Consumer;
-import java.util.stream.Collectors;
-
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
public class DefaultPolicyEvaluator implements PolicyEvaluator {
private final AuthorizationProvider authorization;
- private Map<String, PolicyProviderFactory> policyProviders = new HashMap<>();
+ private final StoreFactory storeFactory;
+ private final PolicyStore policyStore;
- public DefaultPolicyEvaluator(AuthorizationProvider authorization, List<PolicyProviderFactory> policyProviderFactories) {
+ public DefaultPolicyEvaluator(AuthorizationProvider authorization) {
this.authorization = authorization;
-
- for (PolicyProviderFactory providerFactory : policyProviderFactories) {
- this.policyProviders.put(providerFactory.getId(), providerFactory);
- }
+ storeFactory = this.authorization.getStoreFactory();
+ policyStore = storeFactory.getPolicyStore();
}
@Override
public void evaluate(ResourcePermission permission, EvaluationContext executionContext, Decision decision) {
ResourceServer resourceServer = permission.getResourceServer();
+ PolicyEnforcementMode enforcementMode = resourceServer.getPolicyEnforcementMode();
- if (PolicyEnforcementMode.DISABLED.equals(resourceServer.getPolicyEnforcementMode())) {
+ if (PolicyEnforcementMode.DISABLED.equals(enforcementMode)) {
createEvaluation(permission, executionContext, decision, null, null).grant();
return;
}
- StoreFactory storeFactory = this.authorization.getStoreFactory();
- PolicyStore policyStore = storeFactory.getPolicyStore();
- AtomicInteger policiesCount = new AtomicInteger(0);
- Consumer<Policy> consumer = createDecisionConsumer(permission, executionContext, decision, policiesCount);
+ AtomicBoolean verified = new AtomicBoolean(false);
+ Consumer<Policy> consumer = createDecisionConsumer(permission, executionContext, decision, verified);
Resource resource = permission.getResource();
+ List<Scope> scopes = permission.getScopes();
if (resource != null) {
- List<? extends Policy> resourcePolicies = policyStore.findByResource(resource.getId());
-
- if (!resourcePolicies.isEmpty()) {
- resourcePolicies.forEach(consumer);
- }
+ evaluatePolicies(() -> policyStore.findByResource(resource.getId(), resourceServer.getId()), consumer);
if (resource.getType() != null) {
- policyStore.findByResourceType(resource.getType(), resourceServer.getId()).forEach(consumer);
+ evaluatePolicies(() -> policyStore.findByResourceType(resource.getType(), resourceServer.getId()), consumer);
}
- if (permission.getScopes().isEmpty() && !resource.getScopes().isEmpty()) {
- policyStore.findByScopeIds(resource.getScopes().stream().map(Scope::getId).collect(Collectors.toList()), resourceServer.getId()).forEach(consumer);
+ if (scopes.isEmpty() && !resource.getScopes().isEmpty()) {
+ scopes.removeAll(resource.getScopes());
+ evaluatePolicies(() -> policyStore.findByScopeIds(resource.getScopes().stream().map(Scope::getId).collect(Collectors.toList()), resourceServer.getId()), consumer);
}
}
- if (!permission.getScopes().isEmpty()) {
- policyStore.findByScopeIds(permission.getScopes().stream().map(Scope::getId).collect(Collectors.toList()), resourceServer.getId()).forEach(consumer);
+ if (!scopes.isEmpty()) {
+ evaluatePolicies(() -> policyStore.findByScopeIds(scopes.stream().map(Scope::getId).collect(Collectors.toList()), resourceServer.getId()), consumer);
}
- if (PolicyEnforcementMode.PERMISSIVE.equals(resourceServer.getPolicyEnforcementMode()) && policiesCount.get() == 0) {
+ if (PolicyEnforcementMode.PERMISSIVE.equals(enforcementMode) && verified.get()) {
createEvaluation(permission, executionContext, decision, null, null).grant();
}
}
- private Consumer<Policy> createDecisionConsumer(ResourcePermission permission, EvaluationContext executionContext, Decision decision, AtomicInteger policiesCount) {
- return (parentPolicy) -> {
- if (hasRequestedScopes(permission, parentPolicy)) {
- for (Policy associatedPolicy : parentPolicy.getAssociatedPolicies()) {
- PolicyProviderFactory providerFactory = policyProviders.get(associatedPolicy.getType());
+ private void evaluatePolicies(Supplier<List<Policy>> supplier, Consumer<Policy> consumer) {
+ List<Policy> policies = supplier.get();
- if (providerFactory == null) {
- throw new RuntimeException("Could not find a policy provider for policy type [" + associatedPolicy.getType() + "].");
- }
+ if (!policies.isEmpty()) {
+ policies.forEach(consumer);
+ }
+ }
- PolicyProvider policyProvider = providerFactory.create(associatedPolicy, this.authorization);
+ private Consumer<Policy> createDecisionConsumer(ResourcePermission permission, EvaluationContext executionContext, Decision decision, AtomicBoolean verified) {
+ return (parentPolicy) -> {
+ if (!hasRequestedScopes(permission, parentPolicy)) {
+ return;
+ }
- if (policyProvider == null) {
- throw new RuntimeException("Unknown parentPolicy provider for type [" + associatedPolicy.getType() + "].");
- }
+ for (Policy associatedPolicy : parentPolicy.getAssociatedPolicies()) {
+ PolicyProvider policyProvider = authorization.getProvider(associatedPolicy.getType());
- DefaultEvaluation evaluation = createEvaluation(permission, executionContext, decision, parentPolicy, associatedPolicy);
+ if (policyProvider == null) {
+ throw new RuntimeException("Unknown parentPolicy provider for type [" + associatedPolicy.getType() + "].");
+ }
- policyProvider.evaluate(evaluation);
- evaluation.denyIfNoEffect();
+ DefaultEvaluation evaluation = createEvaluation(permission, executionContext, decision, parentPolicy, associatedPolicy);
- policiesCount.incrementAndGet();
- }
+ policyProvider.evaluate(evaluation);
+ evaluation.denyIfNoEffect();
}
+
+ verified.compareAndSet(false, true);
};
}
private DefaultEvaluation createEvaluation(ResourcePermission permission, EvaluationContext executionContext, Decision decision, Policy parentPolicy, Policy associatedPolicy) {
- return new DefaultEvaluation(permission, executionContext, parentPolicy, associatedPolicy, decision);
+ return new DefaultEvaluation(permission, executionContext, parentPolicy, associatedPolicy, decision, authorization);
}
private boolean hasRequestedScopes(final ResourcePermission permission, final Policy policy) {
@@ -136,7 +134,7 @@ public class DefaultPolicyEvaluator implements PolicyEvaluator {
Set<Resource> policyResources = policy.getResources();
if (resourcePermission != null && !policyResources.isEmpty()) {
- if (!policyResources.stream().filter(resource -> resource.getId().equals(resourcePermission.getId())).findFirst().isPresent()) {
+ if (!policyResources.stream().filter(resource -> resource.getId().equals(resourcePermission.getId())).findFirst().isPresent()) {
return false;
}
}
@@ -161,7 +159,7 @@ public class DefaultPolicyEvaluator implements PolicyEvaluator {
String type = resource.getType();
if (type != null) {
- List<Resource> resourcesByType = authorization.getStoreFactory().getResourceStore().findByType(type);
+ List<Resource> resourcesByType = authorization.getStoreFactory().getResourceStore().findByType(type, resource.getResourceServer().getId());
for (Resource resourceType : resourcesByType) {
if (resourceType.getOwner().equals(resource.getResourceServer().getClientId())) {
diff --git a/server-spi-private/src/main/java/org/keycloak/authorization/policy/evaluation/Evaluation.java b/server-spi-private/src/main/java/org/keycloak/authorization/policy/evaluation/Evaluation.java
index f5b0868..4ac0264 100644
--- a/server-spi-private/src/main/java/org/keycloak/authorization/policy/evaluation/Evaluation.java
+++ b/server-spi-private/src/main/java/org/keycloak/authorization/policy/evaluation/Evaluation.java
@@ -18,6 +18,8 @@
package org.keycloak.authorization.policy.evaluation;
+import org.keycloak.authorization.AuthorizationProvider;
+import org.keycloak.authorization.model.Policy;
import org.keycloak.authorization.permission.ResourcePermission;
/**
@@ -43,6 +45,15 @@ public interface Evaluation {
EvaluationContext getContext();
/**
+ * Returns the {@link Policy}. being evaluated.
+ *
+ * @return the evaluation context
+ */
+ Policy getPolicy();
+
+ AuthorizationProvider getAuthorizationProvider();
+
+ /**
* Grants the requested permission to the caller.
*/
void grant();
diff --git a/server-spi-private/src/main/java/org/keycloak/authorization/policy/provider/PolicyProviderFactory.java b/server-spi-private/src/main/java/org/keycloak/authorization/policy/provider/PolicyProviderFactory.java
index f82bdb7..f7041b5 100644
--- a/server-spi-private/src/main/java/org/keycloak/authorization/policy/provider/PolicyProviderFactory.java
+++ b/server-spi-private/src/main/java/org/keycloak/authorization/policy/provider/PolicyProviderFactory.java
@@ -32,7 +32,7 @@ public interface PolicyProviderFactory extends ProviderFactory<PolicyProvider> {
String getGroup();
- PolicyProvider create(Policy policy, AuthorizationProvider authorization);
+ PolicyProvider create(AuthorizationProvider authorization);
PolicyProviderAdminService getAdminResource(ResourceServer resourceServer);
}
diff --git a/server-spi-private/src/main/java/org/keycloak/authorization/store/PolicyStore.java b/server-spi-private/src/main/java/org/keycloak/authorization/store/PolicyStore.java
index 51c7dd7..626e317 100644
--- a/server-spi-private/src/main/java/org/keycloak/authorization/store/PolicyStore.java
+++ b/server-spi-private/src/main/java/org/keycloak/authorization/store/PolicyStore.java
@@ -53,9 +53,10 @@ public interface PolicyStore {
* Returns a {@link Policy} with the given <code>id</code>
*
* @param id the identifier of the policy
+ * @param resourceServerId the resource server id
* @return a policy with the given identifier.
*/
- Policy findById(String id);
+ Policy findById(String id, String resourceServerId);
/**
* Returns a {@link Policy} with the given <code>name</code>
@@ -87,9 +88,10 @@ public interface PolicyStore {
* Returns a list of {@link Policy} associated with a {@link org.keycloak.authorization.core.model.Resource} with the given <code>resourceId</code>.
*
* @param resourceId the identifier of a resource
+ * @param resourceServerId the resource server id
* @return a list of policies associated with the given resource
*/
- List<Policy> findByResource(String resourceId);
+ List<Policy> findByResource(String resourceId, String resourceServerId);
/**
* Returns a list of {@link Policy} associated with a {@link org.keycloak.authorization.core.model.Resource} with the given <code>type</code>.
@@ -113,15 +115,26 @@ public interface PolicyStore {
* Returns a list of {@link Policy} with the given <code>type</code>.
*
* @param type the type of the policy
+ * @param resourceServerId the resource server id
* @return a list of policies with the given type
*/
- List<Policy> findByType(String type);
+ List<Policy> findByType(String type, String resourceServerId);
/**
* Returns a list of {@link Policy} that depends on another policy with the given <code>id</code>.
*
* @param id the id of the policy to query its dependents
+ * @param resourceServerId the resource server id
* @return a list of policies that depends on the a policy with the given identifier
*/
- List<Policy> findDependentPolicies(String id);
+ List<Policy> findDependentPolicies(String id, String resourceServerId);
+
+ /**
+ * Notify this store about changes to data associated with policies. E.g.: resources and scopes..
+ *
+ * TODO: need a better strategy to handle cross-references between stores, specially in cases where the store is caching data. Use some event-based solution here.
+ *
+ * @param cached
+ */
+ default void notifyChange(Object cached) {}
}
diff --git a/server-spi-private/src/main/java/org/keycloak/authorization/store/ResourceStore.java b/server-spi-private/src/main/java/org/keycloak/authorization/store/ResourceStore.java
index f06be78..b55ec74 100644
--- a/server-spi-private/src/main/java/org/keycloak/authorization/store/ResourceStore.java
+++ b/server-spi-private/src/main/java/org/keycloak/authorization/store/ResourceStore.java
@@ -53,7 +53,7 @@ public interface ResourceStore {
* @param id the identifier of an existing resource instance
* @return the resource instance with the given identifier or null if no instance was found
*/
- Resource findById(String id);
+ Resource findById(String id, String resourceServerId);
/**
* Finds all {@link Resource} instances with the given {@code ownerId}.
@@ -61,7 +61,15 @@ public interface ResourceStore {
* @param ownerId the identifier of the owner
* @return a list with all resource instances owned by the given owner
*/
- List<Resource> findByOwner(String ownerId);
+ List<Resource> findByOwner(String ownerId, String resourceServerId);
+
+ /**
+ * Finds all {@link Resource} instances with the given uri.
+ *
+ * @param ownerId the identifier of the owner
+ * @return a list with all resource instances owned by the given owner
+ */
+ List<Resource> findByUri(String uri, String resourceServerId);
/**
* Finds all {@link Resource} instances associated with a given resource server.
@@ -86,7 +94,7 @@ public interface ResourceStore {
* @param id one or more scope identifiers
* @return a list of resources associated with the given scope(s)
*/
- List<Resource> findByScope(String... id);
+ List<Resource> findByScope(List<String> id, String resourceServerId);
/**
* Find a {@link Resource} by its name.
@@ -103,5 +111,5 @@ public interface ResourceStore {
* @param type the type of the resource
* @return a list of resources with the given type
*/
- List<Resource> findByType(String type);
+ List<Resource> findByType(String type, String resourceServerId);
}
diff --git a/server-spi-private/src/main/java/org/keycloak/authorization/store/ScopeStore.java b/server-spi-private/src/main/java/org/keycloak/authorization/store/ScopeStore.java
index 81a7064..fa9e70d 100644
--- a/server-spi-private/src/main/java/org/keycloak/authorization/store/ScopeStore.java
+++ b/server-spi-private/src/main/java/org/keycloak/authorization/store/ScopeStore.java
@@ -53,17 +53,17 @@ public interface ScopeStore {
* Returns a {@link Scope} with the given <code>id</code>
*
* @param id the identifier of the scope
- *
+ * @param resourceServerId the resource server id
* @return a scope with the given identifier.
*/
- Scope findById(String id);
+ Scope findById(String id, String resourceServerId);
/**
* Returns a {@link Scope} with the given <code>name</code>
*
* @param name the name of the scope
*
- * @param resourceServerId
+ * @param resourceServerId the resource server id
* @return a scope with the given name.
*/
Scope findByName(String name, String resourceServerId);
diff --git a/server-spi-private/src/main/java/org/keycloak/authorization/store/syncronization/RealmSynchronizer.java b/server-spi-private/src/main/java/org/keycloak/authorization/store/syncronization/RealmSynchronizer.java
index 4f0ef32..ed24c53 100644
--- a/server-spi-private/src/main/java/org/keycloak/authorization/store/syncronization/RealmSynchronizer.java
+++ b/server-spi-private/src/main/java/org/keycloak/authorization/store/syncronization/RealmSynchronizer.java
@@ -36,7 +36,7 @@ public class RealmSynchronizer implements Synchronizer<RealmRemovedEvent> {
StoreFactory storeFactory = authorizationProvider.getStoreFactory();
event.getRealm().getClients().forEach(clientModel -> {
- ResourceServer resourceServer = storeFactory.getResourceServerStore().findByClient(clientModel.getClientId());
+ ResourceServer resourceServer = storeFactory.getResourceServerStore().findByClient(clientModel.getId());
if (resourceServer != null) {
String id = resourceServer.getId();
diff --git a/server-spi-private/src/main/java/org/keycloak/authorization/store/syncronization/UserSynchronizer.java b/server-spi-private/src/main/java/org/keycloak/authorization/store/syncronization/UserSynchronizer.java
index 01830ff..03a2cda 100644
--- a/server-spi-private/src/main/java/org/keycloak/authorization/store/syncronization/UserSynchronizer.java
+++ b/server-spi-private/src/main/java/org/keycloak/authorization/store/syncronization/UserSynchronizer.java
@@ -17,11 +17,16 @@
package org.keycloak.authorization.store.syncronization;
+import java.util.function.Consumer;
+
import org.keycloak.authorization.AuthorizationProvider;
+import org.keycloak.authorization.model.ResourceServer;
import org.keycloak.authorization.store.PolicyStore;
+import org.keycloak.authorization.store.ResourceServerStore;
import org.keycloak.authorization.store.ResourceStore;
import org.keycloak.authorization.store.StoreFactory;
import org.keycloak.models.KeycloakSessionFactory;
+import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserModel.UserRemovedEvent;
import org.keycloak.provider.ProviderFactory;
@@ -39,17 +44,25 @@ public class UserSynchronizer implements Synchronizer<UserRemovedEvent> {
UserModel userModel = event.getUser();
ResourceStore resourceStore = storeFactory.getResourceStore();
PolicyStore policyStore = storeFactory.getPolicyStore();
+ ResourceServerStore resourceServerStore = storeFactory.getResourceServerStore();
+ RealmModel realm = event.getRealm();
+
+ realm.getClients().forEach(clientModel -> {
+ ResourceServer resourceServer = resourceServerStore.findByClient(clientModel.getId());
- resourceStore.findByOwner(userModel.getId()).forEach(resource -> {
- String resourceId = resource.getId();
- policyStore.findByResource(resourceId).forEach(policy -> {
- if (policy.getResources().size() == 1) {
- policyStore.delete(policy.getId());
- } else {
- policy.removeResource(resource);
- }
- });
- resourceStore.delete(resourceId);
+ if (resourceServer != null) {
+ resourceStore.findByOwner(userModel.getId(), resourceServer.getId()).forEach(resource -> {
+ String resourceId = resource.getId();
+ policyStore.findByResource(resourceId, resourceServer.getId()).forEach(policy -> {
+ if (policy.getResources().size() == 1) {
+ policyStore.delete(policy.getId());
+ } else {
+ policy.removeResource(resource);
+ }
+ });
+ resourceStore.delete(resourceId);
+ });
+ }
});
}
}
diff --git a/server-spi-private/src/main/java/org/keycloak/keys/HmacKeyProvider.java b/server-spi-private/src/main/java/org/keycloak/keys/HmacKeyProvider.java
new file mode 100644
index 0000000..2428774
--- /dev/null
+++ b/server-spi-private/src/main/java/org/keycloak/keys/HmacKeyProvider.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.keys;
+
+import org.keycloak.jose.jws.AlgorithmType;
+
+import javax.crypto.SecretKey;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.security.cert.X509Certificate;
+import java.util.List;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public interface HmacKeyProvider extends KeyProvider<HmacKeyMetadata> {
+
+ default AlgorithmType getType() {
+ return AlgorithmType.HMAC;
+ }
+
+ /**
+ * Return the active secret key, or <code>null</code> if no active key is available.
+ *
+ * @return
+ */
+ SecretKey getSecretKey();
+
+ /**
+ * Return the secret key for the specified kid, or <code>null</code> if the kid is unknown.
+ *
+ * @param kid
+ * @return
+ */
+ SecretKey getSecretKey(String kid);
+
+}
diff --git a/server-spi-private/src/main/java/org/keycloak/keys/HmacKeyProviderFactory.java b/server-spi-private/src/main/java/org/keycloak/keys/HmacKeyProviderFactory.java
new file mode 100644
index 0000000..ba7007b
--- /dev/null
+++ b/server-spi-private/src/main/java/org/keycloak/keys/HmacKeyProviderFactory.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.keys;
+
+import org.keycloak.jose.jws.AlgorithmType;
+
+import java.util.Collections;
+import java.util.Map;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public interface HmacKeyProviderFactory extends KeyProviderFactory {
+
+ @Override
+ default Map<String, Object> getTypeMetadata() {
+ return Collections.singletonMap("algorithmType", AlgorithmType.HMAC);
+ }
+
+}
diff --git a/server-spi-private/src/main/java/org/keycloak/keys/KeyProvider.java b/server-spi-private/src/main/java/org/keycloak/keys/KeyProvider.java
index 5eebe81..fb9e152 100644
--- a/server-spi-private/src/main/java/org/keycloak/keys/KeyProvider.java
+++ b/server-spi-private/src/main/java/org/keycloak/keys/KeyProvider.java
@@ -17,6 +17,7 @@
package org.keycloak.keys;
+import org.keycloak.jose.jws.AlgorithmType;
import org.keycloak.provider.Provider;
import java.security.PrivateKey;
@@ -27,42 +28,26 @@ import java.util.List;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
-public interface KeyProvider extends Provider {
+public interface KeyProvider<T extends KeyMetadata> extends Provider {
/**
- * Return the KID for the active keypair, or <code>null</code> if no active key is available.
- *
- * @return
- */
- String getKid();
-
- /**
- * Return the private key for the active keypair, or <code>null</code> if no active key is available.
+ * Returns the algorithm type the keys can be used for
*
* @return
*/
- PrivateKey getPrivateKey();
+ AlgorithmType getType();
/**
- * Return the public key for the specified kid, or <code>null</code> if the kid is unknown.
- *
- * @param kid
- * @return
- */
- PublicKey getPublicKey(String kid);
-
- /**
- * Return the certificate for the specified kid, or <code>null</code> if the kid is unknown.
+ * Return the KID for the active keypair, or <code>null</code> if no active key is available.
*
- * @param kid
* @return
*/
- X509Certificate getCertificate(String kid);
+ String getKid();
/**
* Return metadata about all keypairs held by the provider
* @return
*/
- List<KeyMetadata> getKeyMetadata();
+ List<T> getKeyMetadata();
}
diff --git a/server-spi-private/src/main/java/org/keycloak/keys/RsaKeyProvider.java b/server-spi-private/src/main/java/org/keycloak/keys/RsaKeyProvider.java
new file mode 100644
index 0000000..6e61c28
--- /dev/null
+++ b/server-spi-private/src/main/java/org/keycloak/keys/RsaKeyProvider.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.keys;
+
+import org.keycloak.jose.jws.AlgorithmType;
+import org.keycloak.provider.Provider;
+
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.security.cert.X509Certificate;
+import java.util.List;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public interface RsaKeyProvider extends KeyProvider<RsaKeyMetadata> {
+
+ default AlgorithmType getType() {
+ return AlgorithmType.RSA;
+ }
+
+ /**
+ * Return the private key for the active keypair, or <code>null</code> if no active key is available.
+ *
+ * @return
+ */
+ PrivateKey getPrivateKey();
+
+ /**
+ * Return the public key for the specified kid, or <code>null</code> if the kid is unknown.
+ *
+ * @param kid
+ * @return
+ */
+ PublicKey getPublicKey(String kid);
+
+ /**
+ * Return the certificate for the specified kid, or <code>null</code> if the kid is unknown.
+ *
+ * @param kid
+ * @return
+ */
+ X509Certificate getCertificate(String kid);
+
+}
diff --git a/server-spi-private/src/main/java/org/keycloak/keys/RsaKeyProviderFactory.java b/server-spi-private/src/main/java/org/keycloak/keys/RsaKeyProviderFactory.java
new file mode 100644
index 0000000..440103c
--- /dev/null
+++ b/server-spi-private/src/main/java/org/keycloak/keys/RsaKeyProviderFactory.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.keys;
+
+import org.keycloak.jose.jws.AlgorithmType;
+
+import java.util.Collections;
+import java.util.Map;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public interface RsaKeyProviderFactory extends KeyProviderFactory {
+
+ @Override
+ default Map<String, Object> getTypeMetadata() {
+ return Collections.singletonMap("algorithmType", AlgorithmType.RSA);
+ }
+
+}
diff --git a/server-spi-private/src/main/java/org/keycloak/migration/MigrationModelManager.java b/server-spi-private/src/main/java/org/keycloak/migration/MigrationModelManager.java
index 205d17c..1754c18 100755
--- a/server-spi-private/src/main/java/org/keycloak/migration/MigrationModelManager.java
+++ b/server-spi-private/src/main/java/org/keycloak/migration/MigrationModelManager.java
@@ -31,6 +31,7 @@ import org.keycloak.migration.migrators.MigrateTo2_0_0;
import org.keycloak.migration.migrators.MigrateTo2_1_0;
import org.keycloak.migration.migrators.MigrateTo2_2_0;
import org.keycloak.migration.migrators.MigrateTo2_3_0;
+import org.keycloak.migration.migrators.MigrateTo2_5_0;
import org.keycloak.migration.migrators.Migration;
import org.keycloak.models.KeycloakSession;
@@ -55,6 +56,7 @@ public class MigrationModelManager {
new MigrateTo2_1_0(),
new MigrateTo2_2_0(),
new MigrateTo2_3_0(),
+ new MigrateTo2_5_0(),
};
public static void migrate(KeycloakSession session) {
diff --git a/server-spi-private/src/main/java/org/keycloak/migration/migrators/MigrateTo2_1_0.java b/server-spi-private/src/main/java/org/keycloak/migration/migrators/MigrateTo2_1_0.java
index 995dafb..f8844f1 100644
--- a/server-spi-private/src/main/java/org/keycloak/migration/migrators/MigrateTo2_1_0.java
+++ b/server-spi-private/src/main/java/org/keycloak/migration/migrators/MigrateTo2_1_0.java
@@ -70,7 +70,7 @@ public class MigrateTo2_1_0 implements Migration {
ResourceServer resourceServer = storeFactory.getResourceServerStore().findByClient(clientModel.getId());
if (resourceServer != null) {
- policyStore.findByType("role").forEach(policy -> {
+ policyStore.findByType("role", resourceServer.getId()).forEach(policy -> {
Map<String, String> config = policy.getConfig();
String roles = config.get("roles");
List roleConfig;
diff --git a/server-spi-private/src/main/java/org/keycloak/migration/migrators/MigrateTo2_5_0.java b/server-spi-private/src/main/java/org/keycloak/migration/migrators/MigrateTo2_5_0.java
new file mode 100644
index 0000000..ee9a041
--- /dev/null
+++ b/server-spi-private/src/main/java/org/keycloak/migration/migrators/MigrateTo2_5_0.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.migration.migrators;
+
+
+import org.keycloak.migration.ModelVersion;
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.ClientTemplateModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.utils.DefaultKeyProviders;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class MigrateTo2_5_0 implements Migration {
+
+ public static final ModelVersion VERSION = new ModelVersion("2.5.0");
+
+ @Override
+ public void migrate(KeycloakSession session) {
+ session.realms().getRealms().stream().forEach(
+ r -> DefaultKeyProviders.createSecretProvider(r)
+ );
+ }
+
+ @Override
+ public ModelVersion getVersion() {
+ return VERSION;
+ }
+}
diff --git a/server-spi-private/src/main/java/org/keycloak/models/LDAPConstants.java b/server-spi-private/src/main/java/org/keycloak/models/LDAPConstants.java
index 08be1c1..7bef0cb 100644
--- a/server-spi-private/src/main/java/org/keycloak/models/LDAPConstants.java
+++ b/server-spi-private/src/main/java/org/keycloak/models/LDAPConstants.java
@@ -88,6 +88,7 @@ public class LDAPConstants {
public static final String OBJECT_CLASS = "objectclass";
public static final String UID = "uid";
public static final String USER_PASSWORD_ATTRIBUTE = "userpassword";
+ public static final String JPEG_PHOTO = "jpegPhoto";
public static final String GROUP = "group";
public static final String GROUP_OF_NAMES = "groupOfNames";
public static final String GROUP_OF_ENTRIES = "groupOfEntries";
diff --git a/server-spi-private/src/main/java/org/keycloak/models/utils/DefaultKeyProviders.java b/server-spi-private/src/main/java/org/keycloak/models/utils/DefaultKeyProviders.java
index 30ff7d7..e039292 100644
--- a/server-spi-private/src/main/java/org/keycloak/models/utils/DefaultKeyProviders.java
+++ b/server-spi-private/src/main/java/org/keycloak/models/utils/DefaultKeyProviders.java
@@ -39,6 +39,22 @@ public class DefaultKeyProviders {
generated.setConfig(config);
realm.addComponentModel(generated);
+
+ createSecretProvider(realm);
+ }
+
+ public static void createSecretProvider(RealmModel realm) {
+ ComponentModel generated = new ComponentModel();
+ generated.setName("hmac-generated");
+ generated.setParentId(realm.getId());
+ generated.setProviderId("hmac-generated");
+ generated.setProviderType(KeyProvider.class.getName());
+
+ MultivaluedHashMap<String, String> config = new MultivaluedHashMap<>();
+ config.putSingle("priority", "100");
+ generated.setConfig(config);
+
+ realm.addComponentModel(generated);
}
public static void createProviders(RealmModel realm, String privateKeyPem, String certificatePem) {
@@ -57,6 +73,8 @@ public class DefaultKeyProviders {
rsa.setConfig(config);
realm.addComponentModel(rsa);
+
+ createSecretProvider(realm);
}
}
diff --git a/server-spi-private/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java b/server-spi-private/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java
index cba151e..a258cd7 100755
--- a/server-spi-private/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java
+++ b/server-spi-private/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java
@@ -188,14 +188,14 @@ public final class KeycloakModelUtils {
}
/**
- * Try to find user by username or email
+ * Try to find user by username or email for authentication
*
* @param realm realm
* @param username username or email of user
* @return found user
*/
public static UserModel findUserByNameOrEmail(KeycloakSession session, RealmModel realm, String username) {
- if (username.indexOf('@') != -1) {
+ if (realm.isLoginWithEmailAllowed() && username.indexOf('@') != -1) {
UserModel user = session.users().getUserByEmail(username, realm);
if (user != null) {
return user;
diff --git a/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java b/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java
index 27dd6dc..0ca0400 100755
--- a/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java
+++ b/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java
@@ -17,14 +17,23 @@
package org.keycloak.models.utils;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
import org.keycloak.authorization.AuthorizationProvider;
import org.keycloak.authorization.model.Policy;
import org.keycloak.authorization.model.Resource;
import org.keycloak.authorization.model.ResourceServer;
import org.keycloak.authorization.model.Scope;
-import org.keycloak.authorization.store.PolicyStore;
import org.keycloak.authorization.store.ResourceStore;
-import org.keycloak.authorization.store.StoreFactory;
import org.keycloak.common.util.MultivaluedHashMap;
import org.keycloak.common.util.Time;
import org.keycloak.component.ComponentModel;
@@ -84,20 +93,6 @@ import org.keycloak.representations.idm.authorization.ResourceRepresentation;
import org.keycloak.representations.idm.authorization.ResourceServerRepresentation;
import org.keycloak.representations.idm.authorization.ScopeRepresentation;
import org.keycloak.storage.StorageId;
-import org.keycloak.util.JsonSerialization;
-
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.stream.Collectors;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@@ -292,6 +287,8 @@ public class ModelToRepresentation {
rep.setAdminEventsDetailsEnabled(realm.isAdminEventsDetailsEnabled());
rep.setVerifyEmail(realm.isVerifyEmail());
+ rep.setLoginWithEmailAllowed(realm.isLoginWithEmailAllowed());
+ rep.setDuplicateEmailsAllowed(realm.isDuplicateEmailsAllowed());
rep.setResetPasswordAllowed(realm.isResetPasswordAllowed());
rep.setEditUsernameAllowed(realm.isEditUsernameAllowed());
rep.setRevokeRefreshToken(realm.isRevokeRefreshToken());
@@ -770,34 +767,16 @@ public class ModelToRepresentation {
}
public static ScopeRepresentation toRepresentation(Scope model, AuthorizationProvider authorizationProvider) {
+ return toRepresentation(model, authorizationProvider, true);
+ }
+
+ public static ScopeRepresentation toRepresentation(Scope model, AuthorizationProvider authorizationProvider, boolean deep) {
ScopeRepresentation scope = new ScopeRepresentation();
scope.setId(model.getId());
scope.setName(model.getName());
scope.setIconUri(model.getIconUri());
- StoreFactory storeFactory = authorizationProvider.getStoreFactory();
-
- scope.setResources(new ArrayList<>());
-
- storeFactory.getResourceStore().findByScope(model.getId()).forEach(resource -> scope.getResources().add(toRepresentation(resource, resource.getResourceServer(), authorizationProvider)));
-
- PolicyStore policyStore = storeFactory.getPolicyStore();
-
- scope.setPolicies(new ArrayList<>());
-
- policyStore.findByScopeIds(Arrays.asList(model.getId()), model.getResourceServer().getId()).forEach(policyModel -> {
- PolicyRepresentation policy = new PolicyRepresentation();
-
- policy.setId(policyModel.getId());
- policy.setName(policyModel.getName());
- policy.setType(policyModel.getType());
-
- if (!scope.getPolicies().contains(policy)) {
- scope.getPolicies().add(policy);
- }
- });
-
return scope;
}
@@ -813,7 +792,7 @@ public class ModelToRepresentation {
return server;
}
- public static PolicyRepresentation toRepresentation(Policy model, AuthorizationProvider authorization) {
+ public static PolicyRepresentation toRepresentation(Policy model) {
PolicyRepresentation representation = new PolicyRepresentation();
representation.setId(model.getId());
@@ -822,45 +801,16 @@ public class ModelToRepresentation {
representation.setType(model.getType());
representation.setDecisionStrategy(model.getDecisionStrategy());
representation.setLogic(model.getLogic());
- representation.setConfig(new HashMap<>(model.getConfig()));
-
- List<Policy> policies = authorization.getStoreFactory().getPolicyStore().findDependentPolicies(model.getId());
-
- representation.setDependentPolicies(policies.stream().map(policy -> {
- PolicyRepresentation representation1 = new PolicyRepresentation();
-
- representation1.setId(policy.getId());
- representation1.setName(policy.getName());
-
- return representation1;
- }).collect(Collectors.toList()));
-
- List<PolicyRepresentation> associatedPolicies = new ArrayList<>();
-
- List<String> obj = model.getAssociatedPolicies().stream().map(policy -> {
- PolicyRepresentation representation1 = new PolicyRepresentation();
-
- representation1.setId(policy.getId());
- representation1.setName(policy.getName());
- representation1.setType(policy.getType());
-
- associatedPolicies.add(representation1);
-
- return policy.getId();
- }).collect(Collectors.toList());
-
- representation.setAssociatedPolicies(associatedPolicies);
-
- try {
- representation.getConfig().put("applyPolicies", JsonSerialization.writeValueAsString(obj));
- } catch (IOException e) {
- e.printStackTrace();
- }
+ representation.setConfig(model.getConfig());
return representation;
}
public static ResourceRepresentation toRepresentation(Resource model, ResourceServer resourceServer, AuthorizationProvider authorization) {
+ return toRepresentation(model, resourceServer, authorization, true);
+ }
+
+ public static ResourceRepresentation toRepresentation(Resource model, ResourceServer resourceServer, AuthorizationProvider authorization, Boolean deep) {
ResourceRepresentation resource = new ResourceRepresentation();
resource.setId(model.getId());
@@ -891,55 +841,36 @@ public class ModelToRepresentation {
resource.setOwner(owner);
- resource.setScopes(model.getScopes().stream().map(model1 -> {
- ScopeRepresentation scope = new ScopeRepresentation();
- scope.setId(model1.getId());
- scope.setName(model1.getName());
- String iconUri = model1.getIconUri();
- if (iconUri != null) {
- scope.setIconUri(iconUri);
- }
- return scope;
- }).collect(Collectors.toSet()));
-
- resource.setTypedScopes(new ArrayList<>());
-
- if (resource.getType() != null) {
- ResourceStore resourceStore = authorization.getStoreFactory().getResourceStore();
- for (Resource typed : resourceStore.findByType(resource.getType())) {
- if (typed.getOwner().equals(resourceServer.getClientId()) && !typed.getId().equals(resource.getId())) {
- resource.setTypedScopes(typed.getScopes().stream().map(model1 -> {
- ScopeRepresentation scope = new ScopeRepresentation();
- scope.setId(model1.getId());
- scope.setName(model1.getName());
- String iconUri = model1.getIconUri();
- if (iconUri != null) {
- scope.setIconUri(iconUri);
- }
- return scope;
- }).filter(scopeRepresentation -> !resource.getScopes().contains(scopeRepresentation)).collect(Collectors.toList()));
+ if (deep) {
+ resource.setScopes(model.getScopes().stream().map(model1 -> {
+ ScopeRepresentation scope = new ScopeRepresentation();
+ scope.setId(model1.getId());
+ scope.setName(model1.getName());
+ String iconUri = model1.getIconUri();
+ if (iconUri != null) {
+ scope.setIconUri(iconUri);
+ }
+ return scope;
+ }).collect(Collectors.toSet()));
+
+ resource.setTypedScopes(new ArrayList<>());
+
+ if (resource.getType() != null) {
+ ResourceStore resourceStore = authorization.getStoreFactory().getResourceStore();
+ for (Resource typed : resourceStore.findByType(resource.getType(), resourceServer.getId())) {
+ if (typed.getOwner().equals(resourceServer.getClientId()) && !typed.getId().equals(resource.getId())) {
+ resource.setTypedScopes(typed.getScopes().stream().map(model1 -> {
+ ScopeRepresentation scope = new ScopeRepresentation();
+ scope.setId(model1.getId());
+ scope.setName(model1.getName());
+ String iconUri = model1.getIconUri();
+ if (iconUri != null) {
+ scope.setIconUri(iconUri);
+ }
+ return scope;
+ }).filter(scopeRepresentation -> !resource.getScopes().contains(scopeRepresentation)).collect(Collectors.toList()));
+ }
}
- }
- }
-
- resource.setPolicies(new ArrayList<>());
-
- Set<Policy> policies = new HashSet<>();
- PolicyStore policyStore = authorization.getStoreFactory().getPolicyStore();
-
- policies.addAll(policyStore.findByResource(resource.getId()));
- policies.addAll(policyStore.findByResourceType(resource.getType(), resourceServer.getId()));
- policies.addAll(policyStore.findByScopeIds(resource.getScopes().stream().map(scope -> scope.getId()).collect(Collectors.toList()), resourceServer.getId()));
-
- for (Policy policyModel : policies) {
- PolicyRepresentation policy = new PolicyRepresentation();
-
- policy.setId(policyModel.getId());
- policy.setName(policyModel.getName());
- policy.setType(policyModel.getType());
-
- if (!resource.getPolicies().contains(policy)) {
- resource.getPolicies().add(policy);
}
}
diff --git a/server-spi-private/src/main/java/org/keycloak/models/utils/RepresentationToModel.java b/server-spi-private/src/main/java/org/keycloak/models/utils/RepresentationToModel.java
index d8b934f..0fa958c 100755
--- a/server-spi-private/src/main/java/org/keycloak/models/utils/RepresentationToModel.java
+++ b/server-spi-private/src/main/java/org/keycloak/models/utils/RepresentationToModel.java
@@ -184,6 +184,8 @@ public class RepresentationToModel {
newRealm.setRegistrationEmailAsUsername(rep.isRegistrationEmailAsUsername());
if (rep.isRememberMe() != null) newRealm.setRememberMe(rep.isRememberMe());
if (rep.isVerifyEmail() != null) newRealm.setVerifyEmail(rep.isVerifyEmail());
+ if (rep.isLoginWithEmailAllowed() != null) newRealm.setLoginWithEmailAllowed(rep.isLoginWithEmailAllowed());
+ if (rep.isDuplicateEmailsAllowed() != null) newRealm.setDuplicateEmailsAllowed(rep.isDuplicateEmailsAllowed());
if (rep.isResetPasswordAllowed() != null) newRealm.setResetPasswordAllowed(rep.isResetPasswordAllowed());
if (rep.isEditUsernameAllowed() != null) newRealm.setEditUsernameAllowed(rep.isEditUsernameAllowed());
if (rep.getLoginTheme() != null) newRealm.setLoginTheme(rep.getLoginTheme());
@@ -785,6 +787,8 @@ public class RepresentationToModel {
if (rep.isRegistrationEmailAsUsername() != null) realm.setRegistrationEmailAsUsername(rep.isRegistrationEmailAsUsername());
if (rep.isRememberMe() != null) realm.setRememberMe(rep.isRememberMe());
if (rep.isVerifyEmail() != null) realm.setVerifyEmail(rep.isVerifyEmail());
+ if (rep.isLoginWithEmailAllowed() != null) realm.setLoginWithEmailAllowed(rep.isLoginWithEmailAllowed());
+ if (rep.isDuplicateEmailsAllowed() != null) realm.setDuplicateEmailsAllowed(rep.isDuplicateEmailsAllowed());
if (rep.isResetPasswordAllowed() != null) realm.setResetPasswordAllowed(rep.isResetPasswordAllowed());
if (rep.isEditUsernameAllowed() != null) realm.setEditUsernameAllowed(rep.isEditUsernameAllowed());
if (rep.getSslRequired() != null) realm.setSslRequired(SslRequired.valueOf(rep.getSslRequired().toUpperCase()));
@@ -1894,7 +1898,7 @@ public class RepresentationToModel {
if (roles != null && !roles.isEmpty()) {
try {
- List<Map> rolesMap = JsonSerialization.readValue(roles, List.class);
+ List<Map> rolesMap = (List<Map>)JsonSerialization.readValue(roles, List.class);
config.put("roles", JsonSerialization.writeValueAsString(rolesMap.stream().map(roleConfig -> {
String roleName = roleConfig.get("id").toString();
String clientId = null;
@@ -1946,7 +1950,7 @@ public class RepresentationToModel {
if (users != null && !users.isEmpty()) {
try {
- List<String> usersMap = JsonSerialization.readValue(users, List.class);
+ List<String> usersMap = (List<String>) JsonSerialization.readValue(users, List.class);
config.put("users", JsonSerialization.writeValueAsString(usersMap.stream().map(userId -> {
UserModel user = session.users().getUserByUsername(userId, realm);
@@ -1970,12 +1974,12 @@ public class RepresentationToModel {
if (scopes != null && !scopes.isEmpty()) {
try {
ScopeStore scopeStore = storeFactory.getScopeStore();
- List<String> scopesMap = JsonSerialization.readValue(scopes, List.class);
+ List<String> scopesMap = (List<String>) JsonSerialization.readValue(scopes, List.class);
config.put("scopes", JsonSerialization.writeValueAsString(scopesMap.stream().map(scopeName -> {
Scope newScope = scopeStore.findByName(scopeName, resourceServer.getId());
if (newScope == null) {
- newScope = scopeStore.findById(scopeName);
+ newScope = scopeStore.findById(scopeName, resourceServer.getId());
}
if (newScope == null) {
@@ -1995,21 +1999,18 @@ public class RepresentationToModel {
ResourceStore resourceStore = storeFactory.getResourceStore();
try {
List<String> resources = JsonSerialization.readValue(policyResources, List.class);
- config.put("resources", JsonSerialization.writeValueAsString(resources.stream().map(new Function<String, String>() {
- @Override
- public String apply(String resourceName) {
- Resource resource = resourceStore.findByName(resourceName, resourceServer.getId());
+ config.put("resources", JsonSerialization.writeValueAsString(resources.stream().map(resourceName -> {
+ Resource resource = resourceStore.findByName(resourceName, resourceServer.getId());
- if (resource == null) {
- resource = resourceStore.findById(resourceName);
- }
-
- if (resource == null) {
- throw new RuntimeException("Resource with name [" + resourceName + "] not defined.");
- }
+ if (resource == null) {
+ resource = resourceStore.findById(resourceName, resourceServer.getId());
+ }
- return resource.getId();
+ if (resource == null) {
+ throw new RuntimeException("Resource with name [" + resourceName + "] not defined.");
}
+
+ return resource.getId();
}).collect(Collectors.toList())));
} catch (Exception e) {
throw new RuntimeException("Error while exporting policy [" + policyRepresentation.getName() + "].", e);
@@ -2021,12 +2022,12 @@ public class RepresentationToModel {
if (applyPolicies != null && !applyPolicies.isEmpty()) {
PolicyStore policyStore = storeFactory.getPolicyStore();
try {
- List<String> policies = JsonSerialization.readValue(applyPolicies, List.class);
+ List<String> policies = (List<String>) JsonSerialization.readValue(applyPolicies, List.class);
config.put("applyPolicies", JsonSerialization.writeValueAsString(policies.stream().map(policyName -> {
Policy policy = policyStore.findByName(policyName, resourceServer.getId());
if (policy == null) {
- policy = policyStore.findById(policyName);
+ policy = policyStore.findById(policyName, resourceServer.getId());
}
if (policy == null) {
@@ -2058,7 +2059,7 @@ public class RepresentationToModel {
Policy existing;
if (policy.getId() != null) {
- existing = policyStore.findById(policy.getId());
+ existing = policyStore.findById(policy.getId(), resourceServer.getId());
} else {
existing = policyStore.findByName(policy.getName(), resourceServer.getId());
}
@@ -2115,7 +2116,14 @@ public class RepresentationToModel {
}
}
if (!hasScope) {
- policy.addScope(storeFactory.getScopeStore().findById(scopeId));
+ ResourceServer resourceServer = policy.getResourceServer();
+ Scope scope = storeFactory.getScopeStore().findById(scopeId, resourceServer.getId());
+
+ if (scope == null) {
+ storeFactory.getScopeStore().findByName(scopeId, resourceServer.getId());
+ }
+
+ policy.addScope(scope);
}
}
@@ -2131,6 +2139,8 @@ public class RepresentationToModel {
policy.removeScope(scopeModel);
}
}
+
+ policy.getConfig().remove("scopes");
}
}
@@ -2160,7 +2170,7 @@ public class RepresentationToModel {
if (!hasPolicy) {
- Policy associatedPolicy = policyStore.findById(policyId);
+ Policy associatedPolicy = policyStore.findById(policyId, resourceServer.getId());
if (associatedPolicy == null) {
associatedPolicy = policyStore.findByName(policyId, resourceServer.getId());
@@ -2182,6 +2192,8 @@ public class RepresentationToModel {
policy.removeAssociatedPolicy(policyModel);;
}
}
+
+ policy.getConfig().remove("applyPolicies");
}
}
@@ -2206,7 +2218,7 @@ public class RepresentationToModel {
}
}
if (!hasResource && !"".equals(resourceId)) {
- policy.addResource(storeFactory.getResourceStore().findById(resourceId));
+ policy.addResource(storeFactory.getResourceStore().findById(resourceId, policy.getResourceServer().getId()));
}
}
@@ -2223,6 +2235,8 @@ public class RepresentationToModel {
policy.removeResource(resourceModel);
}
}
+
+ policy.getConfig().remove("resources");
}
}
@@ -2231,7 +2245,7 @@ public class RepresentationToModel {
Resource existing;
if (resource.getId() != null) {
- existing = resourceStore.findById(resource.getId());
+ existing = resourceStore.findById(resource.getId(), resourceServer.getId());
} else {
existing = resourceStore.findByName(resource.getName(), resourceServer.getId());
}
@@ -2282,7 +2296,7 @@ public class RepresentationToModel {
Scope existing;
if (scope.getId() != null) {
- existing = scopeStore.findById(scope.getId());
+ existing = scopeStore.findById(scope.getId(), resourceServer.getId());
} else {
existing = scopeStore.findByName(scope.getName(), resourceServer.getId());
}
diff --git a/server-spi-private/src/main/java/org/keycloak/provider/ConfigurationValidationHelper.java b/server-spi-private/src/main/java/org/keycloak/provider/ConfigurationValidationHelper.java
index 047c1bd..94b20ba 100644
--- a/server-spi-private/src/main/java/org/keycloak/provider/ConfigurationValidationHelper.java
+++ b/server-spi-private/src/main/java/org/keycloak/provider/ConfigurationValidationHelper.java
@@ -41,6 +41,28 @@ public class ConfigurationValidationHelper {
return checkInt(property.getName(), property.getLabel(), required);
}
+ public ConfigurationValidationHelper checkList(ProviderConfigProperty property, boolean required) throws ComponentValidationException {
+ checkSingle(property.getName(), property.getLabel(), required);
+
+ String value = model.getConfig().getFirst(property.getName());
+ if (value != null && !property.getOptions().contains(value)) {
+ StringBuilder options = new StringBuilder();
+ int i = 1;
+ for (String o : property.getOptions()) {
+ if (i == property.getOptions().size()) {
+ options.append(" or ");
+ } else if (i > 1) {
+ options.append(", ");
+ }
+ options.append(o);
+ i++;
+ }
+ throw new ComponentValidationException("''{0}'' should be {1}", property.getLabel(), options.toString());
+ }
+
+ return this;
+ }
+
public ConfigurationValidationHelper checkInt(String key, String label, boolean required) throws ComponentValidationException {
checkSingle(key, label, required);
diff --git a/server-spi-private/src/main/java/org/keycloak/services/managers/ClientSessionCode.java b/server-spi-private/src/main/java/org/keycloak/services/managers/ClientSessionCode.java
index 1369e27..e343823 100755
--- a/server-spi-private/src/main/java/org/keycloak/services/managers/ClientSessionCode.java
+++ b/server-spi-private/src/main/java/org/keycloak/services/managers/ClientSessionCode.java
@@ -126,9 +126,13 @@ public class ClientSessionCode {
}
public static ClientSessionModel getClientSession(String code, KeycloakSession session, RealmModel realm) {
- String[] parts = code.split("\\.");
- String id = parts[1];
- return session.sessions().getClientSession(realm, id);
+ try {
+ String[] parts = code.split("\\.");
+ String id = parts[1];
+ return session.sessions().getClientSession(realm, id);
+ } catch (ArrayIndexOutOfBoundsException e) {
+ return null;
+ }
}
public ClientSessionModel getClientSession() {
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpCreateUserIfUniqueAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpCreateUserIfUniqueAuthenticator.java
index 9c35844..317cb64 100644
--- a/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpCreateUserIfUniqueAuthenticator.java
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpCreateUserIfUniqueAuthenticator.java
@@ -119,7 +119,7 @@ public class IdpCreateUserIfUniqueAuthenticator extends AbstractIdpAuthenticator
// Could be overriden to detect duplication based on other criterias (firstName, lastName, ...)
protected ExistingUserInfo checkExistingUser(AuthenticationFlowContext context, String username, SerializedBrokeredIdentityContext serializedCtx, BrokeredIdentityContext brokerContext) {
- if (brokerContext.getEmail() != null) {
+ if (brokerContext.getEmail() != null && !context.getRealm().isDuplicateEmailsAllowed()) {
UserModel existingUser = context.getSession().users().getUserByEmail(brokerContext.getEmail(), context.getRealm());
if (existingUser != null) {
return new ExistingUserInfo(existingUser.getId(), UserModel.EMAIL, existingUser.getEmail());
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/browser/AbstractUsernameFormAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/browser/AbstractUsernameFormAuthenticator.java
index f88b674..f837d3c 100755
--- a/services/src/main/java/org/keycloak/authentication/authenticators/browser/AbstractUsernameFormAuthenticator.java
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/browser/AbstractUsernameFormAuthenticator.java
@@ -172,8 +172,9 @@ public abstract class AbstractUsernameFormAuthenticator extends AbstractFormAuth
List<CredentialInput> credentials = new LinkedList<>();
String password = inputData.getFirst(CredentialRepresentation.PASSWORD);
credentials.add(UserCredentialModel.password(password));
- boolean valid = context.getSession().userCredentialManager().isValid(context.getRealm(), user, credentials);
- if (!valid) {
+ if (password != null && !password.isEmpty() && context.getSession().userCredentialManager().isValid(context.getRealm(), user, credentials)) {
+ return true;
+ } else {
context.getEvent().user(user);
context.getEvent().error(Errors.INVALID_USER_CREDENTIALS);
Response challengeResponse = invalidCredentials(context);
@@ -181,7 +182,6 @@ public abstract class AbstractUsernameFormAuthenticator extends AbstractFormAuth
context.clearUser();
return false;
}
- return true;
}
}
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/browser/ScriptBasedAuthenticatorFactory.java b/services/src/main/java/org/keycloak/authentication/authenticators/browser/ScriptBasedAuthenticatorFactory.java
index b2e8b98..4a1003c 100644
--- a/services/src/main/java/org/keycloak/authentication/authenticators/browser/ScriptBasedAuthenticatorFactory.java
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/browser/ScriptBasedAuthenticatorFactory.java
@@ -153,6 +153,6 @@ public class ScriptBasedAuthenticatorFactory implements AuthenticatorFactory, En
@Override
public boolean isSupported() {
- return Profile.isPreviewEnabled();
+ return Profile.isFeatureEnabled(Profile.Feature.SCRIPTS);
}
}
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/directgrant/ValidateOTP.java b/services/src/main/java/org/keycloak/authentication/authenticators/directgrant/ValidateOTP.java
index a9ea094..69968cc 100755
--- a/services/src/main/java/org/keycloak/authentication/authenticators/directgrant/ValidateOTP.java
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/directgrant/ValidateOTP.java
@@ -53,9 +53,7 @@ public class ValidateOTP extends AbstractDirectGrantAuthenticator {
}
return;
}
- MultivaluedMap<String, String> inputData = context.getHttpRequest().getDecodedFormParameters();
- List<UserCredentialModel> credentials = new LinkedList<>();
- String otp = inputData.getFirst(CredentialRepresentation.TOTP);
+ String otp = retrieveOTP(context);
if (otp == null) {
if (context.getUser() != null) {
context.getEvent().user(context.getUser());
@@ -142,4 +140,9 @@ public class ValidateOTP extends AbstractDirectGrantAuthenticator {
public String getId() {
return PROVIDER_ID;
}
+
+ protected String retrieveOTP(AuthenticationFlowContext context) {
+ MultivaluedMap<String, String> inputData = context.getHttpRequest().getDecodedFormParameters();
+ return inputData.getFirst(CredentialRepresentation.TOTP);
+ }
}
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/directgrant/ValidatePassword.java b/services/src/main/java/org/keycloak/authentication/authenticators/directgrant/ValidatePassword.java
index 0de3b2a..b992225 100755
--- a/services/src/main/java/org/keycloak/authentication/authenticators/directgrant/ValidatePassword.java
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/directgrant/ValidatePassword.java
@@ -43,9 +43,7 @@ public class ValidatePassword extends AbstractDirectGrantAuthenticator {
@Override
public void authenticate(AuthenticationFlowContext context) {
- MultivaluedMap<String, String> inputData = context.getHttpRequest().getDecodedFormParameters();
- List<UserCredentialModel> credentials = new LinkedList<>();
- String password = inputData.getFirst(CredentialRepresentation.PASSWORD);
+ String password = retrievePassword(context);
boolean valid = context.getSession().userCredentialManager().isValid(context.getRealm(), context.getUser(), UserCredentialModel.password(password));
if (!valid) {
context.getEvent().user(context.getUser());
@@ -118,4 +116,9 @@ public class ValidatePassword extends AbstractDirectGrantAuthenticator {
public String getId() {
return PROVIDER_ID;
}
+
+ protected String retrievePassword(AuthenticationFlowContext context) {
+ MultivaluedMap<String, String> inputData = context.getHttpRequest().getDecodedFormParameters();
+ return inputData.getFirst(CredentialRepresentation.PASSWORD);
+ }
}
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/directgrant/ValidateUsername.java b/services/src/main/java/org/keycloak/authentication/authenticators/directgrant/ValidateUsername.java
index d9e8c4b..da7a67f 100755
--- a/services/src/main/java/org/keycloak/authentication/authenticators/directgrant/ValidateUsername.java
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/directgrant/ValidateUsername.java
@@ -47,8 +47,7 @@ public class ValidateUsername extends AbstractDirectGrantAuthenticator {
@Override
public void authenticate(AuthenticationFlowContext context) {
- MultivaluedMap<String, String> inputData = context.getHttpRequest().getDecodedFormParameters();
- String username = inputData.getFirst(AuthenticationManager.FORM_USERNAME);
+ String username = retrieveUsername(context);
if (username == null) {
context.getEvent().error(Errors.USER_NOT_FOUND);
Response challengeResponse = errorResponse(Response.Status.UNAUTHORIZED.getStatusCode(), "invalid_request", "Missing parameter: username");
@@ -154,4 +153,9 @@ public class ValidateUsername extends AbstractDirectGrantAuthenticator {
public String getId() {
return PROVIDER_ID;
}
+
+ protected String retrieveUsername(AuthenticationFlowContext context) {
+ MultivaluedMap<String, String> inputData = context.getHttpRequest().getDecodedFormParameters();
+ return inputData.getFirst(AuthenticationManager.FORM_USERNAME);
+ }
}
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/resetcred/ResetCredentialChooseUser.java b/services/src/main/java/org/keycloak/authentication/authenticators/resetcred/ResetCredentialChooseUser.java
index d0919cc..19edea6 100755
--- a/services/src/main/java/org/keycloak/authentication/authenticators/resetcred/ResetCredentialChooseUser.java
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/resetcred/ResetCredentialChooseUser.java
@@ -80,10 +80,11 @@ public class ResetCredentialChooseUser implements Authenticator, AuthenticatorFa
context.failureChallenge(AuthenticationFlowError.INVALID_USER, challenge);
return;
}
-
- UserModel user = context.getSession().users().getUserByUsername(username, context.getRealm());
- if (user == null && username.contains("@")) {
- user = context.getSession().users().getUserByEmail(username, context.getRealm());
+
+ RealmModel realm = context.getRealm();
+ UserModel user = context.getSession().users().getUserByUsername(username, realm);
+ if (user == null && realm.isLoginWithEmailAllowed() && username.contains("@")) {
+ user = context.getSession().users().getUserByEmail(username, realm);
}
context.getClientSession().setNote(AbstractUsernameFormAuthenticator.ATTEMPTED_USERNAME, username);
diff --git a/services/src/main/java/org/keycloak/authentication/forms/RegistrationProfile.java b/services/src/main/java/org/keycloak/authentication/forms/RegistrationProfile.java
index 355bbe2..08319a3 100755
--- a/services/src/main/java/org/keycloak/authentication/forms/RegistrationProfile.java
+++ b/services/src/main/java/org/keycloak/authentication/forms/RegistrationProfile.java
@@ -83,7 +83,7 @@ public class RegistrationProfile implements FormAction, FormActionFactory {
emailValid = false;
}
- if (emailValid && context.getSession().users().getUserByEmail(email, context.getRealm()) != null) {
+ if (emailValid && !context.getRealm().isDuplicateEmailsAllowed() && context.getSession().users().getUserByEmail(email, context.getRealm()) != null) {
eventError = Errors.EMAIL_IN_USE;
formData.remove(Validation.FIELD_EMAIL);
context.getEvent().detail(Details.EMAIL, email);
diff --git a/services/src/main/java/org/keycloak/authentication/forms/RegistrationUserCreation.java b/services/src/main/java/org/keycloak/authentication/forms/RegistrationUserCreation.java
index 7b52d32..90dee70 100755
--- a/services/src/main/java/org/keycloak/authentication/forms/RegistrationUserCreation.java
+++ b/services/src/main/java/org/keycloak/authentication/forms/RegistrationUserCreation.java
@@ -86,7 +86,7 @@ public class RegistrationUserCreation implements FormAction, FormActionFactory {
context.validationError(formData, errors);
return;
}
- if (email != null && context.getSession().users().getUserByEmail(email, context.getRealm()) != null) {
+ if (email != null && !context.getRealm().isDuplicateEmailsAllowed() && context.getSession().users().getUserByEmail(email, context.getRealm()) != null) {
context.error(Errors.EMAIL_IN_USE);
formData.remove(Validation.FIELD_EMAIL);
errors.add(new FormMessage(RegistrationPage.FIELD_EMAIL, Messages.EMAIL_EXISTS));
diff --git a/services/src/main/java/org/keycloak/authentication/requiredactions/UpdateProfile.java b/services/src/main/java/org/keycloak/authentication/requiredactions/UpdateProfile.java
index fcc5df7..ca0185e 100644
--- a/services/src/main/java/org/keycloak/authentication/requiredactions/UpdateProfile.java
+++ b/services/src/main/java/org/keycloak/authentication/requiredactions/UpdateProfile.java
@@ -104,16 +104,18 @@ public class UpdateProfile implements RequiredActionProvider, RequiredActionFact
boolean emailChanged = oldEmail != null ? !oldEmail.equals(email) : email != null;
if (emailChanged) {
- UserModel userByEmail = session.users().getUserByEmail(email, realm);
-
- // check for duplicated email
- if (userByEmail != null && !userByEmail.getId().equals(user.getId())) {
- Response challenge = context.form()
- .setError(Messages.EMAIL_EXISTS)
- .setFormData(formData)
- .createResponse(UserModel.RequiredAction.UPDATE_PROFILE);
- context.challenge(challenge);
- return;
+ if (!realm.isDuplicateEmailsAllowed()) {
+ UserModel userByEmail = session.users().getUserByEmail(email, realm);
+
+ // check for duplicated email
+ if (userByEmail != null && !userByEmail.getId().equals(user.getId())) {
+ Response challenge = context.form()
+ .setError(Messages.EMAIL_EXISTS)
+ .setFormData(formData)
+ .createResponse(UserModel.RequiredAction.UPDATE_PROFILE);
+ context.challenge(challenge);
+ return;
+ }
}
user.setEmail(email);
diff --git a/services/src/main/java/org/keycloak/authorization/admin/PolicyEvaluationService.java b/services/src/main/java/org/keycloak/authorization/admin/PolicyEvaluationService.java
index 076f9af..b096d24 100644
--- a/services/src/main/java/org/keycloak/authorization/admin/PolicyEvaluationService.java
+++ b/services/src/main/java/org/keycloak/authorization/admin/PolicyEvaluationService.java
@@ -17,6 +17,27 @@
*/
package org.keycloak.authorization.admin;
+import static java.util.Arrays.asList;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import javax.ws.rs.Consumes;
+import javax.ws.rs.POST;
+import javax.ws.rs.Produces;
+import javax.ws.rs.container.AsyncResponse;
+import javax.ws.rs.container.Suspended;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.Response;
+
import org.jboss.resteasy.spi.HttpRequest;
import org.keycloak.authorization.AuthorizationProvider;
import org.keycloak.authorization.admin.representation.PolicyEvaluationRequest;
@@ -46,29 +67,10 @@ import org.keycloak.models.UserSessionModel;
import org.keycloak.protocol.ProtocolMapper;
import org.keycloak.protocol.oidc.mappers.OIDCAccessTokenMapper;
import org.keycloak.representations.AccessToken;
+import org.keycloak.representations.idm.authorization.ScopeRepresentation;
import org.keycloak.services.Urls;
import org.keycloak.services.resources.admin.RealmAuth;
-import javax.ws.rs.Consumes;
-import javax.ws.rs.POST;
-import javax.ws.rs.Produces;
-import javax.ws.rs.container.AsyncResponse;
-import javax.ws.rs.container.Suspended;
-import javax.ws.rs.core.Context;
-import javax.ws.rs.core.Response;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.function.Function;
-import java.util.stream.Collectors;
-import java.util.stream.Stream;
-
-import static java.util.Arrays.asList;
-
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
@@ -144,40 +146,35 @@ public class PolicyEvaluationService {
private List<ResourcePermission> createPermissions(PolicyEvaluationRequest representation, EvaluationContext evaluationContext, AuthorizationProvider authorization) {
List<PolicyEvaluationRequest.Resource> resources = representation.getResources();
return resources.stream().flatMap((Function<PolicyEvaluationRequest.Resource, Stream<ResourcePermission>>) resource -> {
- Set<String> givenScopes = resource.getScopes();
+ StoreFactory storeFactory = authorization.getStoreFactory();
+ if (resource == null) {
+ resource = new PolicyEvaluationRequest.Resource();
+ }
+
+ Set<ScopeRepresentation> givenScopes = resource.getScopes();
if (givenScopes == null) {
givenScopes = new HashSet();
}
- StoreFactory storeFactory = authorization.getStoreFactory();
+ Set<String> scopeNames = givenScopes.stream().map(ScopeRepresentation::getName).collect(Collectors.toSet());
if (resource.getId() != null) {
- Resource resourceModel = storeFactory.getResourceStore().findById(resource.getId());
- return Permissions.createResourcePermissions(resourceModel, givenScopes, authorization).stream();
+ Resource resourceModel = storeFactory.getResourceStore().findById(resource.getId(), resourceServer.getId());
+ return Permissions.createResourcePermissions(resourceModel, scopeNames, authorization).stream();
} else if (resource.getType() != null) {
- Set<String> finalGivenScopes = givenScopes;
- return storeFactory.getResourceStore().findByType(resource.getType()).stream().flatMap(resource1 -> Permissions.createResourcePermissions(resource1, finalGivenScopes, authorization).stream());
+ return storeFactory.getResourceStore().findByType(resource.getType(), resourceServer.getId()).stream().flatMap(resource1 -> Permissions.createResourcePermissions(resource1, scopeNames, authorization).stream());
} else {
ScopeStore scopeStore = storeFactory.getScopeStore();
- List<Scope> scopes = givenScopes.stream().map(scopeName -> scopeStore.findByName(scopeName, this.resourceServer.getId())).collect(Collectors.toList());
- List<ResourcePermission> collect = scopes.stream().map(scope -> new ResourcePermission(null, asList(scope), resourceServer)).collect(Collectors.toList());
-
- if (scopes.isEmpty()) {
- scopes = scopeStore.findByResourceServer(resourceServer.getId());
- }
+ List<Scope> scopes = scopeNames.stream().map(scopeName -> scopeStore.findByName(scopeName, this.resourceServer.getId())).collect(Collectors.toList());
+ List<ResourcePermission> collect = new ArrayList<ResourcePermission>();
- for (Scope scope : scopes) {
- collect.addAll(storeFactory.getResourceStore().findByScope(scope.getId()).stream().map(resource12 -> new ResourcePermission(resource12, asList(scope), resourceServer)).collect(Collectors.toList()));
+ if (!scopes.isEmpty()) {
+ collect.addAll(scopes.stream().map(scope -> new ResourcePermission(null, asList(scope), resourceServer)).collect(Collectors.toList()));
+ } else {
+ collect.addAll(Permissions.all(resourceServer, evaluationContext.getIdentity(), authorization));
}
- collect.addAll(storeFactory.getResourceStore().findByResourceServer(resourceServer.getId()).stream().map(new Function<Resource, ResourcePermission>() {
- @Override
- public ResourcePermission apply(Resource resource) {
- return new ResourcePermission(resource, resource.getScopes(), resourceServer);
- }
- }).collect(Collectors.toList()));
-
return collect.stream();
}
}).collect(Collectors.toList());
diff --git a/services/src/main/java/org/keycloak/authorization/admin/PolicyService.java b/services/src/main/java/org/keycloak/authorization/admin/PolicyService.java
index e274bea..3caf2ea 100644
--- a/services/src/main/java/org/keycloak/authorization/admin/PolicyService.java
+++ b/services/src/main/java/org/keycloak/authorization/admin/PolicyService.java
@@ -17,6 +17,28 @@
*/
package org.keycloak.authorization.admin;
+import static org.keycloak.models.utils.ModelToRepresentation.toRepresentation;
+import static org.keycloak.models.utils.RepresentationToModel.toModel;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+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.QueryParam;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.Response.Status;
+
import org.jboss.resteasy.annotations.cache.NoCache;
import org.jboss.resteasy.spi.ResteasyProviderFactory;
import org.keycloak.authorization.AuthorizationProvider;
@@ -25,35 +47,16 @@ import org.keycloak.authorization.model.ResourceServer;
import org.keycloak.authorization.policy.provider.PolicyProviderAdminService;
import org.keycloak.authorization.policy.provider.PolicyProviderFactory;
import org.keycloak.authorization.store.PolicyStore;
+import org.keycloak.authorization.store.ResourceStore;
import org.keycloak.authorization.store.StoreFactory;
import org.keycloak.models.Constants;
import org.keycloak.models.utils.ModelToRepresentation;
import org.keycloak.representations.idm.authorization.PolicyProviderRepresentation;
import org.keycloak.representations.idm.authorization.PolicyRepresentation;
import org.keycloak.representations.idm.authorization.ResourceRepresentation;
+import org.keycloak.representations.idm.authorization.ScopeRepresentation;
import org.keycloak.services.resources.admin.RealmAuth;
-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.QueryParam;
-import javax.ws.rs.core.Response;
-import javax.ws.rs.core.Response.Status;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.stream.Collectors;
-
-import static org.keycloak.models.utils.ModelToRepresentation.toRepresentation;
-import static org.keycloak.models.utils.RepresentationToModel.toModel;
-
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
@@ -100,7 +103,7 @@ public class PolicyService {
this.auth.requireManage();
representation.setId(id);
StoreFactory storeFactory = authorization.getStoreFactory();
- Policy policy = storeFactory.getPolicyStore().findById(representation.getId());
+ Policy policy = storeFactory.getPolicyStore().findById(representation.getId(), resourceServer.getId());
if (policy == null) {
return Response.status(Status.NOT_FOUND).build();
@@ -127,7 +130,7 @@ public class PolicyService {
this.auth.requireManage();
StoreFactory storeFactory = authorization.getStoreFactory();
PolicyStore policyStore = storeFactory.getPolicyStore();
- Policy policy = policyStore.findById(id);
+ Policy policy = policyStore.findById(id, resourceServer.getId());
if (policy == null) {
return Response.status(Status.NOT_FOUND).build();
@@ -143,7 +146,7 @@ public class PolicyService {
}
}
- policyStore.findDependentPolicies(id).forEach(dependentPolicy -> {
+ policyStore.findDependentPolicies(id, resourceServer.getId()).forEach(dependentPolicy -> {
if (dependentPolicy.getAssociatedPolicies().size() == 1) {
policyStore.delete(dependentPolicy.getId());
} else {
@@ -163,13 +166,109 @@ public class PolicyService {
public Response findById(@PathParam("id") String id) {
this.auth.requireView();
StoreFactory storeFactory = authorization.getStoreFactory();
- Policy model = storeFactory.getPolicyStore().findById(id);
+ Policy model = storeFactory.getPolicyStore().findById(id, resourceServer.getId());
if (model == null) {
return Response.status(Status.NOT_FOUND).build();
}
- return Response.ok(toRepresentation(model, authorization)).build();
+ return Response.ok(toRepresentation(model)).build();
+ }
+
+ @Path("{id}/dependentPolicies")
+ @GET
+ @Produces("application/json")
+ @NoCache
+ public Response getDependentPolicies(@PathParam("id") String id) {
+ this.auth.requireView();
+ StoreFactory storeFactory = authorization.getStoreFactory();
+ Policy model = storeFactory.getPolicyStore().findById(id, resourceServer.getId());
+
+ if (model == null) {
+ return Response.status(Status.NOT_FOUND).build();
+ }
+
+ List<Policy> policies = authorization.getStoreFactory().getPolicyStore().findDependentPolicies(model.getId(), resourceServer.getId());
+
+ return Response.ok(policies.stream().map(policy -> {
+ PolicyRepresentation representation1 = new PolicyRepresentation();
+
+ representation1.setId(policy.getId());
+ representation1.setName(policy.getName());
+ representation1.setType(policy.getType());
+
+ return representation1;
+ }).collect(Collectors.toList())).build();
+ }
+
+ @Path("{id}/scopes")
+ @GET
+ @Produces("application/json")
+ @NoCache
+ public Response getScopes(@PathParam("id") String id) {
+ this.auth.requireView();
+ StoreFactory storeFactory = authorization.getStoreFactory();
+ Policy model = storeFactory.getPolicyStore().findById(id, resourceServer.getId());
+
+ if (model == null) {
+ return Response.status(Status.NOT_FOUND).build();
+ }
+
+ return Response.ok(model.getScopes().stream().map(scope -> {
+ ScopeRepresentation representation = new ScopeRepresentation();
+
+ representation.setId(scope.getId());
+ representation.setName(scope.getName());
+
+ return representation;
+ }).collect(Collectors.toList())).build();
+ }
+
+ @Path("{id}/resources")
+ @GET
+ @Produces("application/json")
+ @NoCache
+ public Response getResources(@PathParam("id") String id) {
+ this.auth.requireView();
+ StoreFactory storeFactory = authorization.getStoreFactory();
+ Policy model = storeFactory.getPolicyStore().findById(id, resourceServer.getId());
+
+ if (model == null) {
+ return Response.status(Status.NOT_FOUND).build();
+ }
+
+ return Response.ok(model.getResources().stream().map(resource -> {
+ ResourceRepresentation representation = new ResourceRepresentation();
+
+ representation.setId(resource.getId());
+ representation.setName(resource.getName());
+
+ return representation;
+ }).collect(Collectors.toList())).build();
+ }
+
+ @Path("{id}/associatedPolicies")
+ @GET
+ @Produces("application/json")
+ @NoCache
+ public Response getAssociatedPolicies(@PathParam("id") String id) {
+ this.auth.requireView();
+ StoreFactory storeFactory = authorization.getStoreFactory();
+ Policy model = storeFactory.getPolicyStore().findById(id, resourceServer.getId());
+
+ if (model == null) {
+ return Response.status(Status.NOT_FOUND).build();
+ }
+
+ return Response.ok(model.getAssociatedPolicies().stream().map(policy -> {
+ PolicyRepresentation representation1 = new PolicyRepresentation();
+
+ representation1.setId(policy.getId());
+ representation1.setName(policy.getName());
+ representation1.setType(policy.getType());
+
+ return representation1;
+ }).collect(Collectors.toList())).build();
}
@Path("/search")
@@ -190,13 +289,14 @@ public class PolicyService {
return Response.status(Status.OK).build();
}
- return Response.ok(toRepresentation(model, authorization)).build();
+ return Response.ok(toRepresentation(model)).build();
}
@GET
@Produces("application/json")
@NoCache
- public Response findAll(@QueryParam("name") String name,
+ public Response findAll(@QueryParam("policyId") String id,
+ @QueryParam("name") String name,
@QueryParam("type") String type,
@QueryParam("resource") String resource,
@QueryParam("permission") Boolean permission,
@@ -206,6 +306,10 @@ public class PolicyService {
Map<String, String[]> search = new HashMap<>();
+ if (id != null && !"".equals(id.trim())) {
+ search.put("id", new String[] {id});
+ }
+
if (name != null && !"".equals(name.trim())) {
search.put("name", new String[] {name});
}
@@ -216,16 +320,17 @@ public class PolicyService {
StoreFactory storeFactory = authorization.getStoreFactory();
+ PolicyStore policyStore = storeFactory.getPolicyStore();
if (resource != null && !"".equals(resource.trim())) {
List<Policy> policies = new ArrayList<>();
HashMap<String, String[]> resourceSearch = new HashMap<>();
resourceSearch.put("name", new String[] {resource});
- storeFactory.getResourceStore().findByResourceServer(resourceSearch, resourceServer.getId(), -1, -1).forEach(resource1 -> {
- ResourceRepresentation resourceRepresentation = ModelToRepresentation.toRepresentation(resource1, resourceServer, authorization);
- resourceRepresentation.getPolicies().forEach(policyRepresentation -> {
- Policy associated = storeFactory.getPolicyStore().findById(policyRepresentation.getId());
+ ResourceStore resourceStore = storeFactory.getResourceStore();
+ resourceStore.findByResourceServer(resourceSearch, resourceServer.getId(), -1, -1).forEach(resource1 -> {
+ policyStore.findByResource(resource1.getId(), resourceServer.getId()).forEach(policyRepresentation -> {
+ Policy associated = policyStore.findById(policyRepresentation.getId(), resourceServer.getId());
policies.add(associated);
findAssociatedPolicies(associated, policies);
});
@@ -243,8 +348,8 @@ public class PolicyService {
}
return Response.ok(
- storeFactory.getPolicyStore().findByResourceServer(search, resourceServer.getId(), firstResult != null ? firstResult : -1, maxResult != null ? maxResult : Constants.DEFAULT_MAX_RESULTS).stream()
- .map(policy -> toRepresentation(policy, authorization))
+ policyStore.findByResourceServer(search, resourceServer.getId(), firstResult != null ? firstResult : -1, maxResult != null ? maxResult : Constants.DEFAULT_MAX_RESULTS).stream()
+ .map(policy -> toRepresentation(policy))
.collect(Collectors.toList()))
.build();
}
diff --git a/services/src/main/java/org/keycloak/authorization/admin/representation/PolicyEvaluationRequest.java b/services/src/main/java/org/keycloak/authorization/admin/representation/PolicyEvaluationRequest.java
index 17edef9..da7b420 100644
--- a/services/src/main/java/org/keycloak/authorization/admin/representation/PolicyEvaluationRequest.java
+++ b/services/src/main/java/org/keycloak/authorization/admin/representation/PolicyEvaluationRequest.java
@@ -22,6 +22,8 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
+import org.keycloak.representations.idm.authorization.ResourceRepresentation;
+
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
@@ -82,42 +84,7 @@ public class PolicyEvaluationRequest {
this.entitlements = entitlements;
}
- public static class Resource {
- private String id;
- private String name;
- private String type;
- private Set<String> scopes;
-
- public String getId() {
- return this.id;
- }
-
- public void setId(String id) {
- this.id = id;
- }
-
- public String getName() {
- return this.name;
- }
-
- public void setName(String name) {
- this.name = name;
- }
-
- public String getType() {
- return type;
- }
-
- public void setType(final String type) {
- this.type = type;
- }
-
- public Set<String> getScopes() {
- return scopes;
- }
+ public static class Resource extends ResourceRepresentation {
- public void setScopes(final Set<String> scopes) {
- this.scopes = scopes;
- }
}
}
diff --git a/services/src/main/java/org/keycloak/authorization/admin/representation/PolicyEvaluationResponse.java b/services/src/main/java/org/keycloak/authorization/admin/representation/PolicyEvaluationResponse.java
index 4f0c64a..ac6a97b 100644
--- a/services/src/main/java/org/keycloak/authorization/admin/representation/PolicyEvaluationResponse.java
+++ b/services/src/main/java/org/keycloak/authorization/admin/representation/PolicyEvaluationResponse.java
@@ -35,9 +35,11 @@ import org.keycloak.representations.idm.authorization.ScopeRepresentation;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@@ -62,7 +64,7 @@ public class PolicyEvaluationResponse {
AccessToken accessToken = identity.getAccessToken();
AccessToken.Authorization authorizationData = new AccessToken.Authorization();
- authorizationData.setPermissions(Permissions.allPermits(results, authorization));
+ authorizationData.setPermissions(Permissions.allPermits(results, authorization, resourceServer));
accessToken.setAuthorization(authorizationData);
response.rpt = accessToken;
@@ -80,7 +82,12 @@ public class PolicyEvaluationResponse {
resultsRep.add(rep);
if (result.getPermission().getResource() != null) {
- rep.setResource(ModelToRepresentation.toRepresentation(result.getPermission().getResource(), resourceServer, authorization));
+ ResourceRepresentation resource = new ResourceRepresentation();
+
+ resource.setId(result.getPermission().getResource().getId());
+ resource.setName(result.getPermission().getResource().getName());
+
+ rep.setResource(resource);
} else {
ResourceRepresentation resource = new ResourceRepresentation();
@@ -89,7 +96,14 @@ public class PolicyEvaluationResponse {
rep.setResource(resource);
}
- rep.setScopes(result.getPermission().getScopes().stream().map(scope -> ModelToRepresentation.toRepresentation(scope, authorization)).collect(Collectors.toList()));
+ rep.setScopes(result.getPermission().getScopes().stream().map(scope -> {
+ ScopeRepresentation representation = new ScopeRepresentation();
+
+ representation.setId(scope.getId());
+ representation.setName(scope.getName());
+
+ return representation;
+ }).collect(Collectors.toList()));
List<PolicyResultRepresentation> policies = new ArrayList<>();
@@ -100,7 +114,7 @@ public class PolicyEvaluationResponse {
rep.setPolicies(policies);
}
- resultsRep.sort((o1, o2) -> o1.getResource().getName().compareTo(o2.getResource().getName()));
+ resultsRep.sort(Comparator.comparing(o -> o.getResource().getName()));
Map<String, EvaluationResultRepresentation> groupedResults = new HashMap<>();
@@ -127,17 +141,29 @@ public class PolicyEvaluationResponse {
List<ScopeRepresentation> currentScopes = evaluationResultRepresentation.getScopes();
if (currentScopes != null) {
+ List<ScopeRepresentation> allowedScopes = result.getAllowedScopes();
for (ScopeRepresentation scope : currentScopes) {
if (!scopes.contains(scope)) {
scopes.add(scope);
}
if (evaluationResultRepresentation.getStatus().equals(Effect.PERMIT)) {
- List<ScopeRepresentation> allowedScopes = result.getAllowedScopes();
if (!allowedScopes.contains(scope)) {
allowedScopes.add(scope);
}
+ } else {
+ evaluationResultRepresentation.getPolicies().forEach(new Consumer<PolicyResultRepresentation>() {
+ @Override
+ public void accept(PolicyResultRepresentation policyResultRepresentation) {
+ if (policyResultRepresentation.getStatus().equals(Effect.PERMIT)) {
+ if (!allowedScopes.contains(scope)) {
+ allowedScopes.add(scope);
+ }
+ }
+ }
+ });
}
}
+ result.setAllowedScopes(allowedScopes);
}
if (resource.getId() != null) {
@@ -160,18 +186,14 @@ public class PolicyEvaluationResponse {
}
if (policy.getStatus().equals(Effect.DENY)) {
- Policy policyModel = authorization.getStoreFactory().getPolicyStore().findById(policy.getPolicy().getId());
+ Policy policyModel = authorization.getStoreFactory().getPolicyStore().findById(policy.getPolicy().getId(), resourceServer.getId());
for (ScopeRepresentation scope : policyModel.getScopes().stream().map(scopeModel -> ModelToRepresentation.toRepresentation(scopeModel, authorization)).collect(Collectors.toList())) {
- if (!policy.getScopes().contains(scope)) {
+ if (!policy.getScopes().contains(scope) && policyModel.getScopes().stream().filter(policyScope -> policyScope.getId().equals(scope.getId())).findFirst().isPresent()) {
+ result.getAllowedScopes().remove(scope);
policy.getScopes().add(scope);
}
}
- for (ScopeRepresentation scope : currentScopes) {
- if (!policy.getScopes().contains(scope)) {
- policy.getScopes().add(scope);
- }
- }
- }
+ } else {}
}
});
@@ -183,7 +205,14 @@ public class PolicyEvaluationResponse {
private static PolicyResultRepresentation toRepresentation(PolicyResult policy, AuthorizationProvider authorization) {
PolicyResultRepresentation policyResultRep = new PolicyResultRepresentation();
- policyResultRep.setPolicy(ModelToRepresentation.toRepresentation(policy.getPolicy(), authorization));
+ PolicyRepresentation representation = new PolicyRepresentation();
+
+ representation.setId(policy.getPolicy().getId());
+ representation.setName(policy.getPolicy().getName());
+ representation.setType(policy.getPolicy().getType());
+ representation.setDecisionStrategy(policy.getPolicy().getDecisionStrategy());
+
+ policyResultRep.setPolicy(representation);
policyResultRep.setStatus(policy.getStatus());
policyResultRep.setAssociatedPolicies(policy.getAssociatedPolicies().stream().map(result -> toRepresentation(result, authorization)).collect(Collectors.toList()));
diff --git a/services/src/main/java/org/keycloak/authorization/admin/ResourceSetService.java b/services/src/main/java/org/keycloak/authorization/admin/ResourceSetService.java
index c1cb821..bb241d8 100644
--- a/services/src/main/java/org/keycloak/authorization/admin/ResourceSetService.java
+++ b/services/src/main/java/org/keycloak/authorization/admin/ResourceSetService.java
@@ -32,8 +32,10 @@ import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserProvider;
+import org.keycloak.representations.idm.authorization.PolicyRepresentation;
import org.keycloak.representations.idm.authorization.ResourceOwnerRepresentation;
import org.keycloak.representations.idm.authorization.ResourceRepresentation;
+import org.keycloak.representations.idm.authorization.ScopeRepresentation;
import org.keycloak.services.ErrorResponse;
import org.keycloak.services.resources.admin.RealmAuth;
@@ -48,10 +50,15 @@ import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
+
+import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.List;
import java.util.Map;
+import java.util.Set;
+import java.util.function.Function;
import java.util.stream.Collectors;
import static org.keycloak.models.utils.ModelToRepresentation.toRepresentation;
@@ -127,7 +134,7 @@ public class ResourceSetService {
resource.setId(id);
StoreFactory storeFactory = this.authorization.getStoreFactory();
ResourceStore resourceStore = storeFactory.getResourceStore();
- Resource model = resourceStore.findById(resource.getId());
+ Resource model = resourceStore.findById(resource.getId(), resourceServer.getId());
if (model == null) {
return Response.status(Status.NOT_FOUND).build();
@@ -143,14 +150,14 @@ public class ResourceSetService {
public Response delete(@PathParam("id") String id) {
requireManage();
StoreFactory storeFactory = authorization.getStoreFactory();
- Resource resource = storeFactory.getResourceStore().findById(id);
+ Resource resource = storeFactory.getResourceStore().findById(id, resourceServer.getId());
if (resource == null) {
return Response.status(Status.NOT_FOUND).build();
}
PolicyStore policyStore = storeFactory.getPolicyStore();
- List<Policy> policies = policyStore.findByResource(id);
+ List<Policy> policies = policyStore.findByResource(id, resourceServer.getId());
for (Policy policyModel : policies) {
if (policyModel.getResources().size() == 1) {
@@ -172,13 +179,93 @@ public class ResourceSetService {
public Response findById(@PathParam("id") String id) {
requireView();
StoreFactory storeFactory = authorization.getStoreFactory();
- Resource model = storeFactory.getResourceStore().findById(id);
+ Resource model = storeFactory.getResourceStore().findById(id, resourceServer.getId());
if (model == null) {
return Response.status(Status.NOT_FOUND).build();
}
- return Response.ok(toRepresentation(model, this.resourceServer, authorization)).build();
+ return Response.ok(toRepresentation(model, this.resourceServer, authorization, true)).build();
+ }
+
+ @Path("{id}/scopes")
+ @GET
+ @NoCache
+ @Produces("application/json")
+ public Response getScopes(@PathParam("id") String id) {
+ requireView();
+ StoreFactory storeFactory = authorization.getStoreFactory();
+ Resource model = storeFactory.getResourceStore().findById(id, resourceServer.getId());
+
+ if (model == null) {
+ return Response.status(Status.NOT_FOUND).build();
+ }
+
+ List<ScopeRepresentation> scopes = model.getScopes().stream().map(scope -> {
+ ScopeRepresentation representation = new ScopeRepresentation();
+
+ representation.setId(scope.getId());
+ representation.setName(scope.getName());
+
+ return representation;
+ }).collect(Collectors.toList());
+
+ if (model.getType() != null) {
+ ResourceStore resourceStore = authorization.getStoreFactory().getResourceStore();
+ for (Resource typed : resourceStore.findByType(model.getType(), resourceServer.getId())) {
+ if (typed.getOwner().equals(resourceServer.getClientId()) && !typed.getId().equals(model.getId())) {
+ scopes.addAll(typed.getScopes().stream().map(model1 -> {
+ ScopeRepresentation scope = new ScopeRepresentation();
+ scope.setId(model1.getId());
+ scope.setName(model1.getName());
+ String iconUri = model1.getIconUri();
+ if (iconUri != null) {
+ scope.setIconUri(iconUri);
+ }
+ return scope;
+ }).filter(scopeRepresentation -> !scopes.contains(scopeRepresentation)).collect(Collectors.toList()));
+ }
+ }
+ }
+
+ return Response.ok(scopes).build();
+ }
+
+ @Path("{id}/permissions")
+ @GET
+ @NoCache
+ @Produces("application/json")
+ public Response getPermissions(@PathParam("id") String id) {
+ requireView();
+ StoreFactory storeFactory = authorization.getStoreFactory();
+ Resource model = storeFactory.getResourceStore().findById(id, resourceServer.getId());
+
+ if (model == null) {
+ return Response.status(Status.NOT_FOUND).build();
+ }
+
+ PolicyStore policyStore = authorization.getStoreFactory().getPolicyStore();
+ Set<Policy> policies = new HashSet<>();
+
+ policies.addAll(policyStore.findByResource(model.getId(), resourceServer.getId()));
+ policies.addAll(policyStore.findByResourceType(model.getType(), resourceServer.getId()));
+ policies.addAll(policyStore.findByScopeIds(model.getScopes().stream().map(scope -> scope.getId()).collect(Collectors.toList()), resourceServer.getId()));
+
+ List<PolicyRepresentation> representation = new ArrayList<>();
+
+ for (Policy policyModel : policies) {
+ PolicyRepresentation policy = new PolicyRepresentation();
+
+ policy.setId(policyModel.getId());
+ policy.setName(policyModel.getName());
+ policy.setType(policyModel.getType());
+
+ if (!representation.contains(policy)) {
+ representation.add(policy);
+ }
+ }
+
+ return Response.ok(representation).build();
}
@Path("/search")
@@ -205,18 +292,29 @@ public class ResourceSetService {
@GET
@NoCache
@Produces("application/json")
- public Response find(@QueryParam("name") String name,
+ public Response find(@QueryParam("_id") String id,
+ @QueryParam("name") String name,
@QueryParam("uri") String uri,
@QueryParam("owner") String owner,
@QueryParam("type") String type,
@QueryParam("scope") String scope,
+ @QueryParam("deep") Boolean deep,
@QueryParam("first") Integer firstResult,
@QueryParam("max") Integer maxResult) {
requireView();
+
StoreFactory storeFactory = authorization.getStoreFactory();
+ if (deep == null) {
+ deep = true;
+ }
+
Map<String, String[]> search = new HashMap<>();
+ if (id != null && !"".equals(id.trim())) {
+ search.put("id", new String[] {id});
+ }
+
if (name != null && !"".equals(name.trim())) {
search.put("name", new String[] {name});
}
@@ -260,9 +358,10 @@ public class ResourceSetService {
search.put("scope", scopes.stream().map(Scope::getId).toArray(String[]::new));
}
+ Boolean finalDeep = deep;
return Response.ok(
storeFactory.getResourceStore().findByResourceServer(search, this.resourceServer.getId(), firstResult != null ? firstResult : -1, maxResult != null ? maxResult : Constants.DEFAULT_MAX_RESULTS).stream()
- .map(resource -> toRepresentation(resource, this.resourceServer, authorization))
+ .map(resource -> toRepresentation(resource, resourceServer, authorization, finalDeep))
.collect(Collectors.toList()))
.build();
}
diff --git a/services/src/main/java/org/keycloak/authorization/admin/ScopeService.java b/services/src/main/java/org/keycloak/authorization/admin/ScopeService.java
index 83cffeb..7724830 100644
--- a/services/src/main/java/org/keycloak/authorization/admin/ScopeService.java
+++ b/services/src/main/java/org/keycloak/authorization/admin/ScopeService.java
@@ -26,6 +26,8 @@ import org.keycloak.authorization.model.Scope;
import org.keycloak.authorization.store.PolicyStore;
import org.keycloak.authorization.store.StoreFactory;
import org.keycloak.models.Constants;
+import org.keycloak.representations.idm.authorization.PolicyRepresentation;
+import org.keycloak.representations.idm.authorization.ResourceRepresentation;
import org.keycloak.representations.idm.authorization.ScopeRepresentation;
import org.keycloak.services.ErrorResponse;
import org.keycloak.services.resources.admin.RealmAuth;
@@ -41,6 +43,7 @@ import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
+
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
@@ -85,7 +88,7 @@ public class ScopeService {
this.auth.requireManage();
scope.setId(id);
StoreFactory storeFactory = authorization.getStoreFactory();
- Scope model = storeFactory.getScopeStore().findById(scope.getId());
+ Scope model = storeFactory.getScopeStore().findById(scope.getId(), resourceServer.getId());
if (model == null) {
return Response.status(Status.NOT_FOUND).build();
@@ -101,13 +104,13 @@ public class ScopeService {
public Response delete(@PathParam("id") String id) {
this.auth.requireManage();
StoreFactory storeFactory = authorization.getStoreFactory();
- List<Resource> resources = storeFactory.getResourceStore().findByScope(id);
+ List<Resource> resources = storeFactory.getResourceStore().findByScope(Arrays.asList(id), resourceServer.getId());
if (!resources.isEmpty()) {
return ErrorResponse.exists("Scopes can not be removed while associated with resources.");
}
- Scope scope = storeFactory.getScopeStore().findById(id);
+ Scope scope = storeFactory.getScopeStore().findById(id, resourceServer.getId());
if (scope == null) {
return Response.status(Status.NOT_FOUND).build();
@@ -134,7 +137,7 @@ public class ScopeService {
@Produces("application/json")
public Response findById(@PathParam("id") String id) {
this.auth.requireView();
- Scope model = this.authorization.getStoreFactory().getScopeStore().findById(id);
+ Scope model = this.authorization.getStoreFactory().getScopeStore().findById(id, resourceServer.getId());
if (model == null) {
return Response.status(Status.NOT_FOUND).build();
@@ -143,6 +146,53 @@ public class ScopeService {
return Response.ok(toRepresentation(model, this.authorization)).build();
}
+ @Path("{id}/resources")
+ @GET
+ @Produces("application/json")
+ public Response getResources(@PathParam("id") String id) {
+ this.auth.requireView();
+ StoreFactory storeFactory = this.authorization.getStoreFactory();
+ Scope model = storeFactory.getScopeStore().findById(id, resourceServer.getId());
+
+ if (model == null) {
+ return Response.status(Status.NOT_FOUND).build();
+ }
+
+ return Response.ok(storeFactory.getResourceStore().findByScope(Arrays.asList(model.getId()), resourceServer.getId()).stream().map(resource -> {
+ ResourceRepresentation representation = new ResourceRepresentation();
+
+ representation.setId(resource.getId());
+ representation.setName(resource.getName());
+
+ return representation;
+ }).collect(Collectors.toList())).build();
+ }
+
+ @Path("{id}/permissions")
+ @GET
+ @Produces("application/json")
+ public Response getPermissions(@PathParam("id") String id) {
+ this.auth.requireView();
+ StoreFactory storeFactory = this.authorization.getStoreFactory();
+ Scope model = storeFactory.getScopeStore().findById(id, resourceServer.getId());
+
+ if (model == null) {
+ return Response.status(Status.NOT_FOUND).build();
+ }
+
+ PolicyStore policyStore = storeFactory.getPolicyStore();
+
+ return Response.ok(policyStore.findByScopeIds(Arrays.asList(model.getId()), resourceServer.getId()).stream().map(policy -> {
+ PolicyRepresentation representation = new PolicyRepresentation();
+
+ representation.setId(policy.getId());
+ representation.setName(policy.getName());
+ representation.setType(policy.getType());
+
+ return representation;
+ }).collect(Collectors.toList())).build();
+ }
+
@Path("/search")
@GET
@Produces("application/json")
@@ -166,20 +216,31 @@ public class ScopeService {
@GET
@Produces("application/json")
- public Response findAll(@QueryParam("name") String name,
+ public Response findAll(@QueryParam("scopeId") String id,
+ @QueryParam("name") String name,
+ @QueryParam("deep") Boolean deep,
@QueryParam("first") Integer firstResult,
@QueryParam("max") Integer maxResult) {
this.auth.requireView();
+ if (deep == null) {
+ deep = true;
+ }
+
Map<String, String[]> search = new HashMap<>();
+ if (id != null && !"".equals(id.trim())) {
+ search.put("id", new String[] {id});
+ }
+
if (name != null && !"".equals(name.trim())) {
search.put("name", new String[] {name});
}
+ Boolean finalDeep = deep;
return Response.ok(
this.authorization.getStoreFactory().getScopeStore().findByResourceServer(search, this.resourceServer.getId(), firstResult != null ? firstResult : -1, maxResult != null ? maxResult : Constants.DEFAULT_MAX_RESULTS).stream()
- .map(scope -> toRepresentation(scope, this.authorization))
+ .map(scope -> toRepresentation(scope, this.authorization, finalDeep))
.collect(Collectors.toList()))
.build();
}
diff --git a/services/src/main/java/org/keycloak/authorization/authorization/AuthorizationTokenService.java b/services/src/main/java/org/keycloak/authorization/authorization/AuthorizationTokenService.java
index 77a35e9..e60a0d6 100644
--- a/services/src/main/java/org/keycloak/authorization/authorization/AuthorizationTokenService.java
+++ b/services/src/main/java/org/keycloak/authorization/authorization/AuthorizationTokenService.java
@@ -110,7 +110,7 @@ public class AuthorizationTokenService {
authorization.evaluators().from(createPermissions(ticket, authorizationRequest, authorization), evaluationContext).evaluate(new DecisionResultCollector() {
@Override
public void onComplete(List<Result> results) {
- List<Permission> entitlements = Permissions.allPermits(results, authorization);
+ List<Permission> entitlements = Permissions.permits(results, authorization, ticket.getResourceServerId());
if (entitlements.isEmpty()) {
HashMap<Object, Object> error = new HashMap<>();
@@ -144,7 +144,7 @@ public class AuthorizationTokenService {
Resource resource;
if (requestedResource.getId() != null) {
- resource = storeFactory.getResourceStore().findById(requestedResource.getId());
+ resource = storeFactory.getResourceStore().findById(requestedResource.getId(), ticket.getResourceServerId());
} else {
resource = storeFactory.getResourceStore().findByName(requestedResource.getName(), ticket.getResourceServerId());
}
@@ -171,7 +171,7 @@ public class AuthorizationTokenService {
}
return scope.getId();
- }).filter(s -> s != null).collect(Collectors.toList()).toArray(new String[requestedScopes.size()])));
+ }).filter(s -> s != null).collect(Collectors.toList()), ticket.getResourceServerId()));
for (Resource resource1 : resources) {
permissionsToEvaluate.put(resource1.getId(), collect);
@@ -204,7 +204,7 @@ public class AuthorizationTokenService {
if (permissions != null) {
permissions.forEach(permission -> {
- Resource resourcePermission = storeFactory.getResourceStore().findById(permission.getResourceSetId());
+ Resource resourcePermission = storeFactory.getResourceStore().findById(permission.getResourceSetId(), ticket.getResourceServerId());
if (resourcePermission != null) {
Set<String> scopes = permissionsToEvaluate.get(resourcePermission.getId());
@@ -240,7 +240,7 @@ public class AuthorizationTokenService {
}).collect(Collectors.toList());
return Arrays.asList(new ResourcePermission(null, scopes, resourceServer)).stream();
} else {
- Resource entryResource = storeFactory.getResourceStore().findById(key);
+ Resource entryResource = storeFactory.getResourceStore().findById(key, resourceServer.getId());
return Permissions.createResourcePermissions(entryResource, entry.getValue(), authorization).stream();
}
}).collect(Collectors.toList());
diff --git a/services/src/main/java/org/keycloak/authorization/config/UmaWellKnownProvider.java b/services/src/main/java/org/keycloak/authorization/config/UmaWellKnownProvider.java
index 884f028..222e754 100644
--- a/services/src/main/java/org/keycloak/authorization/config/UmaWellKnownProvider.java
+++ b/services/src/main/java/org/keycloak/authorization/config/UmaWellKnownProvider.java
@@ -47,7 +47,7 @@ public class UmaWellKnownProvider implements WellKnownProvider {
return Configuration.fromDefault(RealmsResource.realmBaseUrl(uriInfo).build(realm.getName()).toString(), realm.getName(),
URI.create(RealmsResource.protocolUrl(uriInfo).path(OIDCLoginProtocolService.class, "auth").build(realm.getName(), OIDCLoginProtocol.LOGIN_PROTOCOL).toString()),
URI.create(RealmsResource.protocolUrl(uriInfo).path(OIDCLoginProtocolService.class, "token").build(realm.getName(), OIDCLoginProtocol.LOGIN_PROTOCOL).toString()),
- PemUtils.encodeKey(session.keys().getActiveKey(realm).getPublicKey()));
+ PemUtils.encodeKey(session.keys().getActiveRsaKey(realm).getPublicKey()));
}
@Override
diff --git a/services/src/main/java/org/keycloak/authorization/DefaultAuthorizationProviderFactory.java b/services/src/main/java/org/keycloak/authorization/DefaultAuthorizationProviderFactory.java
index 6146594..159e5aa 100644
--- a/services/src/main/java/org/keycloak/authorization/DefaultAuthorizationProviderFactory.java
+++ b/services/src/main/java/org/keycloak/authorization/DefaultAuthorizationProviderFactory.java
@@ -18,14 +18,20 @@
package org.keycloak.authorization;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Executor;
+
import org.keycloak.Config;
+import org.keycloak.authorization.policy.provider.PolicyProvider;
+import org.keycloak.authorization.policy.provider.PolicyProviderFactory;
import org.keycloak.authorization.store.StoreFactory;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.RealmModel;
import org.keycloak.models.cache.authorization.CachedStoreFactoryProvider;
-
-import java.util.concurrent.Executor;
+import org.keycloak.provider.ProviderFactory;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
@@ -33,6 +39,7 @@ import java.util.concurrent.Executor;
public class DefaultAuthorizationProviderFactory implements AuthorizationProviderFactory {
private Executor scheduler;
+ private Map<String, PolicyProviderFactory> policyProviderFactories;
@Override
public AuthorizationProvider create(KeycloakSession session) {
@@ -54,6 +61,7 @@ public class DefaultAuthorizationProviderFactory implements AuthorizationProvide
@Override
public void postInit(KeycloakSessionFactory factory) {
+ policyProviderFactories = configurePolicyProviderFactories(factory);
}
@Override
@@ -74,6 +82,21 @@ public class DefaultAuthorizationProviderFactory implements AuthorizationProvide
storeFactory = session.getProvider(StoreFactory.class);
}
- return new AuthorizationProvider(session, realm, storeFactory);
+ return new AuthorizationProvider(session, realm, storeFactory, policyProviderFactories);
+ }
+
+ private Map<String, PolicyProviderFactory> configurePolicyProviderFactories(KeycloakSessionFactory keycloakSessionFactory) {
+ List<ProviderFactory> providerFactories = keycloakSessionFactory.getProviderFactories(PolicyProvider.class);
+
+ if (providerFactories.isEmpty()) {
+ throw new RuntimeException("Could not find any policy provider.");
+ }
+
+ HashMap<String, PolicyProviderFactory> providers = new HashMap<>();
+
+ providerFactories.forEach(providerFactory -> providers.put(providerFactory.getId(), (PolicyProviderFactory) providerFactory));
+
+ return providers;
}
+
}
\ No newline at end of file
diff --git a/services/src/main/java/org/keycloak/authorization/entitlement/EntitlementService.java b/services/src/main/java/org/keycloak/authorization/entitlement/EntitlementService.java
index 9f89e4a..894c3c7 100644
--- a/services/src/main/java/org/keycloak/authorization/entitlement/EntitlementService.java
+++ b/services/src/main/java/org/keycloak/authorization/entitlement/EntitlementService.java
@@ -46,6 +46,7 @@ import org.keycloak.protocol.oidc.TokenManager;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.idm.authorization.Permission;
import org.keycloak.representations.idm.authorization.ScopeRepresentation;
+import org.keycloak.services.ErrorResponse;
import org.keycloak.services.ErrorResponseException;
import org.keycloak.services.resources.Cors;
@@ -125,7 +126,7 @@ public class EntitlementService {
@Override
protected void onComplete(List<Result> results) {
- List<Permission> entitlements = Permissions.allPermits(results, authorization);
+ List<Permission> entitlements = Permissions.allPermits(results, authorization, resourceServer);
if (entitlements.isEmpty()) {
HashMap<Object, Object> error = new HashMap<>();
@@ -169,31 +170,40 @@ public class EntitlementService {
StoreFactory storeFactory = authorization.getStoreFactory();
ResourceServer resourceServer = storeFactory.getResourceServerStore().findByClient(client.getId());
- authorization.evaluators().from(createPermissions(entitlementRequest, resourceServer, authorization), new KeycloakEvaluationContext(this.authorization.getKeycloakSession())).evaluate(new DecisionResultCollector() {
-
- @Override
- public void onError(Throwable cause) {
- asyncResponse.resume(cause);
- }
+ try {
+ authorization.evaluators().from(createPermissions(entitlementRequest, resourceServer, authorization), new KeycloakEvaluationContext(this.authorization.getKeycloakSession())).evaluate(new DecisionResultCollector() {
+ @Override
+ public void onError(Throwable cause) {
+ asyncResponse.resume(cause);
+ }
- @Override
- protected void onComplete(List<Result> results) {
- List<Permission> entitlements = Permissions.allPermits(results, authorization);
+ @Override
+ protected void onComplete(List<Result> results) {
+ List<Permission> entitlements = Permissions.permits(results, authorization, resourceServer.getId());
- if (entitlements.isEmpty()) {
- HashMap<Object, Object> error = new HashMap<>();
+ if (entitlements.isEmpty()) {
+ HashMap<Object, Object> error = new HashMap<>();
- error.put(OAuth2Constants.ERROR, "not_authorized");
+ error.put(OAuth2Constants.ERROR, "not_authorized");
- asyncResponse.resume(Cors.add(request, Response.status(Status.FORBIDDEN)
- .entity(error))
- .allowedOrigins(identity.getAccessToken())
- .exposedHeaders(Cors.ACCESS_CONTROL_ALLOW_METHODS).build());
- } else {
- asyncResponse.resume(Cors.add(request, Response.ok().entity(new EntitlementResponse(createRequestingPartyToken(entitlements)))).allowedOrigins(identity.getAccessToken()).allowedMethods("GET").exposedHeaders(Cors.ACCESS_CONTROL_ALLOW_METHODS).build());
+ asyncResponse.resume(Cors.add(request, Response.status(Status.FORBIDDEN)
+ .entity(error))
+ .allowedOrigins(identity.getAccessToken())
+ .exposedHeaders(Cors.ACCESS_CONTROL_ALLOW_METHODS).build());
+ } else {
+ asyncResponse.resume(Cors.add(request, Response.ok().entity(new EntitlementResponse(createRequestingPartyToken(entitlements)))).allowedOrigins(identity.getAccessToken()).allowedMethods("GET").exposedHeaders(Cors.ACCESS_CONTROL_ALLOW_METHODS).build());
+ }
}
+ });
+ } catch (Exception e) {
+ String message = e.getMessage();
+
+ if (message == null) {
+ message = "Could not process authorization request";
}
- });
+
+ asyncResponse.resume(ErrorResponse.error(message, Status.BAD_REQUEST));
+ }
}
private String createRequestingPartyToken(List<Permission> permissions) {
@@ -215,7 +225,7 @@ public class EntitlementService {
Resource resource;
if (requestedResource.getResourceSetId() != null) {
- resource = storeFactory.getResourceStore().findById(requestedResource.getResourceSetId());
+ resource = storeFactory.getResourceStore().findById(requestedResource.getResourceSetId(), resourceServer.getId());
} else {
resource = storeFactory.getResourceStore().findByName(requestedResource.getResourceSetName(), resourceServer.getId());
}
@@ -242,7 +252,7 @@ public class EntitlementService {
}
return scope.getId();
- }).filter(s -> s != null).collect(Collectors.toList()).toArray(new String[requestedScopes.size()])));
+ }).filter(s -> s != null).collect(Collectors.toList()), resourceServer.getId()));
for (Resource resource1 : resources) {
permissionsToEvaluate.put(resource1.getId(), collect);
@@ -276,7 +286,7 @@ public class EntitlementService {
if (permissions != null) {
permissions.forEach(permission -> {
- Resource resourcePermission = storeFactory.getResourceStore().findById(permission.getResourceSetId());
+ Resource resourcePermission = storeFactory.getResourceStore().findById(permission.getResourceSetId(), resourceServer.getId());
if (resourcePermission != null) {
Set<String> scopes = permissionsToEvaluate.get(resourcePermission.getId());
@@ -310,7 +320,7 @@ public class EntitlementService {
}).collect(Collectors.toList());
return Arrays.asList(new ResourcePermission(null, scopes, resourceServer)).stream();
} else {
- Resource entryResource = storeFactory.getResourceStore().findById(key);
+ Resource entryResource = storeFactory.getResourceStore().findById(key, resourceServer.getId());
return Permissions.createResourcePermissions(entryResource, entry.getValue(), authorization).stream();
}
}).collect(Collectors.toList());
diff --git a/services/src/main/java/org/keycloak/authorization/protection/permission/AbstractPermissionService.java b/services/src/main/java/org/keycloak/authorization/protection/permission/AbstractPermissionService.java
index eee504a..80fb84a 100644
--- a/services/src/main/java/org/keycloak/authorization/protection/permission/AbstractPermissionService.java
+++ b/services/src/main/java/org/keycloak/authorization/protection/permission/AbstractPermissionService.java
@@ -77,7 +77,7 @@ public class AbstractPermissionService {
if (!resourceNotProvider) {
if (resourceSetId != null) {
- resource = storeFactory.getResourceStore().findById(resourceSetId);
+ resource = storeFactory.getResourceStore().findById(resourceSetId, resourceServer.getId());
} else {
resource = storeFactory.getResourceStore().findByName(resourceSetName, this.resourceServer.getId());
}
@@ -113,7 +113,7 @@ public class AbstractPermissionService {
}
}
- for (Resource baseResource : authorization.getStoreFactory().getResourceStore().findByType(resource.getType())) {
+ for (Resource baseResource : authorization.getStoreFactory().getResourceStore().findByType(resource.getType(), resourceServer.getId())) {
if (baseResource.getOwner().equals(resource.getResourceServer().getClientId())) {
for (Scope baseScope : baseResource.getScopes()) {
if (baseScope.getName().equals(scopeName)) {
@@ -131,7 +131,7 @@ public class AbstractPermissionService {
}
private String createPermissionTicket(List<ResourceRepresentation> resources) {
- KeyManager.ActiveKey keys = this.authorization.getKeycloakSession().keys().getActiveKey(this.authorization.getRealm());
+ KeyManager.ActiveRsaKey keys = this.authorization.getKeycloakSession().keys().getActiveRsaKey(this.authorization.getRealm());
return new JWSBuilder().kid(keys.getKid()).jsonContent(new PermissionTicket(resources, this.resourceServer.getId(), this.identity.getAccessToken()))
.rsa256(keys.getPrivateKey());
}
diff --git a/services/src/main/java/org/keycloak/authorization/protection/resource/ResourceService.java b/services/src/main/java/org/keycloak/authorization/protection/resource/ResourceService.java
index fdaa12f..3573d3d 100644
--- a/services/src/main/java/org/keycloak/authorization/protection/resource/ResourceService.java
+++ b/services/src/main/java/org/keycloak/authorization/protection/resource/ResourceService.java
@@ -109,7 +109,7 @@ public class ResourceService {
}
private Set<String> findAll() {
- Response response = this.resourceManager.find(null, null, null, null, null, -1, -1);
+ Response response = this.resourceManager.find(null, null, null, null, null, null, true, -1, -1);
List<ResourceRepresentation> resources = (List<ResourceRepresentation>) response.getEntity();
return resources.stream().map(ResourceRepresentation::getId).collect(Collectors.toSet());
}
@@ -132,25 +132,23 @@ public class ResourceService {
if ("name".equals(filterType)) {
- resources.addAll(storeFactory.getResourceStore().findByResourceServer(this.resourceServer.getId()).stream().filter(description -> filterValue == null || filterValue.equals(description.getName())).collect(Collectors.toSet()).stream()
- .map(resource -> ModelToRepresentation.toRepresentation(resource, this.resourceServer, authorization))
- .collect(Collectors.toList()));
+ resources.add(ModelToRepresentation.toRepresentation(storeFactory.getResourceStore().findByName(filterValue, this.resourceServer.getId()), resourceServer, authorization));
} else if ("type".equals(filterType)) {
resources.addAll(storeFactory.getResourceStore().findByResourceServer(this.resourceServer.getId()).stream().filter(description -> filterValue == null || filterValue.equals(description.getType())).collect(Collectors.toSet()).stream()
.map(resource -> ModelToRepresentation.toRepresentation(resource, this.resourceServer, authorization))
.collect(Collectors.toList()));
} else if ("uri".equals(filterType)) {
- resources.addAll(storeFactory.getResourceStore().findByResourceServer(this.resourceServer.getId()).stream().filter(description -> filterValue == null || filterValue.equals(description.getUri())).collect(Collectors.toSet()).stream()
+ resources.addAll(storeFactory.getResourceStore().findByUri(filterValue, this.resourceServer.getId()).stream()
.map(resource -> ModelToRepresentation.toRepresentation(resource, this.resourceServer, authorization))
.collect(Collectors.toList()));
} else if ("owner".equals(filterType)) {
- resources.addAll(storeFactory.getResourceStore().findByResourceServer(this.resourceServer.getId()).stream().filter(description -> filterValue == null || filterValue.equals(description.getOwner())).collect(Collectors.toSet()).stream()
+ resources.addAll(storeFactory.getResourceStore().findByOwner(filterValue, this.resourceServer.getId()).stream()
.map(resource -> ModelToRepresentation.toRepresentation(resource, this.resourceServer, authorization))
.collect(Collectors.toList()));
}
}
} else {
- resources = storeFactory.getResourceStore().findByOwner(identity.getId()).stream()
+ resources = storeFactory.getResourceStore().findByOwner(identity.getId(), resourceServer.getId()).stream()
.map(resource -> ModelToRepresentation.toRepresentation(resource, this.resourceServer, authorization))
.collect(Collectors.toSet());
}
diff --git a/services/src/main/java/org/keycloak/authorization/util/Permissions.java b/services/src/main/java/org/keycloak/authorization/util/Permissions.java
index 116ddd6..90de912 100644
--- a/services/src/main/java/org/keycloak/authorization/util/Permissions.java
+++ b/services/src/main/java/org/keycloak/authorization/util/Permissions.java
@@ -18,9 +18,18 @@
package org.keycloak.authorization.util;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
import org.keycloak.authorization.AuthorizationProvider;
import org.keycloak.authorization.Decision.Effect;
import org.keycloak.authorization.identity.Identity;
+import org.keycloak.authorization.model.Policy;
import org.keycloak.authorization.model.Resource;
import org.keycloak.authorization.model.ResourceServer;
import org.keycloak.authorization.model.Scope;
@@ -31,16 +40,6 @@ import org.keycloak.authorization.store.ScopeStore;
import org.keycloak.authorization.store.StoreFactory;
import org.keycloak.representations.idm.authorization.Permission;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.stream.Collectors;
-
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
@@ -62,8 +61,8 @@ public final class Permissions {
StoreFactory storeFactory = authorization.getStoreFactory();
ResourceStore resourceStore = storeFactory.getResourceStore();
- resourceStore.findByOwner(resourceServer.getClientId()).stream().forEach(resource -> permissions.addAll(createResourcePermissions(resource, resource.getScopes().stream().map(Scope::getName).collect(Collectors.toSet()), authorization)));
- resourceStore.findByOwner(identity.getId()).stream().forEach(resource -> permissions.addAll(createResourcePermissions(resource, resource.getScopes().stream().map(Scope::getName).collect(Collectors.toSet()), authorization)));
+ resourceStore.findByOwner(resourceServer.getClientId(), resourceServer.getId()).stream().forEach(resource -> permissions.addAll(createResourcePermissionsWithScopes(resource, resource.getScopes(), authorization)));
+ resourceStore.findByOwner(identity.getId(), resourceServer.getId()).stream().forEach(resource -> permissions.addAll(createResourcePermissionsWithScopes(resource, resource.getScopes(), authorization)));
return permissions;
}
@@ -81,7 +80,7 @@ public final class Permissions {
if (type != null && !resource.getOwner().equals(resourceServer.getClientId())) {
StoreFactory storeFactory = authorization.getStoreFactory();
ResourceStore resourceStore = storeFactory.getResourceStore();
- resourceStore.findByType(type).forEach(resource1 -> {
+ resourceStore.findByType(type, resourceServer.getId()).forEach(resource1 -> {
if (resource1.getOwner().equals(resourceServer.getClientId())) {
for (Scope typeScope : resource1.getScopes()) {
if (!scopes.contains(typeScope)) {
@@ -95,78 +94,141 @@ public final class Permissions {
ScopeStore scopeStore = authorization.getStoreFactory().getScopeStore();
scopes = requestedScopes.stream().map(scopeName -> {
Scope byName = scopeStore.findByName(scopeName, resource.getResourceServer().getId());
+
+ if (byName == null) {
+ throw new RuntimeException("Invalid scope [" + scopeName + "].");
+ }
+
return byName;
}).collect(Collectors.toList());
}
- if (scopes.isEmpty()) {
- permissions.add(new ResourcePermission(resource, Collections.emptyList(), resource.getResourceServer()));
- } else {
- for (Scope scope : scopes) {
- permissions.add(new ResourcePermission(resource, Arrays.asList(scope), resource.getResourceServer()));
- }
+ permissions.add(new ResourcePermission(resource, scopes, resource.getResourceServer()));
+
+ return permissions;
+ }
+
+ public static List<ResourcePermission> createResourcePermissionsWithScopes(Resource resource, List<Scope> scopes, AuthorizationProvider authorization) {
+ List<ResourcePermission> permissions = new ArrayList<>();
+ String type = resource.getType();
+ ResourceServer resourceServer = resource.getResourceServer();
+
+ // check if there is a typed resource whose scopes are inherited by the resource being requested. In this case, we assume that parent resource
+ // is owned by the resource server itself
+ if (type != null && !resource.getOwner().equals(resourceServer.getClientId())) {
+ StoreFactory storeFactory = authorization.getStoreFactory();
+ ResourceStore resourceStore = storeFactory.getResourceStore();
+ resourceStore.findByType(type, resourceServer.getId()).forEach(resource1 -> {
+ if (resource1.getOwner().equals(resourceServer.getClientId())) {
+ for (Scope typeScope : resource1.getScopes()) {
+ if (!scopes.contains(typeScope)) {
+ scopes.add(typeScope);
+ }
+ }
+ }
+ });
}
+ permissions.add(new ResourcePermission(resource, scopes, resource.getResourceServer()));
+
return permissions;
}
- public static List<Permission> allPermits(List<Result> evaluation, AuthorizationProvider authorizationProvider) {
+ public static List<Permission> allPermits(List<Result> evaluation, AuthorizationProvider authorizationProvider, ResourceServer resourceServer) {
+ Map<String, Permission> permissions = new HashMap<>();
+
+ for (Result evaluationResult : evaluation) {
+ ResourcePermission permission = evaluationResult.getPermission();
+
+ // if overall decision was a DENY, check for scope-based policies for a PERMIT
+ if (evaluationResult.getEffect().equals(Effect.DENY)) {
+ for (Result.PolicyResult result : evaluationResult.getResults()) {
+ Policy policy = result.getPolicy();
+
+ if ("scope".equals(policy.getType())) {
+ Set<Resource> resources = policy.getResources();
+
+ if (Effect.PERMIT.equals(result.getStatus())) {
+ List<Scope> scopes = policy.getScopes().stream().collect(Collectors.toList());
+
+ if (!resources.isEmpty()) {
+ resources.forEach(resource -> grantPermission(authorizationProvider, permissions, new ResourcePermission(resource, scopes, policy.getResourceServer()), resourceServer.getId()));
+ } else {
+ grantPermission(authorizationProvider, permissions, new ResourcePermission(permission.getResource(), scopes, policy.getResourceServer()), resourceServer.getId());
+ }
+ }
+ }
+ }
+ continue;
+ }
+
+ grantPermission(authorizationProvider, permissions, permission, resourceServer.getId());
+ }
+
+ return permissions.values().stream().collect(Collectors.toList());
+ }
+
+ public static List<Permission> permits(List<Result> evaluation, AuthorizationProvider authorizationProvider, String resourceServer) {
Map<String, Permission> permissions = new HashMap<>();
for (Result evaluationResult : evaluation) {
ResourcePermission permission = evaluationResult.getPermission();
- Set<String> scopes = permission.getScopes().stream().map(Scope::getName).collect(Collectors.toSet());
if (evaluationResult.getEffect().equals(Effect.DENY)) {
continue;
}
- List<Resource> resources = new ArrayList<>();
- Resource resource = permission.getResource();
+ grantPermission(authorizationProvider, permissions, permission, resourceServer);
+ }
- if (resource != null) {
- resources.add(resource);
- } else {
- List<Scope> permissionScopes = permission.getScopes();
+ return permissions.values().stream().collect(Collectors.toList());
+ }
- if (!permissionScopes.isEmpty()) {
- ResourceStore resourceStore = authorizationProvider.getStoreFactory().getResourceStore();
- resources.addAll(resourceStore.findByScope(permissionScopes.stream().map(Scope::getId).collect(Collectors.toList()).toArray(new String[permissionScopes.size()])));
- }
+ private static void grantPermission(AuthorizationProvider authorizationProvider, Map<String, Permission> permissions, ResourcePermission permission, String resourceServer) {
+ List<Resource> resources = new ArrayList<>();
+ Resource resource = permission.getResource();
+ Set<String> scopes = permission.getScopes().stream().map(Scope::getName).collect(Collectors.toSet());
+
+ if (resource != null) {
+ resources.add(resource);
+ } else {
+ List<Scope> permissionScopes = permission.getScopes();
+
+ if (!permissionScopes.isEmpty()) {
+ ResourceStore resourceStore = authorizationProvider.getStoreFactory().getResourceStore();
+ resources.addAll(resourceStore.findByScope(permissionScopes.stream().map(Scope::getId).collect(Collectors.toList()), resourceServer));
}
+ }
- if (!resources.isEmpty()) {
- for (Resource allowedResource : resources) {
- String resourceId = allowedResource.getId();
- String resourceName = allowedResource.getName();
- Permission evalPermission = permissions.get(allowedResource.getId());
+ if (!resources.isEmpty()) {
+ for (Resource allowedResource : resources) {
+ String resourceId = allowedResource.getId();
+ String resourceName = allowedResource.getName();
+ Permission evalPermission = permissions.get(allowedResource.getId());
- if (evalPermission == null) {
- evalPermission = new Permission(resourceId, resourceName, scopes);
- permissions.put(resourceId, evalPermission);
- }
+ if (evalPermission == null) {
+ evalPermission = new Permission(resourceId, resourceName, scopes);
+ permissions.put(resourceId, evalPermission);
+ }
- if (scopes != null && !scopes.isEmpty()) {
- Set<String> finalScopes = evalPermission.getScopes();
+ if (scopes != null && !scopes.isEmpty()) {
+ Set<String> finalScopes = evalPermission.getScopes();
- if (finalScopes == null) {
- finalScopes = new HashSet();
- evalPermission.setScopes(finalScopes);
- }
+ if (finalScopes == null) {
+ finalScopes = new HashSet();
+ evalPermission.setScopes(finalScopes);
+ }
- for (String scopeName : scopes) {
- if (!finalScopes.contains(scopeName)) {
- finalScopes.add(scopeName);
- }
+ for (String scopeName : scopes) {
+ if (!finalScopes.contains(scopeName)) {
+ finalScopes.add(scopeName);
}
}
}
- } else {
- Permission scopePermission = new Permission(null, null, scopes);
- permissions.put(scopePermission.toString(), scopePermission);
}
+ } else {
+ Permission scopePermission = new Permission(null, null, scopes);
+ permissions.put(scopePermission.toString(), scopePermission);
}
-
- return permissions.values().stream().collect(Collectors.toList());
}
}
diff --git a/services/src/main/java/org/keycloak/authorization/util/Tokens.java b/services/src/main/java/org/keycloak/authorization/util/Tokens.java
index 056f9fb..d693a4c 100644
--- a/services/src/main/java/org/keycloak/authorization/util/Tokens.java
+++ b/services/src/main/java/org/keycloak/authorization/util/Tokens.java
@@ -57,7 +57,7 @@ public class Tokens {
public static boolean verifySignature(KeycloakSession keycloakSession, RealmModel realm, String token) {
try {
JWSInput jws = new JWSInput(token);
- PublicKey publicKey = keycloakSession.keys().getPublicKey(realm, jws.getHeader().getKeyId());
+ PublicKey publicKey = keycloakSession.keys().getRsaPublicKey(realm, jws.getHeader().getKeyId());
return RSAProvider.verify(jws, publicKey);
} catch (Exception e) {
throw new ErrorResponseException("invalid_signature", "Unexpected error while validating signature.", Status.INTERNAL_SERVER_ERROR);
diff --git a/services/src/main/java/org/keycloak/broker/saml/SAMLEndpoint.java b/services/src/main/java/org/keycloak/broker/saml/SAMLEndpoint.java
index 95afb09..39a9d2e 100755
--- a/services/src/main/java/org/keycloak/broker/saml/SAMLEndpoint.java
+++ b/services/src/main/java/org/keycloak/broker/saml/SAMLEndpoint.java
@@ -301,7 +301,7 @@ public class SAMLEndpoint {
.relayState(relayState);
boolean postBinding = config.isPostBindingResponse();
if (config.isWantAuthnRequestsSigned()) {
- KeyManager.ActiveKey keys = session.keys().getActiveKey(realm);
+ KeyManager.ActiveRsaKey keys = session.keys().getActiveRsaKey(realm);
String keyName = config.getXmlSigKeyInfoKeyNameTransformer().getKeyName(keys.getKid(), keys.getCertificate());
binding.signWith(keyName, keys.getPrivateKey(), keys.getPublicKey(), keys.getCertificate())
.signatureAlgorithm(provider.getSignatureAlgorithm())
@@ -332,7 +332,7 @@ public class SAMLEndpoint {
protected Response handleLoginResponse(String samlResponse, SAMLDocumentHolder holder, ResponseType responseType, String relayState, String clientId) {
try {
- KeyManager.ActiveKey keys = session.keys().getActiveKey(realm);
+ KeyManager.ActiveRsaKey keys = session.keys().getActiveRsaKey(realm);
AssertionType assertion = AssertionUtil.getAssertion(responseType, keys.getPrivateKey());
SubjectType subject = assertion.getSubject();
SubjectType.STSubType subType = subject.getSubType();
diff --git a/services/src/main/java/org/keycloak/broker/saml/SAMLIdentityProvider.java b/services/src/main/java/org/keycloak/broker/saml/SAMLIdentityProvider.java
index d7ea042..1f8f793 100755
--- a/services/src/main/java/org/keycloak/broker/saml/SAMLIdentityProvider.java
+++ b/services/src/main/java/org/keycloak/broker/saml/SAMLIdentityProvider.java
@@ -30,6 +30,7 @@ import org.keycloak.dom.saml.v2.assertion.NameIDType;
import org.keycloak.dom.saml.v2.assertion.SubjectType;
import org.keycloak.dom.saml.v2.protocol.ResponseType;
import org.keycloak.events.EventBuilder;
+import org.keycloak.keys.RsaKeyMetadata;
import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.FederatedIdentityModel;
import org.keycloak.models.KeyManager;
@@ -103,7 +104,7 @@ public class SAMLIdentityProvider extends AbstractIdentityProvider<SAMLIdentityP
boolean postBinding = getConfig().isPostBindingAuthnRequest();
if (getConfig().isWantAuthnRequestsSigned()) {
- KeyManager.ActiveKey keys = session.keys().getActiveKey(realm);
+ KeyManager.ActiveRsaKey keys = session.keys().getActiveRsaKey(realm);
KeyPair keypair = new KeyPair(keys.getPublicKey(), keys.getPrivateKey());
@@ -205,7 +206,7 @@ public class SAMLIdentityProvider extends AbstractIdentityProvider<SAMLIdentityP
JaxrsSAML2BindingBuilder binding = new JaxrsSAML2BindingBuilder()
.relayState(userSession.getId());
if (getConfig().isWantAuthnRequestsSigned()) {
- KeyManager.ActiveKey keys = session.keys().getActiveKey(realm);
+ KeyManager.ActiveRsaKey keys = session.keys().getActiveRsaKey(realm);
String keyName = getConfig().getXmlSigKeyInfoKeyNameTransformer().getKeyName(keys.getKid(), keys.getCertificate());
binding.signWith(keyName, keys.getPrivateKey(), keys.getPublicKey(), keys.getCertificate())
.signatureAlgorithm(getSignatureAlgorithm())
@@ -236,18 +237,18 @@ public class SAMLIdentityProvider extends AbstractIdentityProvider<SAMLIdentityP
String nameIDPolicyFormat = getConfig().getNameIDPolicyFormat();
StringBuilder keysString = new StringBuilder();
- Set<KeyMetadata> keys = new TreeSet<>((o1, o2) -> o1.getStatus() == o2.getStatus() // Status can be only PASSIVE OR ACTIVE, push PASSIVE to end of list
+ Set<RsaKeyMetadata> keys = new TreeSet<>((o1, o2) -> o1.getStatus() == o2.getStatus() // Status can be only PASSIVE OR ACTIVE, push PASSIVE to end of list
? (int) (o2.getProviderPriority() - o1.getProviderPriority())
: (o1.getStatus() == KeyMetadata.Status.PASSIVE ? 1 : -1));
- keys.addAll(session.keys().getKeys(realm, false));
- for (KeyMetadata key : keys) {
+ keys.addAll(session.keys().getRsaKeys(realm, false));
+ for (RsaKeyMetadata key : keys) {
addKeyInfo(keysString, key, KeyTypes.SIGNING.value());
}
String descriptor = SPMetadataDescriptor.getSPDescriptor(authnBinding, endpoint, endpoint, wantAuthnRequestsSigned, entityId, nameIDPolicyFormat, keysString.toString());
return Response.ok(descriptor, MediaType.APPLICATION_XML_TYPE).build();
}
- private static void addKeyInfo(StringBuilder target, KeyMetadata key, String purpose) {
+ private static void addKeyInfo(StringBuilder target, RsaKeyMetadata key, String purpose) {
if (key == null) {
return;
}
diff --git a/services/src/main/java/org/keycloak/credential/PasswordCredentialProvider.java b/services/src/main/java/org/keycloak/credential/PasswordCredentialProvider.java
index 0a6fe8c..b5bc57a 100644
--- a/services/src/main/java/org/keycloak/credential/PasswordCredentialProvider.java
+++ b/services/src/main/java/org/keycloak/credential/PasswordCredentialProvider.java
@@ -31,9 +31,7 @@ import org.keycloak.policy.PasswordPolicyManagerProvider;
import org.keycloak.policy.PolicyError;
import java.util.Collections;
-import java.util.Comparator;
import java.util.HashSet;
-import java.util.LinkedList;
import java.util.List;
import java.util.Set;
@@ -57,7 +55,7 @@ public class PasswordCredentialProvider implements CredentialProvider, Credentia
}
public CredentialModel getPassword(RealmModel realm, UserModel user) {
- List<CredentialModel> passwords = null;
+ List<CredentialModel> passwords;
if (user instanceof CachedUserModel && !((CachedUserModel)user).isMarkedForEviction()) {
CachedUserModel cached = (CachedUserModel)user;
passwords = (List<CredentialModel>)cached.getCachedWith().get(PASSWORD_CACHE_KEY);
@@ -107,25 +105,20 @@ public class PasswordCredentialProvider implements CredentialProvider, Credentia
CredentialModel oldPassword = getPassword(realm, user);
if (oldPassword == null) return;
int expiredPasswordsPolicyValue = policy.getExpiredPasswords();
- if (expiredPasswordsPolicyValue > -1) {
+ if (expiredPasswordsPolicyValue > 1) {
List<CredentialModel> list = getCredentialStore().getStoredCredentialsByType(realm, user, CredentialModel.PASSWORD_HISTORY);
- List<CredentialModel> history = new LinkedList<>();
- history.addAll(list);
- if (history.size() + 1 >= expiredPasswordsPolicyValue) {
- Collections.sort(history, new Comparator<CredentialModel>() {
- @Override
- public int compare(CredentialModel o1, CredentialModel o2) {
- long o1Date = o1.getCreatedDate() == null ? 0 : o1.getCreatedDate().longValue();
- long o2Date = o2.getCreatedDate() == null ? 0 : o2.getCreatedDate().longValue();
- if (o1Date > o2Date) return 1;
- else if (o1Date < o2Date) return -1;
- else return 0;
- }
- });
- for (int i = 0; i < history.size() + 2 - expiredPasswordsPolicyValue; i++) {
- getCredentialStore().removeStoredCredential(realm, user, history.get(i).getId());
- }
-
+ // oldPassword will expire few lines below, and there is one active password,
+ // hence (expiredPasswordsPolicyValue - 2) passwords should be left in history
+ final int passwordsToLeave = expiredPasswordsPolicyValue - 2;
+ if (list.size() > passwordsToLeave) {
+ list.stream()
+ .sorted((o1, o2) -> { // sort by date descending
+ Long o1Date = o1.getCreatedDate() == null ? Long.MIN_VALUE : o1.getCreatedDate();
+ Long o2Date = o2.getCreatedDate() == null ? Long.MIN_VALUE : o2.getCreatedDate();
+ return (- o1Date.compareTo(o2Date));
+ })
+ .skip(passwordsToLeave)
+ .forEach(p -> getCredentialStore().removeStoredCredential(realm, user, p.getId()));
}
oldPassword.setType(CredentialModel.PASSWORD_HISTORY);
getCredentialStore().updateCredential(realm, user, oldPassword);
diff --git a/services/src/main/java/org/keycloak/email/DefaultEmailSenderProvider.java b/services/src/main/java/org/keycloak/email/DefaultEmailSenderProvider.java
index 235f123..311627a 100644
--- a/services/src/main/java/org/keycloak/email/DefaultEmailSenderProvider.java
+++ b/services/src/main/java/org/keycloak/email/DefaultEmailSenderProvider.java
@@ -57,7 +57,7 @@ public class DefaultEmailSenderProvider implements EmailSenderProvider {
public void send(RealmModel realm, UserModel user, String subject, String textBody, String htmlBody) throws EmailException {
Transport transport = null;
try {
- String address = user.getEmail();
+ String address = retrieveEmailAddress(user);
Map<String, String> config = realm.getSmtpConfig();
Properties props = new Properties();
@@ -136,6 +136,10 @@ public class DefaultEmailSenderProvider implements EmailSenderProvider {
}
}
}
+
+ protected String retrieveEmailAddress(UserModel user) {
+ return user.getEmail();
+ }
private void setupTruststore(Properties props) throws NoSuchAlgorithmException, KeyManagementException {
diff --git a/services/src/main/java/org/keycloak/exportimport/util/ExportUtils.java b/services/src/main/java/org/keycloak/exportimport/util/ExportUtils.java
index df7f8d3..29b8942 100755
--- a/services/src/main/java/org/keycloak/exportimport/util/ExportUtils.java
+++ b/services/src/main/java/org/keycloak/exportimport/util/ExportUtils.java
@@ -17,18 +17,27 @@
package org.keycloak.exportimport.util;
-import com.fasterxml.jackson.core.JsonEncoding;
-import com.fasterxml.jackson.core.JsonFactory;
-import com.fasterxml.jackson.core.JsonGenerator;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.databind.SerializationFeature;
+import static org.keycloak.models.utils.ModelToRepresentation.toRepresentation;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
import org.keycloak.authorization.AuthorizationProvider;
import org.keycloak.authorization.AuthorizationProviderFactory;
import org.keycloak.authorization.model.Policy;
+import org.keycloak.authorization.model.Resource;
import org.keycloak.authorization.model.ResourceServer;
+import org.keycloak.authorization.model.Scope;
import org.keycloak.authorization.store.PolicyStore;
-import org.keycloak.authorization.store.ResourceStore;
-import org.keycloak.authorization.store.ScopeStore;
import org.keycloak.authorization.store.StoreFactory;
import org.keycloak.common.Version;
import org.keycloak.common.util.Base64;
@@ -63,20 +72,11 @@ import org.keycloak.representations.idm.authorization.ResourceRepresentation;
import org.keycloak.representations.idm.authorization.ResourceServerRepresentation;
import org.keycloak.representations.idm.authorization.ScopeRepresentation;
import org.keycloak.util.JsonSerialization;
-
-import java.io.IOException;
-import java.io.OutputStream;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.stream.Collectors;
-
-import static org.keycloak.models.utils.ModelToRepresentation.toRepresentation;
+import com.fasterxml.jackson.core.JsonEncoding;
+import com.fasterxml.jackson.core.JsonFactory;
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationFeature;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
@@ -295,7 +295,6 @@ public class ExportUtils {
rep.getOwner().setId(null);
}
rep.setId(null);
- rep.setPolicies(null);
rep.getScopes().forEach(scopeRepresentation -> {
scopeRepresentation.setId(null);
scopeRepresentation.setIconUri(null);
@@ -338,10 +337,9 @@ public class ExportUtils {
RealmModel realm = authorizationProvider.getRealm();
StoreFactory storeFactory = authorizationProvider.getStoreFactory();
try {
- PolicyRepresentation rep = toRepresentation(policy, authorizationProvider);
+ PolicyRepresentation rep = toRepresentation(policy);
rep.setId(null);
- rep.setDependentPolicies(null);
Map<String, String> config = rep.getConfig();
@@ -363,20 +361,18 @@ public class ExportUtils {
config.put("users", JsonSerialization.writeValueAsString(userIds.stream().map(userId -> userManager.getUserById(userId, realm).getUsername()).collect(Collectors.toList())));
}
- String scopes = config.get("scopes");
+ Set<Scope> scopes = policy.getScopes();
- if (scopes != null && !scopes.isEmpty()) {
- ScopeStore scopeStore = storeFactory.getScopeStore();
- List<String> scopeIds = JsonSerialization.readValue(scopes, List.class);
- config.put("scopes", JsonSerialization.writeValueAsString(scopeIds.stream().map(scopeId -> scopeStore.findById(scopeId).getName()).collect(Collectors.toList())));
+ if (!scopes.isEmpty()) {
+ List<String> scopeNames = scopes.stream().map(Scope::getName).collect(Collectors.toList());
+ config.put("scopes", JsonSerialization.writeValueAsString(scopeNames));
}
- String policyResources = config.get("resources");
+ Set<Resource> policyResources = policy.getResources();
- if (policyResources != null && !policyResources.isEmpty()) {
- ResourceStore resourceStore = storeFactory.getResourceStore();
- List<String> resourceIds = JsonSerialization.readValue(policyResources, List.class);
- config.put("resources", JsonSerialization.writeValueAsString(resourceIds.stream().map(resourceId -> resourceStore.findById(resourceId).getName()).collect(Collectors.toList())));
+ if (!policyResources.isEmpty()) {
+ List<String> resourceNames = scopes.stream().map(Scope::getName).collect(Collectors.toList());
+ config.put("resources", JsonSerialization.writeValueAsString(resourceNames));
}
Set<Policy> associatedPolicies = policy.getAssociatedPolicies();
@@ -385,8 +381,6 @@ public class ExportUtils {
config.put("applyPolicies", JsonSerialization.writeValueAsString(associatedPolicies.stream().map(associated -> associated.getName()).collect(Collectors.toList())));
}
- rep.setAssociatedPolicies(null);
-
return rep;
} catch (Exception e) {
throw new RuntimeException("Error while exporting policy [" + policy.getName() + "].", e);
diff --git a/services/src/main/java/org/keycloak/forms/login/freemarker/model/RealmBean.java b/services/src/main/java/org/keycloak/forms/login/freemarker/model/RealmBean.java
index 4219f97..cd61568 100755
--- a/services/src/main/java/org/keycloak/forms/login/freemarker/model/RealmBean.java
+++ b/services/src/main/java/org/keycloak/forms/login/freemarker/model/RealmBean.java
@@ -64,6 +64,10 @@ public class RealmBean {
public boolean isRegistrationEmailAsUsername() {
return realm.isRegistrationEmailAsUsername();
}
+
+ public boolean isLoginWithEmailAllowed() {
+ return realm.isLoginWithEmailAllowed();
+ }
public boolean isResetPasswordAllowed() {
return realm.isResetPasswordAllowed();
diff --git a/services/src/main/java/org/keycloak/keys/AbstractHmacKeyProviderFactory.java b/services/src/main/java/org/keycloak/keys/AbstractHmacKeyProviderFactory.java
new file mode 100644
index 0000000..f8032b3
--- /dev/null
+++ b/services/src/main/java/org/keycloak/keys/AbstractHmacKeyProviderFactory.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.keys;
+
+import org.keycloak.component.ComponentModel;
+import org.keycloak.component.ComponentValidationException;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.provider.ConfigurationValidationHelper;
+import org.keycloak.provider.ProviderConfigurationBuilder;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public abstract class AbstractHmacKeyProviderFactory implements HmacKeyProviderFactory {
+
+ public final static ProviderConfigurationBuilder configurationBuilder() {
+ return ProviderConfigurationBuilder.create()
+ .property(Attributes.PRIORITY_PROPERTY)
+ .property(Attributes.ENABLED_PROPERTY)
+ .property(Attributes.ACTIVE_PROPERTY);
+ }
+
+ @Override
+ public void validateConfiguration(KeycloakSession session, RealmModel realm, ComponentModel model) throws ComponentValidationException {
+ ConfigurationValidationHelper.check(model)
+ .checkLong(Attributes.PRIORITY_PROPERTY, false)
+ .checkBoolean(Attributes.ENABLED_PROPERTY, false)
+ .checkBoolean(Attributes.ACTIVE_PROPERTY, false);
+ }
+
+}
diff --git a/services/src/main/java/org/keycloak/keys/AbstractRsaKeyProvider.java b/services/src/main/java/org/keycloak/keys/AbstractRsaKeyProvider.java
index f6bbaeb..0584a75 100644
--- a/services/src/main/java/org/keycloak/keys/AbstractRsaKeyProvider.java
+++ b/services/src/main/java/org/keycloak/keys/AbstractRsaKeyProvider.java
@@ -18,6 +18,7 @@
package org.keycloak.keys;
import org.keycloak.component.ComponentModel;
+import org.keycloak.jose.jws.AlgorithmType;
import org.keycloak.models.RealmModel;
import java.security.KeyPair;
@@ -30,7 +31,7 @@ import java.util.List;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
-public abstract class AbstractRsaKeyProvider implements KeyProvider {
+public abstract class AbstractRsaKeyProvider implements RsaKeyProvider {
private final boolean enabled;
@@ -77,11 +78,11 @@ public abstract class AbstractRsaKeyProvider implements KeyProvider {
}
@Override
- public final List<KeyMetadata> getKeyMetadata() {
+ public final List<RsaKeyMetadata> getKeyMetadata() {
String kid = keys.getKid();
PublicKey publicKey = keys.getKeyPair().getPublic();
if (kid != null && publicKey != null) {
- KeyMetadata k = new KeyMetadata();
+ RsaKeyMetadata k = new RsaKeyMetadata();
k.setProviderId(model.getId());
k.setProviderPriority(model.get(Attributes.PRIORITY_KEY, 0l));
k.setKid(kid);
@@ -92,7 +93,6 @@ public abstract class AbstractRsaKeyProvider implements KeyProvider {
} else {
k.setStatus(KeyMetadata.Status.DISABLED);
}
- k.setType(KeyMetadata.Type.RSA);
k.setPublicKey(publicKey);
k.setCertificate(keys.getCertificate());
return Collections.singletonList(k);
diff --git a/services/src/main/java/org/keycloak/keys/AbstractRsaKeyProviderFactory.java b/services/src/main/java/org/keycloak/keys/AbstractRsaKeyProviderFactory.java
index 1c2af4f..f20dc20 100644
--- a/services/src/main/java/org/keycloak/keys/AbstractRsaKeyProviderFactory.java
+++ b/services/src/main/java/org/keycloak/keys/AbstractRsaKeyProviderFactory.java
@@ -27,7 +27,7 @@ import org.keycloak.provider.ProviderConfigurationBuilder;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
-public abstract class AbstractRsaKeyProviderFactory implements KeyProviderFactory {
+public abstract class AbstractRsaKeyProviderFactory implements RsaKeyProviderFactory {
public final static ProviderConfigurationBuilder configurationBuilder() {
return ProviderConfigurationBuilder.create()
diff --git a/services/src/main/java/org/keycloak/keys/Attributes.java b/services/src/main/java/org/keycloak/keys/Attributes.java
index bc66dd3..edde62c 100644
--- a/services/src/main/java/org/keycloak/keys/Attributes.java
+++ b/services/src/main/java/org/keycloak/keys/Attributes.java
@@ -44,6 +44,13 @@ public interface Attributes {
ProviderConfigProperty CERTIFICATE_PROPERTY = new ProviderConfigProperty(CERTIFICATE_KEY, "X509 Certificate", "X509 Certificate encoded in PEM format", FILE_TYPE, null);
String KEY_SIZE_KEY = "keySize";
- ProviderConfigProperty KEY_SIZE_PROPERTY = new ProviderConfigProperty(KEY_SIZE_KEY, "Keysize", "Size for the generated keys (1024, 2048 or 4096)", LIST_TYPE, "2048", "1024", "2048", "4096");
+ ProviderConfigProperty KEY_SIZE_PROPERTY = new ProviderConfigProperty(KEY_SIZE_KEY, "Key size", "Size for the generated keys", LIST_TYPE, "2048", "1024", "2048", "4096");
+
+ String KID_KEY = "kid";
+
+ String SECRET_KEY = "secret";
+
+ String SECRET_SIZE_KEY = "secretSize";
+ ProviderConfigProperty SECRET_SIZE_PROPERTY = new ProviderConfigProperty(SECRET_SIZE_KEY, "Secret size", "Size in bytes for the generated secret", LIST_TYPE, "32", "32", "64", "128", "256", "512");
}
diff --git a/services/src/main/java/org/keycloak/keys/DefaultKeyManager.java b/services/src/main/java/org/keycloak/keys/DefaultKeyManager.java
index df18005..4f90f2c 100644
--- a/services/src/main/java/org/keycloak/keys/DefaultKeyManager.java
+++ b/services/src/main/java/org/keycloak/keys/DefaultKeyManager.java
@@ -19,11 +19,13 @@ package org.keycloak.keys;
import org.jboss.logging.Logger;
import org.keycloak.component.ComponentModel;
+import org.keycloak.jose.jws.AlgorithmType;
import org.keycloak.models.KeyManager;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.provider.ProviderFactory;
+import javax.crypto.SecretKey;
import java.security.PublicKey;
import java.security.cert.Certificate;
import java.util.Comparator;
@@ -47,33 +49,56 @@ public class DefaultKeyManager implements KeyManager {
}
@Override
- public ActiveKey getActiveKey(RealmModel realm) {
+ public ActiveRsaKey getActiveRsaKey(RealmModel realm) {
for (KeyProvider p : getProviders(realm)) {
- if (p.getKid() != null && p.getPrivateKey() != null) {
- if (logger.isTraceEnabled()) {
- logger.tracev("Active key realm={0} kid={1}", realm.getName(), p.getKid());
+ if (p.getType().equals(AlgorithmType.RSA)) {
+ RsaKeyProvider r = (RsaKeyProvider) p;
+ if (r.getKid() != null && r.getPrivateKey() != null) {
+ if (logger.isTraceEnabled()) {
+ logger.tracev("Active key realm={0} kid={1}", realm.getName(), p.getKid());
+ }
+ String kid = p.getKid();
+ return new ActiveRsaKey(kid, r.getPrivateKey(), r.getPublicKey(kid), r.getCertificate(kid));
+ }
+ }
+ }
+ throw new RuntimeException("Failed to get RSA keys");
+ }
+
+ @Override
+ public ActiveHmacKey getActiveHmacKey(RealmModel realm) {
+ for (KeyProvider p : getProviders(realm)) {
+ if (p.getType().equals(AlgorithmType.HMAC)) {
+ HmacKeyProvider h = (HmacKeyProvider) p;
+ if (h.getKid() != null && h.getSecretKey() != null) {
+ if (logger.isTraceEnabled()) {
+ logger.tracev("Active secret realm={0} kid={1}", realm.getName(), p.getKid());
+ }
+ String kid = p.getKid();
+ return new ActiveHmacKey(kid, h.getSecretKey());
}
- String kid = p.getKid();
- return new ActiveKey(kid, p.getPrivateKey(), p.getPublicKey(kid), p.getCertificate(kid));
}
}
throw new RuntimeException("Failed to get keys");
}
@Override
- public PublicKey getPublicKey(RealmModel realm, String kid) {
+ public PublicKey getRsaPublicKey(RealmModel realm, String kid) {
if (kid == null) {
logger.warnv("KID is null, can't find public key", realm.getName(), kid);
return null;
}
for (KeyProvider p : getProviders(realm)) {
- PublicKey publicKey = p.getPublicKey(kid);
- if (publicKey != null) {
- if (logger.isTraceEnabled()) {
- logger.tracev("Found public key realm={0} kid={1}", realm.getName(), kid);
+ if (p.getType().equals(AlgorithmType.RSA)) {
+ RsaKeyProvider r = (RsaKeyProvider) p;
+ PublicKey publicKey = r.getPublicKey(kid);
+ if (publicKey != null) {
+ if (logger.isTraceEnabled()) {
+ logger.tracev("Found public key realm={0} kid={1}", realm.getName(), kid);
+ }
+ return publicKey;
}
- return publicKey;
}
}
if (logger.isTraceEnabled()) {
@@ -83,19 +108,22 @@ public class DefaultKeyManager implements KeyManager {
}
@Override
- public Certificate getCertificate(RealmModel realm, String kid) {
+ public Certificate getRsaCertificate(RealmModel realm, String kid) {
if (kid == null) {
logger.warnv("KID is null, can't find public key", realm.getName(), kid);
return null;
}
for (KeyProvider p : getProviders(realm)) {
- Certificate certificate = p.getCertificate(kid);
- if (certificate != null) {
- if (logger.isTraceEnabled()) {
- logger.tracev("Found certificate realm={0} kid={1}", realm.getName(), kid);
+ if (p.getType().equals(AlgorithmType.RSA)) {
+ RsaKeyProvider r = (RsaKeyProvider) p;
+ Certificate certificate = r.getCertificate(kid);
+ if (certificate != null) {
+ if (logger.isTraceEnabled()) {
+ logger.tracev("Found certificate realm={0} kid={1}", realm.getName(), kid);
+ }
+ return certificate;
}
- return certificate;
}
}
if (logger.isTraceEnabled()) {
@@ -105,20 +133,63 @@ public class DefaultKeyManager implements KeyManager {
}
@Override
- public List<KeyMetadata> getKeys(RealmModel realm, boolean includeDisabled) {
- List<KeyMetadata> keys = new LinkedList<>();
+ public SecretKey getHmacSecretKey(RealmModel realm, String kid) {
+ if (kid == null) {
+ logger.warnv("KID is null, can't find public key", realm.getName(), kid);
+ return null;
+ }
+
for (KeyProvider p : getProviders(realm)) {
- if (includeDisabled) {
- keys.addAll(p.getKeyMetadata());
- } else {
- p.getKeyMetadata().stream().filter(k -> k.getStatus() != KeyMetadata.Status.DISABLED).forEach(k -> keys.add(k));
+ if (p.getType().equals(AlgorithmType.HMAC)) {
+ HmacKeyProvider h = (HmacKeyProvider) p;
+ SecretKey s = h.getSecretKey(kid);
+ if (s != null) {
+ if (logger.isTraceEnabled()) {
+ logger.tracev("Found secret key realm={0} kid={1}", realm.getName(), kid);
+ }
+ return s;
+ }
+ }
+ }
+ if (logger.isTraceEnabled()) {
+ logger.tracev("Failed to find secret key realm={0} kid={1}", realm.getName(), kid);
+ }
+ return null;
+ }
+
+ @Override
+ public List<RsaKeyMetadata> getRsaKeys(RealmModel realm, boolean includeDisabled) {
+ List<RsaKeyMetadata> keys = new LinkedList<>();
+ for (KeyProvider p : getProviders(realm)) {
+ if (p instanceof RsaKeyProvider) {
+ if (includeDisabled) {
+ keys.addAll(p.getKeyMetadata());
+ } else {
+ List<RsaKeyMetadata> metadata = p.getKeyMetadata();
+ metadata.stream().filter(k -> k.getStatus() != KeyMetadata.Status.DISABLED).forEach(k -> keys.add(k));
+ }
+ }
+ }
+ return keys;
+ }
+
+ @Override
+ public List<HmacKeyMetadata> getHmacKeys(RealmModel realm, boolean includeDisabled) {
+ List<HmacKeyMetadata> keys = new LinkedList<>();
+ for (KeyProvider p : getProviders(realm)) {
+ if (p instanceof HmacKeyProvider) {
+ if (includeDisabled) {
+ keys.addAll(p.getKeyMetadata());
+ } else {
+ List<HmacKeyMetadata> metadata = p.getKeyMetadata();
+ metadata.stream().filter(k -> k.getStatus() != KeyMetadata.Status.DISABLED).forEach(k -> keys.add(k));
+ }
}
}
return keys;
}
private List<KeyProvider> getProviders(RealmModel realm) {
- boolean active = false;
List<KeyProvider> providers = providersMap.get(realm.getId());
if (providers == null) {
providers = new LinkedList<>();
@@ -126,6 +197,9 @@ public class DefaultKeyManager implements KeyManager {
List<ComponentModel> components = new LinkedList<>(realm.getComponents(realm.getId(), KeyProvider.class.getName()));
components.sort(new ProviderComparator());
+ boolean activeRsa = false;
+ boolean activeHmac = false;
+
for (ComponentModel c : components) {
try {
ProviderFactory<KeyProvider> f = session.getKeycloakSessionFactory().getProviderFactory(KeyProvider.class, c.getProviderId());
@@ -133,18 +207,30 @@ public class DefaultKeyManager implements KeyManager {
KeyProvider provider = factory.create(session, c);
session.enlistForClose(provider);
providers.add(provider);
- if (!active && provider.getKid() != null && provider.getPrivateKey() != null) {
- active = true;
+ if (provider.getType().equals(AlgorithmType.RSA)) {
+ RsaKeyProvider r = (RsaKeyProvider) provider;
+ if (r.getKid() != null && r.getPrivateKey() != null) {
+ activeRsa = true;
+ }
+ } else if (provider.getType().equals(AlgorithmType.HMAC)) {
+ HmacKeyProvider r = (HmacKeyProvider) provider;
+ if (r.getKid() != null && r.getSecretKey() != null) {
+ activeHmac = true;
+ }
}
} catch (Throwable t) {
logger.errorv(t, "Failed to load provider {0}", c.getId());
}
}
- if (!active) {
+ if (!activeRsa) {
providers.add(new FailsafeRsaKeyProvider());
}
+ if (!activeHmac) {
+ providers.add(new FailsafeHmacKeyProvider());
+ }
+
providersMap.put(realm.getId(), providers);
}
return providers;
diff --git a/services/src/main/java/org/keycloak/keys/FailsafeHmacKeyProvider.java b/services/src/main/java/org/keycloak/keys/FailsafeHmacKeyProvider.java
new file mode 100644
index 0000000..37e837b
--- /dev/null
+++ b/services/src/main/java/org/keycloak/keys/FailsafeHmacKeyProvider.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.keys;
+
+import org.jboss.logging.Logger;
+import org.keycloak.common.util.KeyUtils;
+import org.keycloak.common.util.Time;
+import org.keycloak.models.utils.KeycloakModelUtils;
+
+import javax.crypto.SecretKey;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class FailsafeHmacKeyProvider implements HmacKeyProvider {
+
+ private static final Logger logger = Logger.getLogger(FailsafeHmacKeyProvider.class);
+
+ private static String KID;
+
+ private static SecretKey KEY;
+
+ private static long EXPIRES;
+
+ private SecretKey key;
+
+ private String kid;
+
+ public FailsafeHmacKeyProvider() {
+ logger.errorv("No active keys found, using failsafe provider, please login to admin console to add keys. Clustering is not supported.");
+
+ synchronized (FailsafeHmacKeyProvider.class) {
+ if (EXPIRES < Time.currentTime()) {
+ KEY = KeyUtils.loadSecretKey(KeycloakModelUtils.generateSecret(32));
+ KID = KeycloakModelUtils.generateId();
+ EXPIRES = Time.currentTime() + 60 * 10;
+
+ if (EXPIRES > 0) {
+ logger.warnv("Keys expired, re-generated kid={0}", KID);
+ }
+ }
+
+ kid = KID;
+ key = KEY;
+ }
+ }
+
+ @Override
+ public String getKid() {
+ return kid;
+ }
+
+ @Override
+ public SecretKey getSecretKey() {
+ return key;
+ }
+
+ @Override
+ public SecretKey getSecretKey(String kid) {
+ return kid.equals(this.kid) ? key : null;
+ }
+
+ @Override
+ public List<HmacKeyMetadata> getKeyMetadata() {
+ return Collections.emptyList();
+ }
+
+ @Override
+ public void close() {
+ }
+
+}
diff --git a/services/src/main/java/org/keycloak/keys/FailsafeRsaKeyProvider.java b/services/src/main/java/org/keycloak/keys/FailsafeRsaKeyProvider.java
index 00586a8..8515f29 100644
--- a/services/src/main/java/org/keycloak/keys/FailsafeRsaKeyProvider.java
+++ b/services/src/main/java/org/keycloak/keys/FailsafeRsaKeyProvider.java
@@ -31,7 +31,7 @@ import java.util.List;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
-public class FailsafeRsaKeyProvider implements KeyProvider {
+public class FailsafeRsaKeyProvider implements RsaKeyProvider {
private static final Logger logger = Logger.getLogger(FailsafeRsaKeyProvider.class);
@@ -85,7 +85,7 @@ public class FailsafeRsaKeyProvider implements KeyProvider {
}
@Override
- public List<KeyMetadata> getKeyMetadata() {
+ public List<RsaKeyMetadata> getKeyMetadata() {
return Collections.emptyList();
}
diff --git a/services/src/main/java/org/keycloak/keys/GeneratedHmacKeyProvider.java b/services/src/main/java/org/keycloak/keys/GeneratedHmacKeyProvider.java
new file mode 100644
index 0000000..cbd9035
--- /dev/null
+++ b/services/src/main/java/org/keycloak/keys/GeneratedHmacKeyProvider.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.keys;
+
+import org.keycloak.common.util.KeyUtils;
+import org.keycloak.component.ComponentModel;
+import org.keycloak.jose.jws.AlgorithmType;
+
+import javax.crypto.SecretKey;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class GeneratedHmacKeyProvider implements HmacKeyProvider {
+
+ private final boolean enabled;
+
+ private final boolean active;
+
+ private final ComponentModel model;
+ private final String kid;
+ private final SecretKey secretKey;
+
+ public GeneratedHmacKeyProvider(ComponentModel model) {
+ this.enabled = model.get(Attributes.ENABLED_KEY, true);
+ this.active = model.get(Attributes.ACTIVE_KEY, true);
+ this.kid = model.get(Attributes.KID_KEY);
+ this.model = model;
+
+ if (model.hasNote(SecretKey.class.getName())) {
+ secretKey = model.getNote(SecretKey.class.getName());
+ } else {
+ secretKey = KeyUtils.loadSecretKey(model.get(Attributes.SECRET_KEY));
+ model.setNote(SecretKey.class.getName(), secretKey);
+ }
+ }
+
+ @Override
+ public SecretKey getSecretKey() {
+ return isActive() ? secretKey : null;
+ }
+
+ @Override
+ public SecretKey getSecretKey(String kid) {
+ return isEnabled() && kid.equals(this.kid) ? secretKey : null;
+ }
+
+ @Override
+ public String getKid() {
+ return isActive() ? kid : null;
+ }
+
+ @Override
+ public List<HmacKeyMetadata> getKeyMetadata() {
+ if (kid != null && secretKey != null) {
+ HmacKeyMetadata k = new HmacKeyMetadata();
+ k.setProviderId(model.getId());
+ k.setProviderPriority(model.get(Attributes.PRIORITY_KEY, 0l));
+ k.setKid(kid);
+ if (isActive()) {
+ k.setStatus(KeyMetadata.Status.ACTIVE);
+ } else if (isEnabled()) {
+ k.setStatus(KeyMetadata.Status.PASSIVE);
+ } else {
+ k.setStatus(KeyMetadata.Status.DISABLED);
+ }
+ return Collections.singletonList(k);
+ } else {
+ return Collections.emptyList();
+ }
+ }
+
+ @Override
+ public void close() {
+ }
+
+ private boolean isEnabled() {
+ return secretKey != null && enabled;
+ }
+
+ private boolean isActive() {
+ return isEnabled() && active;
+ }
+
+}
diff --git a/services/src/main/java/org/keycloak/keys/GeneratedHmacKeyProviderFactory.java b/services/src/main/java/org/keycloak/keys/GeneratedHmacKeyProviderFactory.java
new file mode 100644
index 0000000..7207eab
--- /dev/null
+++ b/services/src/main/java/org/keycloak/keys/GeneratedHmacKeyProviderFactory.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.keys;
+
+import org.jboss.logging.Logger;
+import org.keycloak.Config;
+import org.keycloak.common.util.Base64Url;
+import org.keycloak.component.ComponentModel;
+import org.keycloak.component.ComponentValidationException;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakSessionFactory;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.utils.KeycloakModelUtils;
+import org.keycloak.provider.ConfigurationValidationHelper;
+import org.keycloak.provider.ProviderConfigProperty;
+
+import java.util.List;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class GeneratedHmacKeyProviderFactory extends AbstractHmacKeyProviderFactory {
+
+ private static final Logger logger = Logger.getLogger(GeneratedHmacKeyProviderFactory.class);
+
+ public static final String ID = "hmac-generated";
+
+ private static final String HELP_TEXT = "Generates HMAC secret key";
+
+ private static final List<ProviderConfigProperty> CONFIG_PROPERTIES = AbstractHmacKeyProviderFactory.configurationBuilder()
+ .property(Attributes.SECRET_SIZE_PROPERTY)
+ .build();
+
+ @Override
+ public KeyProvider create(KeycloakSession session, ComponentModel model) {
+ return new GeneratedHmacKeyProvider(model);
+ }
+
+ @Override
+ public String getHelpText() {
+ return HELP_TEXT;
+ }
+
+ @Override
+ public List<ProviderConfigProperty> getConfigProperties() {
+ return CONFIG_PROPERTIES;
+ }
+
+ @Override
+ public void validateConfiguration(KeycloakSession session, RealmModel realm, ComponentModel model) throws ComponentValidationException {
+ ConfigurationValidationHelper.check(model).checkList(Attributes.SECRET_SIZE_PROPERTY, false);
+
+ int size = model.get(Attributes.SECRET_SIZE_KEY, 32);
+
+ if (!(model.contains(Attributes.SECRET_KEY))) {
+ generateSecret(model, size);
+ logger.debugv("Generated secret for {0}", realm.getName());
+ } else {
+ int currentSize = Base64Url.decode(model.get(Attributes.SECRET_KEY)).length;
+ if (currentSize != size) {
+ generateSecret(model, size);
+ logger.debugv("Secret size changed, generating new secret for {0}", realm.getName());
+ }
+ }
+ }
+
+ private void generateSecret(ComponentModel model, int size) {
+ try {
+ String secret = KeycloakModelUtils.generateSecret(size);
+ model.put(Attributes.SECRET_KEY, secret);
+
+ String kid = KeycloakModelUtils.generateId();
+ model.put(Attributes.KID_KEY, kid);
+ } catch (Throwable t) {
+ throw new ComponentValidationException("Failed to generate secret", t);
+ }
+ }
+
+ @Override
+ public void init(Config.Scope config) {
+ }
+
+ @Override
+ public void postInit(KeycloakSessionFactory factory) {
+ }
+
+ @Override
+ public void close() {
+ }
+
+ @Override
+ public String getId() {
+ return ID;
+ }
+
+}
diff --git a/services/src/main/java/org/keycloak/keys/GeneratedRsaKeyProviderFactory.java b/services/src/main/java/org/keycloak/keys/GeneratedRsaKeyProviderFactory.java
index cba36d0..a0a5e87 100644
--- a/services/src/main/java/org/keycloak/keys/GeneratedRsaKeyProviderFactory.java
+++ b/services/src/main/java/org/keycloak/keys/GeneratedRsaKeyProviderFactory.java
@@ -53,26 +53,16 @@ public class GeneratedRsaKeyProviderFactory extends AbstractRsaKeyProviderFactor
@Override
public KeyProvider create(KeycloakSession session, ComponentModel model) {
- return new RsaKeyProvider(session.getContext().getRealm(), model);
+ return new ImportedRsaKeyProvider(session.getContext().getRealm(), model);
}
@Override
public void validateConfiguration(KeycloakSession session, RealmModel realm, ComponentModel model) throws ComponentValidationException {
super.validateConfiguration(session, realm, model);
- ConfigurationValidationHelper.check(model)
- .checkInt(Attributes.KEY_SIZE_PROPERTY, false);
+ ConfigurationValidationHelper.check(model).checkList(Attributes.KEY_SIZE_PROPERTY, false);
- int size;
- if (!model.contains(Attributes.KEY_SIZE_KEY)) {
- size = 2048;
- model.put(Attributes.KEY_SIZE_KEY, size);
- } else {
- size = model.get(Attributes.KEY_SIZE_KEY, 2048);
- if (size != 1024 && size != 2048 && size != 4096) {
- throw new ComponentValidationException("Keysize should be 1024, 2048 or 4096");
- }
- }
+ int size = model.get(Attributes.KEY_SIZE_KEY, 2048);
if (!(model.contains(Attributes.PRIVATE_KEY_KEY) && model.contains(Attributes.CERTIFICATE_KEY))) {
generateKeys(realm, model, size);
diff --git a/services/src/main/java/org/keycloak/partialimport/AbstractPartialImport.java b/services/src/main/java/org/keycloak/partialimport/AbstractPartialImport.java
index ec42600..a811c7e 100644
--- a/services/src/main/java/org/keycloak/partialimport/AbstractPartialImport.java
+++ b/services/src/main/java/org/keycloak/partialimport/AbstractPartialImport.java
@@ -42,7 +42,7 @@ public abstract class AbstractPartialImport<T> implements PartialImport<T> {
public abstract String getName(T resourceRep);
public abstract String getModelId(RealmModel realm, KeycloakSession session, T resourceRep);
public abstract boolean exists(RealmModel realm, KeycloakSession session, T resourceRep);
- public abstract String existsMessage(T resourceRep);
+ public abstract String existsMessage(RealmModel realm, T resourceRep);
public abstract ResourceType getResourceType();
public abstract void remove(RealmModel realm, KeycloakSession session, T resourceRep);
public abstract void create(RealmModel realm, KeycloakSession session, T resourceRep);
@@ -59,7 +59,7 @@ public abstract class AbstractPartialImport<T> implements PartialImport<T> {
switch (partialImportRep.getPolicy()) {
case SKIP: toSkip.add(resourceRep); break;
case OVERWRITE: toOverwrite.add(resourceRep); break;
- default: throw existsError(existsMessage(resourceRep));
+ default: throw existsError(existsMessage(realm, resourceRep));
}
}
}
diff --git a/services/src/main/java/org/keycloak/partialimport/ClientsPartialImport.java b/services/src/main/java/org/keycloak/partialimport/ClientsPartialImport.java
index b7e46df..308d634 100755
--- a/services/src/main/java/org/keycloak/partialimport/ClientsPartialImport.java
+++ b/services/src/main/java/org/keycloak/partialimport/ClientsPartialImport.java
@@ -56,7 +56,7 @@ public class ClientsPartialImport extends AbstractPartialImport<ClientRepresenta
}
@Override
- public String existsMessage(ClientRepresentation clientRep) {
+ public String existsMessage(RealmModel realm, ClientRepresentation clientRep) {
return "Client id '" + getName(clientRep) + "' already exists";
}
diff --git a/services/src/main/java/org/keycloak/partialimport/GroupsPartialImport.java b/services/src/main/java/org/keycloak/partialimport/GroupsPartialImport.java
index cdb57ad..f0f405d 100644
--- a/services/src/main/java/org/keycloak/partialimport/GroupsPartialImport.java
+++ b/services/src/main/java/org/keycloak/partialimport/GroupsPartialImport.java
@@ -59,7 +59,7 @@ public class GroupsPartialImport extends AbstractPartialImport<GroupRepresentati
}
@Override
- public String existsMessage(GroupRepresentation groupRep) {
+ public String existsMessage(RealmModel realm, GroupRepresentation groupRep) {
return "Group '" + groupRep.getPath() + "' already exists";
}
diff --git a/services/src/main/java/org/keycloak/partialimport/IdentityProvidersPartialImport.java b/services/src/main/java/org/keycloak/partialimport/IdentityProvidersPartialImport.java
index cdcb89c..b68d116 100644
--- a/services/src/main/java/org/keycloak/partialimport/IdentityProvidersPartialImport.java
+++ b/services/src/main/java/org/keycloak/partialimport/IdentityProvidersPartialImport.java
@@ -55,7 +55,7 @@ public class IdentityProvidersPartialImport extends AbstractPartialImport<Identi
}
@Override
- public String existsMessage(IdentityProviderRepresentation idpRep) {
+ public String existsMessage(RealmModel realm, IdentityProviderRepresentation idpRep) {
return "Identity Provider '" + getName(idpRep) + "' already exists.";
}
diff --git a/services/src/main/java/org/keycloak/partialimport/PartialImportManager.java b/services/src/main/java/org/keycloak/partialimport/PartialImportManager.java
index 5ec3eec..107ea43 100644
--- a/services/src/main/java/org/keycloak/partialimport/PartialImportManager.java
+++ b/services/src/main/java/org/keycloak/partialimport/PartialImportManager.java
@@ -19,8 +19,10 @@ package org.keycloak.partialimport;
import org.keycloak.events.admin.OperationType;
import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.ModelDuplicateException;
import org.keycloak.models.RealmModel;
import org.keycloak.representations.idm.PartialImportRepresentation;
+import org.keycloak.services.ErrorResponse;
import org.keycloak.services.resources.admin.AdminEventBuilder;
import javax.ws.rs.core.Response;
@@ -86,7 +88,11 @@ public class PartialImportManager {
}
if (session.getTransactionManager().isActive()) {
- session.getTransactionManager().commit();
+ try {
+ session.getTransactionManager().commit();
+ } catch (ModelDuplicateException e) {
+ return ErrorResponse.exists(e.getLocalizedMessage());
+ }
}
return Response.ok(results).build();
diff --git a/services/src/main/java/org/keycloak/partialimport/RealmRolesPartialImport.java b/services/src/main/java/org/keycloak/partialimport/RealmRolesPartialImport.java
index 9c53709..c820d12 100644
--- a/services/src/main/java/org/keycloak/partialimport/RealmRolesPartialImport.java
+++ b/services/src/main/java/org/keycloak/partialimport/RealmRolesPartialImport.java
@@ -73,7 +73,7 @@ public class RealmRolesPartialImport extends AbstractPartialImport<RoleRepresent
}
@Override
- public String existsMessage(RoleRepresentation roleRep) {
+ public String existsMessage(RealmModel realm, RoleRepresentation roleRep) {
return "Realm role '" + getName(roleRep) + "' already exists.";
}
diff --git a/services/src/main/java/org/keycloak/partialimport/UsersPartialImport.java b/services/src/main/java/org/keycloak/partialimport/UsersPartialImport.java
index cce0fec..c03f0be 100755
--- a/services/src/main/java/org/keycloak/partialimport/UsersPartialImport.java
+++ b/services/src/main/java/org/keycloak/partialimport/UsersPartialImport.java
@@ -60,10 +60,12 @@ public class UsersPartialImport extends AbstractPartialImport<UserRepresentation
String userName = user.getUsername();
if (userName != null) {
return session.users().getUserByUsername(userName, realm).getId();
- } else {
+ } else if (!realm.isDuplicateEmailsAllowed()) {
String email = user.getEmail();
return session.users().getUserByEmail(email, realm).getId();
}
+
+ return null;
}
@Override
@@ -76,13 +78,13 @@ public class UsersPartialImport extends AbstractPartialImport<UserRepresentation
}
private boolean userEmailExists(RealmModel realm, KeycloakSession session, UserRepresentation user) {
- return (user.getEmail() != null) &&
+ return (user.getEmail() != null) && !realm.isDuplicateEmailsAllowed() &&
(session.users().getUserByEmail(user.getEmail(), realm) != null);
}
@Override
- public String existsMessage(UserRepresentation user) {
- if (user.getEmail() == null) {
+ public String existsMessage(RealmModel realm, UserRepresentation user) {
+ if (user.getEmail() == null || !realm.isDuplicateEmailsAllowed()) {
return "User with user name " + getName(user) + " already exists.";
}
@@ -97,12 +99,13 @@ public class UsersPartialImport extends AbstractPartialImport<UserRepresentation
@Override
public void remove(RealmModel realm, KeycloakSession session, UserRepresentation user) {
UserModel userModel = session.users().getUserByUsername(user.getUsername(), realm);
- if (userModel == null) {
+ if (userModel == null && !realm.isDuplicateEmailsAllowed()) {
userModel = session.users().getUserByEmail(user.getEmail(), realm);
}
-
- boolean success = new UserManager(session).removeUser(realm, userModel);
- if (!success) throw new RuntimeException("Unable to overwrite user " + getName(user));
+ if (userModel != null) {
+ boolean success = new UserManager(session).removeUser(realm, userModel);
+ if (!success) throw new RuntimeException("Unable to overwrite user " + getName(user));
+ }
}
@Override
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/AccessTokenIntrospectionProvider.java b/services/src/main/java/org/keycloak/protocol/oidc/AccessTokenIntrospectionProvider.java
index 8dbb01b..e89db0f 100644
--- a/services/src/main/java/org/keycloak/protocol/oidc/AccessTokenIntrospectionProvider.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/AccessTokenIntrospectionProvider.java
@@ -56,7 +56,7 @@ public class AccessTokenIntrospectionProvider implements TokenIntrospectionProvi
RSATokenVerifier verifier = RSATokenVerifier.create(token)
.realmUrl(Urls.realmIssuer(session.getContext().getUri().getBaseUri(), realm.getName()));
- PublicKey publicKey = session.keys().getPublicKey(realm, verifier.getHeader().getKeyId());
+ PublicKey publicKey = session.keys().getRsaPublicKey(realm, verifier.getHeader().getKeyId());
if (publicKey == null) {
valid = false;
} else {
@@ -96,7 +96,7 @@ public class AccessTokenIntrospectionProvider implements TokenIntrospectionProvi
RSATokenVerifier verifier = RSATokenVerifier.create(token)
.realmUrl(Urls.realmIssuer(session.getContext().getUri().getBaseUri(), realm.getName()));
- PublicKey publicKey = session.keys().getPublicKey(realm, verifier.getHeader().getKeyId());
+ PublicKey publicKey = session.keys().getRsaPublicKey(realm, verifier.getHeader().getKeyId());
verifier.publicKey(publicKey);
return verifier.verify().getToken();
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LogoutEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LogoutEndpoint.java
index 0c17664..94aedc4 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LogoutEndpoint.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LogoutEndpoint.java
@@ -196,7 +196,7 @@ public class LogoutEndpoint {
event.error(Errors.INVALID_TOKEN);
throw new ErrorResponseException(e.getError(), e.getDescription(), Response.Status.BAD_REQUEST);
}
- return Cors.add(request, Response.noContent()).auth().allowedOrigins(client).allowedMethods("POST").exposedHeaders(Cors.ACCESS_CONTROL_ALLOW_METHODS).build();
+ return Cors.add(request, Response.noContent()).auth().allowedOrigins(uriInfo, client).allowedMethods("POST").exposedHeaders(Cors.ACCESS_CONTROL_ALLOW_METHODS).build();
}
private void logout(UserSessionModel userSession) {
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java
index 8d52282..03be086 100644
--- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java
@@ -278,7 +278,7 @@ public class TokenEndpoint {
event.success();
- return Cors.add(request, Response.ok(res).type(MediaType.APPLICATION_JSON_TYPE)).auth().allowedOrigins(client).allowedMethods("POST").exposedHeaders(Cors.ACCESS_CONTROL_ALLOW_METHODS).build();
+ return Cors.add(request, Response.ok(res).type(MediaType.APPLICATION_JSON_TYPE)).auth().allowedOrigins(uriInfo, client).allowedMethods("POST").exposedHeaders(Cors.ACCESS_CONTROL_ALLOW_METHODS).build();
}
public Response buildRefreshToken() {
@@ -305,7 +305,7 @@ public class TokenEndpoint {
event.success();
- return Cors.add(request, Response.ok(res, MediaType.APPLICATION_JSON_TYPE)).auth().allowedOrigins(client).allowedMethods("POST").exposedHeaders(Cors.ACCESS_CONTROL_ALLOW_METHODS).build();
+ return Cors.add(request, Response.ok(res, MediaType.APPLICATION_JSON_TYPE)).auth().allowedOrigins(uriInfo, client).allowedMethods("POST").exposedHeaders(Cors.ACCESS_CONTROL_ALLOW_METHODS).build();
}
private void updateClientSession(ClientSessionModel clientSession) {
@@ -407,7 +407,7 @@ public class TokenEndpoint {
event.success();
- return Cors.add(request, Response.ok(res, MediaType.APPLICATION_JSON_TYPE)).auth().allowedOrigins(client).allowedMethods("POST").exposedHeaders(Cors.ACCESS_CONTROL_ALLOW_METHODS).build();
+ return Cors.add(request, Response.ok(res, MediaType.APPLICATION_JSON_TYPE)).auth().allowedOrigins(uriInfo, client).allowedMethods("POST").exposedHeaders(Cors.ACCESS_CONTROL_ALLOW_METHODS).build();
}
public Response buildClientCredentialsGrant() {
@@ -471,7 +471,7 @@ public class TokenEndpoint {
event.success();
- return Cors.add(request, Response.ok(res, MediaType.APPLICATION_JSON_TYPE)).auth().allowedOrigins(client).allowedMethods("POST").exposedHeaders(Cors.ACCESS_CONTROL_ALLOW_METHODS).build();
+ return Cors.add(request, Response.ok(res, MediaType.APPLICATION_JSON_TYPE)).auth().allowedOrigins(uriInfo, client).allowedMethods("POST").exposedHeaders(Cors.ACCESS_CONTROL_ALLOW_METHODS).build();
}
}
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/UserInfoEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/UserInfoEndpoint.java
index 89094da..1014c0b 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/UserInfoEndpoint.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/UserInfoEndpoint.java
@@ -132,7 +132,7 @@ public class UserInfoEndpoint {
RSATokenVerifier verifier = RSATokenVerifier.create(tokenString)
.realmUrl(Urls.realmIssuer(uriInfo.getBaseUri(), realm.getName()));
String kid = verifier.getHeader().getKeyId();
- verifier.publicKey(session.keys().getPublicKey(realm, kid));
+ verifier.publicKey(session.keys().getRsaPublicKey(realm, kid));
token = verifier.verify().getToken();
} catch (VerificationException e) {
event.error(Errors.INVALID_TOKEN);
@@ -194,7 +194,7 @@ public class UserInfoEndpoint {
claims.put("aud", audience);
Algorithm signatureAlg = cfg.getUserInfoSignedResponseAlg();
- PrivateKey privateKey = session.keys().getActiveKey(realm).getPrivateKey();
+ PrivateKey privateKey = session.keys().getActiveRsaKey(realm).getPrivateKey();
String signedUserInfo = new JWSBuilder()
.jsonContent(claims)
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/installation/KeycloakOIDCClientInstallation.java b/services/src/main/java/org/keycloak/protocol/oidc/installation/KeycloakOIDCClientInstallation.java
index 79a3919..dfae565 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/installation/KeycloakOIDCClientInstallation.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/installation/KeycloakOIDCClientInstallation.java
@@ -87,7 +87,7 @@ public class KeycloakOIDCClientInstallation implements ClientInstallationProvide
return false;
}
- if (client.isBearerOnly() && client.getNodeReRegistrationTimeout() <= 0) {
+ if (client.isBearerOnly() && !client.isServiceAccountsEnabled() && client.getNodeReRegistrationTimeout() <= 0) {
return false;
}
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/mappers/OIDCAttributeMapperHelper.java b/services/src/main/java/org/keycloak/protocol/oidc/mappers/OIDCAttributeMapperHelper.java
index 7b8ba36..5acde8b 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/mappers/OIDCAttributeMapperHelper.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/mappers/OIDCAttributeMapperHelper.java
@@ -56,7 +56,7 @@ public class OIDCAttributeMapperHelper {
if (attributeValue instanceof List) {
List<Object> valueAsList = (List<Object>) attributeValue;
- if (valueAsList.size() == 0) return null;
+ if (valueAsList.isEmpty()) return null;
if (isMultivalued(mappingModel)) {
List<Object> result = new ArrayList<>();
@@ -69,7 +69,7 @@ public class OIDCAttributeMapperHelper {
ServicesLogger.LOGGER.multipleValuesForMapper(attributeValue.toString(), mappingModel.getName());
}
- attributeValue = valueAsList;
+ attributeValue = valueAsList.get(0);
}
}
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocolService.java b/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocolService.java
index b07f06a..b535636 100644
--- a/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocolService.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocolService.java
@@ -26,6 +26,7 @@ import org.keycloak.jose.jwk.JSONWebKeySet;
import org.keycloak.jose.jwk.JWK;
import org.keycloak.jose.jwk.JWKBuilder;
import org.keycloak.keys.KeyMetadata;
+import org.keycloak.keys.RsaKeyMetadata;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.protocol.oidc.endpoints.AuthorizationEndpoint;
@@ -187,11 +188,11 @@ public class OIDCLoginProtocolService {
@Produces(MediaType.APPLICATION_JSON)
@NoCache
public Response certs() {
- List<KeyMetadata> publicKeys = session.keys().getKeys(realm, false);
+ List<RsaKeyMetadata> publicKeys = session.keys().getRsaKeys(realm, false);
JWK[] keys = new JWK[publicKeys.size()];
int i = 0;
- for (KeyMetadata k : publicKeys) {
+ for (RsaKeyMetadata k : publicKeys) {
keys[i++] = JWKBuilder.create().kid(k.getKid()).rs256(k.getPublicKey());
}
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java b/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java
index 5e48c7b..43985f2 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java
@@ -287,7 +287,7 @@ public class TokenManager {
public RefreshToken toRefreshToken(KeycloakSession session, RealmModel realm, String encodedRefreshToken) throws JWSInputException, OAuthErrorException {
JWSInput jws = new JWSInput(encodedRefreshToken);
- if (!RSAProvider.verify(jws, session.keys().getPublicKey(realm, jws.getHeader().getKeyId()))) {
+ if (!RSAProvider.verify(jws, session.keys().getRsaPublicKey(realm, jws.getHeader().getKeyId()))) {
throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Invalid refresh token");
}
@@ -298,7 +298,7 @@ public class TokenManager {
try {
JWSInput jws = new JWSInput(encodedIDToken);
IDToken idToken;
- if (!RSAProvider.verify(jws, session.keys().getPublicKey(realm, jws.getHeader().getKeyId()))) {
+ if (!RSAProvider.verify(jws, session.keys().getRsaPublicKey(realm, jws.getHeader().getKeyId()))) {
throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Invalid IDToken");
}
idToken = jws.readJsonContent(IDToken.class);
@@ -626,8 +626,8 @@ public class TokenManager {
}
public String encodeToken(KeycloakSession session, RealmModel realm, Object token) {
- KeyManager.ActiveKey activeKey = session.keys().getActiveKey(realm);
- return new JWSBuilder().type(JWT).kid(activeKey.getKid()).jsonContent(token).sign(jwsAlgorithm, activeKey.getPrivateKey());
+ KeyManager.ActiveRsaKey activeRsaKey = session.keys().getActiveRsaKey(realm);
+ return new JWSBuilder().type(JWT).kid(activeRsaKey.getKid()).jsonContent(token).sign(jwsAlgorithm, activeRsaKey.getPrivateKey());
}
public AccessTokenResponseBuilder responseBuilder(RealmModel realm, ClientModel client, EventBuilder event, KeycloakSession session, UserSessionModel userSession, ClientSessionModel clientSession) {
@@ -734,7 +734,7 @@ public class TokenManager {
public AccessTokenResponse build() {
- KeyManager.ActiveKey activeKey = session.keys().getActiveKey(realm);
+ KeyManager.ActiveRsaKey activeRsaKey = session.keys().getActiveRsaKey(realm);
if (accessToken != null) {
event.detail(Details.TOKEN_ID, accessToken.getId());
@@ -751,7 +751,7 @@ public class TokenManager {
AccessTokenResponse res = new AccessTokenResponse();
if (accessToken != null) {
- String encodedToken = new JWSBuilder().type(JWT).kid(activeKey.getKid()).jsonContent(accessToken).sign(jwsAlgorithm, activeKey.getPrivateKey());
+ String encodedToken = new JWSBuilder().type(JWT).kid(activeRsaKey.getKid()).jsonContent(accessToken).sign(jwsAlgorithm, activeRsaKey.getPrivateKey());
res.setToken(encodedToken);
res.setTokenType("bearer");
res.setSessionState(accessToken.getSessionState());
@@ -769,11 +769,11 @@ public class TokenManager {
}
if (idToken != null) {
- String encodedToken = new JWSBuilder().type(JWT).kid(activeKey.getKid()).jsonContent(idToken).sign(jwsAlgorithm, activeKey.getPrivateKey());
+ String encodedToken = new JWSBuilder().type(JWT).kid(activeRsaKey.getKid()).jsonContent(idToken).sign(jwsAlgorithm, activeRsaKey.getPrivateKey());
res.setIdToken(encodedToken);
}
if (refreshToken != null) {
- String encodedToken = new JWSBuilder().type(JWT).kid(activeKey.getKid()).jsonContent(refreshToken).sign(jwsAlgorithm, activeKey.getPrivateKey());
+ String encodedToken = new JWSBuilder().type(JWT).kid(activeRsaKey.getKid()).jsonContent(refreshToken).sign(jwsAlgorithm, activeRsaKey.getPrivateKey());
res.setRefreshToken(encodedToken);
if (refreshToken.getExpiration() != 0) {
res.setRefreshExpiresIn(refreshToken.getExpiration() - Time.currentTime());
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/utils/WebOriginsUtils.java b/services/src/main/java/org/keycloak/protocol/oidc/utils/WebOriginsUtils.java
index 83f90f0..6d15380 100644
--- a/services/src/main/java/org/keycloak/protocol/oidc/utils/WebOriginsUtils.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/utils/WebOriginsUtils.java
@@ -36,9 +36,8 @@ public class WebOriginsUtils {
if (client.getWebOrigins() != null) {
origins.addAll(client.getWebOrigins());
}
- if (origins.contains("+")) {
+ if (origins.contains(INCLUDE_REDIRECTS)) {
origins.remove(INCLUDE_REDIRECTS);
- client.getRedirectUris();
for (String redirectUri : RedirectUtils.resolveValidRedirects(uriInfo, client.getRootUrl(), client.getRedirectUris())) {
if (redirectUri.startsWith("http://") || redirectUri.startsWith("https://")) {
origins.add(UriUtils.getOrigin(redirectUri));
diff --git a/services/src/main/java/org/keycloak/protocol/RestartLoginCookie.java b/services/src/main/java/org/keycloak/protocol/RestartLoginCookie.java
index b333afa..51bdd81 100644
--- a/services/src/main/java/org/keycloak/protocol/RestartLoginCookie.java
+++ b/services/src/main/java/org/keycloak/protocol/RestartLoginCookie.java
@@ -22,6 +22,7 @@ import org.jboss.logging.Logger;
import org.keycloak.common.ClientConnection;
import org.keycloak.jose.jws.JWSBuilder;
import org.keycloak.jose.jws.JWSInput;
+import org.keycloak.jose.jws.crypto.HMACProvider;
import org.keycloak.jose.jws.crypto.RSAProvider;
import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientSessionModel;
@@ -31,6 +32,7 @@ import org.keycloak.models.RealmModel;
import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.util.CookieHelper;
+import javax.crypto.SecretKey;
import javax.ws.rs.core.Cookie;
import javax.ws.rs.core.UriInfo;
import java.security.PublicKey;
@@ -114,12 +116,11 @@ public class RestartLoginCookie {
}
public String encode(KeycloakSession session, RealmModel realm) {
- KeyManager.ActiveKey keys = session.keys().getActiveKey(realm);
+ KeyManager.ActiveHmacKey activeKey = session.keys().getActiveHmacKey(realm);
JWSBuilder builder = new JWSBuilder();
- return builder.kid(keys.getKid()).jsonContent(this)
- .rsa256(keys.getPrivateKey());
-
+ return builder.kid(activeKey.getKid()).jsonContent(this)
+ .hmac256(activeKey.getSecretKey());
}
public RestartLoginCookie() {
@@ -157,8 +158,8 @@ public class RestartLoginCookie {
}
String encodedCookie = cook.getValue();
JWSInput input = new JWSInput(encodedCookie);
- PublicKey publicKey = session.keys().getPublicKey(realm, input.getHeader().getKeyId());
- if (!RSAProvider.verify(input, publicKey)) {
+ SecretKey secretKey = session.keys().getHmacSecretKey(realm, input.getHeader().getKeyId());
+ if (!HMACProvider.verify(input, secretKey)) {
logger.debug("Failed to verify encoded RestartLoginCookie");
return null;
}
diff --git a/services/src/main/java/org/keycloak/protocol/saml/installation/SamlIDPDescriptorClientInstallation.java b/services/src/main/java/org/keycloak/protocol/saml/installation/SamlIDPDescriptorClientInstallation.java
index 9395662..b9c07ec 100755
--- a/services/src/main/java/org/keycloak/protocol/saml/installation/SamlIDPDescriptorClientInstallation.java
+++ b/services/src/main/java/org/keycloak/protocol/saml/installation/SamlIDPDescriptorClientInstallation.java
@@ -19,6 +19,7 @@ package org.keycloak.protocol.saml.installation;
import org.keycloak.Config;
import org.keycloak.common.util.PemUtils;
+import org.keycloak.keys.RsaKeyMetadata;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
@@ -87,11 +88,11 @@ public class SamlIDPDescriptorClientInstallation implements ClientInstallationPr
}
// keys
- Set<KeyMetadata> keys = new TreeSet<>((o1, o2) -> o1.getStatus() == o2.getStatus() // Status can be only PASSIVE OR ACTIVE, push PASSIVE to end of list
+ Set<RsaKeyMetadata> keys = new TreeSet<>((o1, o2) -> o1.getStatus() == o2.getStatus() // Status can be only PASSIVE OR ACTIVE, push PASSIVE to end of list
? (int) (o2.getProviderPriority() - o1.getProviderPriority())
: (o1.getStatus() == KeyMetadata.Status.PASSIVE ? 1 : -1));
- keys.addAll(session.keys().getKeys(realm, false));
- for (KeyMetadata key : keys) {
+ keys.addAll(session.keys().getRsaKeys(realm, false));
+ for (RsaKeyMetadata key : keys) {
addKeyInfo(sb, key, KeyTypes.SIGNING.value());
}
@@ -100,7 +101,7 @@ public class SamlIDPDescriptorClientInstallation implements ClientInstallationPr
return sb.toString();
}
- private static void addKeyInfo(StringBuilder target, KeyMetadata key, String purpose) {
+ private static void addKeyInfo(StringBuilder target, RsaKeyMetadata key, String purpose) {
if (key == null) {
return;
}
diff --git a/services/src/main/java/org/keycloak/protocol/saml/SamlProtocol.java b/services/src/main/java/org/keycloak/protocol/saml/SamlProtocol.java
index 791a917..89d6bf3 100755
--- a/services/src/main/java/org/keycloak/protocol/saml/SamlProtocol.java
+++ b/services/src/main/java/org/keycloak/protocol/saml/SamlProtocol.java
@@ -382,7 +382,7 @@ public class SamlProtocol implements LoginProtocol {
Document samlDocument = null;
KeyManager keyManager = session.keys();
- KeyManager.ActiveKey keys = keyManager.getActiveKey(realm);
+ KeyManager.ActiveRsaKey keys = keyManager.getActiveRsaKey(realm);
boolean postBinding = isPostBinding(clientSession);
String keyName = samlClient.getXmlSigKeyInfoKeyNameTransformer().getKeyName(keys.getKid(), keys.getCertificate());
@@ -518,7 +518,7 @@ public class SamlProtocol implements LoginProtocol {
String bindingUri = getLogoutServiceUrl(uriInfo, client, SAML_REDIRECT_BINDING);
SAML2LogoutRequestBuilder logoutBuilder = createLogoutRequest(bindingUri, clientSession, client);
if (samlClient.requiresRealmSignature() && samlClient.addExtensionsElementWithKeyInfo()) {
- KeyManager.ActiveKey keys = session.keys().getActiveKey(realm);
+ KeyManager.ActiveRsaKey keys = session.keys().getActiveRsaKey(realm);
String keyName = samlClient.getXmlSigKeyInfoKeyNameTransformer().getKeyName(keys.getKid(), keys.getCertificate());
logoutBuilder.addExtension(new KeycloakKeySamlExtensionGenerator(keyName));
}
@@ -561,7 +561,7 @@ public class SamlProtocol implements LoginProtocol {
if (canonicalization != null) {
binding.canonicalizationMethod(canonicalization);
}
- KeyManager.ActiveKey keys = session.keys().getActiveKey(realm);
+ KeyManager.ActiveRsaKey keys = session.keys().getActiveRsaKey(realm);
XmlKeyInfoKeyNameTransformer transformer = XmlKeyInfoKeyNameTransformer.from(
userSession.getNote(SAML_SERVER_SIGNATURE_KEYINFO_KEY_NAME_TRANSFORMER),
SamlClient.DEFAULT_XML_KEY_INFO_KEY_NAME_TRANSFORMER);
@@ -668,7 +668,7 @@ public class SamlProtocol implements LoginProtocol {
private JaxrsSAML2BindingBuilder createBindingBuilder(SamlClient samlClient) {
JaxrsSAML2BindingBuilder binding = new JaxrsSAML2BindingBuilder();
if (samlClient.requiresRealmSignature()) {
- KeyManager.ActiveKey keys = session.keys().getActiveKey(realm);
+ KeyManager.ActiveRsaKey keys = session.keys().getActiveRsaKey(realm);
String keyName = samlClient.getXmlSigKeyInfoKeyNameTransformer().getKeyName(keys.getKid(), keys.getCertificate());
binding.signatureAlgorithm(samlClient.getSignatureAlgorithm()).signWith(keyName, keys.getPrivateKey(), keys.getPublicKey(), keys.getCertificate()).signDocument();
}
diff --git a/services/src/main/java/org/keycloak/protocol/saml/SamlService.java b/services/src/main/java/org/keycloak/protocol/saml/SamlService.java
index fdb184e..1921b43 100755
--- a/services/src/main/java/org/keycloak/protocol/saml/SamlService.java
+++ b/services/src/main/java/org/keycloak/protocol/saml/SamlService.java
@@ -36,6 +36,7 @@ import org.keycloak.events.Details;
import org.keycloak.events.Errors;
import org.keycloak.events.EventBuilder;
import org.keycloak.events.EventType;
+import org.keycloak.keys.RsaKeyMetadata;
import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.KeyManager;
@@ -416,7 +417,7 @@ public class SamlService extends AuthorizationEndpointBase {
boolean postBinding = SamlProtocol.SAML_POST_BINDING.equals(logoutBinding);
if (samlClient.requiresRealmSignature()) {
SignatureAlgorithm algorithm = samlClient.getSignatureAlgorithm();
- KeyManager.ActiveKey keys = session.keys().getActiveKey(realm);
+ KeyManager.ActiveRsaKey keys = session.keys().getActiveRsaKey(realm);
binding.signatureAlgorithm(algorithm).signWith(keys.getKid(), keys.getPrivateKey(), keys.getPublicKey(), keys.getCertificate()).signDocument();
if (! postBinding && samlClient.addExtensionsElementWithKeyInfo()) { // Only include extension if REDIRECT binding and signing whole SAML protocol message
builder.addExtension(new KeycloakKeySamlExtensionGenerator(keys.getKid()));
@@ -567,18 +568,18 @@ public class SamlService extends AuthorizationEndpointBase {
props.put("idp.sso.HTTP-Redirect", RealmsResource.protocolUrl(uriInfo).build(realm.getName(), SamlProtocol.LOGIN_PROTOCOL).toString());
props.put("idp.sls.HTTP-POST", RealmsResource.protocolUrl(uriInfo).build(realm.getName(), SamlProtocol.LOGIN_PROTOCOL).toString());
StringBuilder keysString = new StringBuilder();
- Set<KeyMetadata> keys = new TreeSet<>((o1, o2) -> o1.getStatus() == o2.getStatus() // Status can be only PASSIVE OR ACTIVE, push PASSIVE to end of list
+ Set<RsaKeyMetadata> keys = new TreeSet<>((o1, o2) -> o1.getStatus() == o2.getStatus() // Status can be only PASSIVE OR ACTIVE, push PASSIVE to end of list
? (int) (o2.getProviderPriority() - o1.getProviderPriority())
: (o1.getStatus() == KeyMetadata.Status.PASSIVE ? 1 : -1));
- keys.addAll(session.keys().getKeys(realm, false));
- for (KeyMetadata key : keys) {
+ keys.addAll(session.keys().getRsaKeys(realm, false));
+ for (RsaKeyMetadata key : keys) {
addKeyInfo(keysString, key, KeyTypes.SIGNING.value());
}
props.put("idp.signing.certificates", keysString.toString());
return StringPropertyReplacer.replaceProperties(template, props);
}
- private static void addKeyInfo(StringBuilder target, KeyMetadata key, String purpose) {
+ private static void addKeyInfo(StringBuilder target, RsaKeyMetadata key, String purpose) {
if (key == null) {
return;
}
diff --git a/services/src/main/java/org/keycloak/services/clientregistration/ClientRegistrationTokenUtils.java b/services/src/main/java/org/keycloak/services/clientregistration/ClientRegistrationTokenUtils.java
index 33b8eab..e2d4846 100755
--- a/services/src/main/java/org/keycloak/services/clientregistration/ClientRegistrationTokenUtils.java
+++ b/services/src/main/java/org/keycloak/services/clientregistration/ClientRegistrationTokenUtils.java
@@ -75,7 +75,7 @@ public class ClientRegistrationTokenUtils {
return TokenVerification.error(new RuntimeException("Invalid token", e));
}
- PublicKey publicKey = session.keys().getPublicKey(realm, input.getHeader().getKeyId());
+ PublicKey publicKey = session.keys().getRsaPublicKey(realm, input.getHeader().getKeyId());
if (!RSAProvider.verify(input, publicKey)) {
return TokenVerification.error(new RuntimeException("Failed verify token"));
@@ -115,7 +115,7 @@ public class ClientRegistrationTokenUtils {
jwt.issuer(issuer);
jwt.audience(issuer);
- KeyManager.ActiveKey keys = session.keys().getActiveKey(realm);
+ KeyManager.ActiveRsaKey keys = session.keys().getActiveRsaKey(realm);
String token = new JWSBuilder().kid(keys.getKid()).jsonContent(jwt).rsa256(keys.getPrivateKey());
return token;
diff --git a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
index 04dfada..1392328 100755
--- a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
+++ b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
@@ -19,7 +19,7 @@ package org.keycloak.services.managers;
import org.jboss.logging.Logger;
import org.jboss.resteasy.specimpl.MultivaluedMapImpl;
import org.jboss.resteasy.spi.HttpRequest;
-import org.keycloak.RSATokenVerifier;
+import org.keycloak.TokenVerifier;
import org.keycloak.authentication.RequiredActionContext;
import org.keycloak.authentication.RequiredActionContextResult;
import org.keycloak.authentication.RequiredActionFactory;
@@ -33,6 +33,7 @@ import org.keycloak.events.Errors;
import org.keycloak.events.EventBuilder;
import org.keycloak.events.EventType;
import org.keycloak.forms.login.LoginFormsProvider;
+import org.keycloak.jose.jws.AlgorithmType;
import org.keycloak.jose.jws.JWSBuilder;
import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientSessionModel;
@@ -58,6 +59,7 @@ import org.keycloak.services.resources.RealmsResource;
import org.keycloak.services.util.CookieHelper;
import org.keycloak.services.util.P3PHelper;
+import javax.crypto.SecretKey;
import javax.ws.rs.core.Cookie;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MultivaluedMap;
@@ -113,12 +115,12 @@ public class AuthenticationManager {
if (cookie == null) return;
String tokenString = cookie.getValue();
- RSATokenVerifier verifier = RSATokenVerifier.create(tokenString).realmUrl(Urls.realmIssuer(uriInfo.getBaseUri(), realm.getName())).checkActive(false).checkTokenType(false);
+ TokenVerifier verifier = TokenVerifier.create(tokenString).realmUrl(Urls.realmIssuer(uriInfo.getBaseUri(), realm.getName())).checkActive(false).checkTokenType(false);
String kid = verifier.getHeader().getKeyId();
- PublicKey publicKey = session.keys().getPublicKey(realm, kid);
+ SecretKey secretKey = session.keys().getHmacSecretKey(realm, kid);
- AccessToken token = verifier.publicKey(publicKey).verify().getToken();
+ AccessToken token = verifier.secretKey(secretKey).verify().getToken();
UserSessionModel cookieSession = session.sessions().getUserSession(realm, token.getSessionState());
if (cookieSession == null || !cookieSession.getId().equals(userSession.getId())) return;
expireIdentityCookie(realm, uriInfo, connection);
@@ -337,14 +339,14 @@ public class AuthenticationManager {
}
protected static String encodeToken(KeycloakSession session, RealmModel realm, Object token) {
- KeyManager.ActiveKey activeKey = session.keys().getActiveKey(realm);
+ KeyManager.ActiveHmacKey activeKey = session.keys().getActiveHmacKey(realm);
logger.tracef("Encoding token with kid '%s'", activeKey.getKid());
String encodedToken = new JWSBuilder()
.kid(activeKey.getKid())
.jsonContent(token)
- .rsa256(activeKey.getPrivateKey());
+ .hmac256(activeKey.getSecretKey());
return encodedToken;
}
@@ -691,15 +693,25 @@ public class AuthenticationManager {
protected static AuthResult verifyIdentityToken(KeycloakSession session, RealmModel realm, UriInfo uriInfo, ClientConnection connection, boolean checkActive, boolean checkTokenType,
String tokenString, HttpHeaders headers) {
try {
- RSATokenVerifier verifier = RSATokenVerifier.create(tokenString).realmUrl(Urls.realmIssuer(uriInfo.getBaseUri(), realm.getName())).checkActive(checkActive).checkTokenType(checkTokenType);
+ TokenVerifier verifier = TokenVerifier.create(tokenString).realmUrl(Urls.realmIssuer(uriInfo.getBaseUri(), realm.getName())).checkActive(checkActive).checkTokenType(checkTokenType);
String kid = verifier.getHeader().getKeyId();
+ AlgorithmType algorithmType = verifier.getHeader().getAlgorithm().getType();
- PublicKey publicKey = session.keys().getPublicKey(realm, kid);
- if (publicKey == null) {
- logger.debugf("Identity cookie signed with unknown kid '%s'", kid);
- return null;
+ if (AlgorithmType.RSA.equals(algorithmType)) {
+ PublicKey publicKey = session.keys().getRsaPublicKey(realm, kid);
+ if (publicKey == null) {
+ logger.debugf("Identity cookie signed with unknown kid '%s'", kid);
+ return null;
+ }
+ verifier.publicKey(publicKey);
+ } else if (AlgorithmType.HMAC.equals(algorithmType)) {
+ SecretKey secretKey = session.keys().getHmacSecretKey(realm, kid);
+ if (secretKey == null) {
+ logger.debugf("Identity cookie signed with unknown kid '%s'", kid);
+ return null;
+ }
+ verifier.secretKey(secretKey);
}
- verifier.publicKey(publicKey);
AccessToken token = verifier.verify().getToken();
if (checkActive) {
diff --git a/services/src/main/java/org/keycloak/services/managers/RealmManager.java b/services/src/main/java/org/keycloak/services/managers/RealmManager.java
index 93aafd6..c647623 100755
--- a/services/src/main/java/org/keycloak/services/managers/RealmManager.java
+++ b/services/src/main/java/org/keycloak/services/managers/RealmManager.java
@@ -220,6 +220,7 @@ public class RealmManager implements RealmImporter {
realm.setFailureFactor(30);
realm.setSslRequired(SslRequired.EXTERNAL);
realm.setOTPPolicy(OTPPolicy.DEFAULT_POLICY);
+ realm.setLoginWithEmailAllowed(true);
realm.setEventsListeners(Collections.singleton("jboss-logging"));
diff --git a/services/src/main/java/org/keycloak/services/resources/AccountService.java b/services/src/main/java/org/keycloak/services/resources/AccountService.java
index 75cc5a7..f610cd0 100755
--- a/services/src/main/java/org/keycloak/services/resources/AccountService.java
+++ b/services/src/main/java/org/keycloak/services/resources/AccountService.java
@@ -400,7 +400,7 @@ public class AccountService extends AbstractSecuredLocalService {
String email = formData.getFirst("email");
String oldEmail = user.getEmail();
boolean emailChanged = oldEmail != null ? !oldEmail.equals(email) : email != null;
- if (emailChanged) {
+ if (emailChanged && !realm.isDuplicateEmailsAllowed()) {
UserModel existing = session.users().getUserByEmail(email, realm);
if (existing != null && !existing.getId().equals(user.getId())) {
throw new ModelDuplicateException(Messages.EMAIL_EXISTS);
@@ -419,9 +419,11 @@ public class AccountService extends AbstractSecuredLocalService {
}
if (realm.isRegistrationEmailAsUsername()) {
- UserModel existing = session.users().getUserByEmail(email, realm);
- if (existing != null && !existing.getId().equals(user.getId())) {
- throw new ModelDuplicateException(Messages.USERNAME_EXISTS);
+ if (!realm.isDuplicateEmailsAllowed()) {
+ UserModel existing = session.users().getUserByEmail(email, realm);
+ if (existing != null && !existing.getId().equals(user.getId())) {
+ throw new ModelDuplicateException(Messages.USERNAME_EXISTS);
+ }
}
user.setUsername(email);
}
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/ClientAttributeCertificateResource.java b/services/src/main/java/org/keycloak/services/resources/admin/ClientAttributeCertificateResource.java
index 09582a5..f386b64 100755
--- a/services/src/main/java/org/keycloak/services/resources/admin/ClientAttributeCertificateResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/ClientAttributeCertificateResource.java
@@ -34,7 +34,6 @@ import org.keycloak.models.KeyManager;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.utils.KeycloakModelUtils;
-import org.keycloak.protocol.oidc.utils.JWKSHttpUtils;
import org.keycloak.representations.KeyStoreConfig;
import org.keycloak.representations.idm.CertificateRepresentation;
import org.keycloak.services.ErrorResponseException;
@@ -374,8 +373,8 @@ public class ClientAttributeCertificateResource {
if (config.isRealmCertificate() == null || config.isRealmCertificate().booleanValue()) {
KeyManager keys = session.keys();
- String kid = keys.getActiveKey(realm).getKid();
- Certificate certificate = keys.getCertificate(realm, kid);
+ String kid = keys.getActiveRsaKey(realm).getKid();
+ Certificate certificate = keys.getRsaCertificate(realm, kid);
String certificateAlias = config.getRealmAlias();
if (certificateAlias == null) certificateAlias = realm.getName();
keyStore.setCertificateEntry(certificateAlias, certificate);
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/ClientResource.java b/services/src/main/java/org/keycloak/services/resources/admin/ClientResource.java
index 1275022..c97a8f5 100755
--- a/services/src/main/java/org/keycloak/services/resources/admin/ClientResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/ClientResource.java
@@ -164,7 +164,7 @@ public class ClientResource {
RepresentationToModel.updateClient(rep, client);
- if (Profile.isPreviewEnabled()) {
+ if (Profile.isFeatureEnabled(Profile.Feature.AUTHORIZATION)) {
if (TRUE.equals(rep.getAuthorizationServicesEnabled())) {
authorization().enable();
} else {
@@ -190,7 +190,7 @@ public class ClientResource {
ClientRepresentation representation = ModelToRepresentation.toRepresentation(client);
- if (Profile.isPreviewEnabled()) {
+ if (Profile.isFeatureEnabled(Profile.Feature.AUTHORIZATION)) {
representation.setAuthorizationServicesEnabled(authorization().isEnabled());
}
@@ -577,7 +577,7 @@ public class ClientResource {
@Path("/authz")
public AuthorizationService authorization() {
- ProfileHelper.requirePreview();
+ ProfileHelper.requireFeature(Profile.Feature.AUTHORIZATION);
AuthorizationService resource = new AuthorizationService(this.session, this.client, this.auth);
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/GroupResource.java b/services/src/main/java/org/keycloak/services/resources/admin/GroupResource.java
index ea42335..398e036 100755
--- a/services/src/main/java/org/keycloak/services/resources/admin/GroupResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/GroupResource.java
@@ -48,6 +48,7 @@ import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
+import org.keycloak.services.ErrorResponse;
/**
* @author Bill Burke
@@ -138,6 +139,12 @@ public class GroupResource {
if (group == null) {
throw new NotFoundException("Could not find group by id");
}
+
+ for (GroupModel group : group.getSubGroups()) {
+ if (group.getName().equals(rep.getName())) {
+ return ErrorResponse.exists("Parent already contains subgroup named '" + rep.getName() + "'");
+ }
+ }
Response.ResponseBuilder builder = Response.status(204);
GroupModel child = null;
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/GroupsResource.java b/services/src/main/java/org/keycloak/services/resources/admin/GroupsResource.java
index f670f57..9d01f35 100755
--- a/services/src/main/java/org/keycloak/services/resources/admin/GroupsResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/GroupsResource.java
@@ -39,6 +39,7 @@ import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;
import java.net.URI;
import java.util.List;
+import org.keycloak.services.ErrorResponse;
/**
* @author Bill Burke
@@ -102,6 +103,12 @@ public class GroupsResource {
public Response addTopLevelGroup(GroupRepresentation rep) {
auth.requireManage();
+ for (GroupModel group : realm.getGroups()) {
+ if (group.getName().equals(rep.getName())) {
+ return ErrorResponse.exists("Top level group named '" + rep.getName() + "' already exists.");
+ }
+ }
+
GroupModel child = null;
Response.ResponseBuilder builder = Response.status(204);
if (rep.getId() != null) {
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/KeyResource.java b/services/src/main/java/org/keycloak/services/resources/admin/KeyResource.java
index e58f102..45d644f 100644
--- a/services/src/main/java/org/keycloak/services/resources/admin/KeyResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/KeyResource.java
@@ -19,7 +19,9 @@ package org.keycloak.services.resources.admin;
import org.jboss.resteasy.annotations.cache.NoCache;
import org.keycloak.common.util.PemUtils;
-import org.keycloak.keys.KeyMetadata;
+import org.keycloak.jose.jws.AlgorithmType;
+import org.keycloak.keys.HmacKeyMetadata;
+import org.keycloak.keys.RsaKeyMetadata;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeyManager;
import org.keycloak.models.RealmModel;
@@ -28,9 +30,10 @@ import org.keycloak.representations.idm.KeysMetadataRepresentation;
import javax.ws.rs.GET;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
-import java.util.Collections;
+import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
+import java.util.Map;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@@ -56,20 +59,33 @@ public class KeyResource {
KeyManager keystore = session.keys();
KeysMetadataRepresentation keys = new KeysMetadataRepresentation();
- keys.setActive(Collections.singletonMap(KeyMetadata.Type.RSA.name(), keystore.getActiveKey(realm).getKid()));
+
+ Map<String, String> active = new HashMap<>();
+ active.put(AlgorithmType.RSA.name(), keystore.getActiveRsaKey(realm).getKid());
+ active.put(AlgorithmType.HMAC.name(), keystore.getActiveHmacKey(realm).getKid());
+ keys.setActive(active);
List<KeysMetadataRepresentation.KeyMetadataRepresentation> l = new LinkedList<>();
- for (KeyMetadata m : session.keys().getKeys(realm, true)) {
+ for (RsaKeyMetadata m : session.keys().getRsaKeys(realm, true)) {
KeysMetadataRepresentation.KeyMetadataRepresentation r = new KeysMetadataRepresentation.KeyMetadataRepresentation();
r.setProviderId(m.getProviderId());
r.setProviderPriority(m.getProviderPriority());
r.setKid(m.getKid());
r.setStatus(m.getStatus() != null ? m.getStatus().name() : null);
- r.setType(m.getType() != null ? m.getType().name() : null);
+ r.setType(AlgorithmType.RSA.name());
r.setPublicKey(PemUtils.encodeKey(m.getPublicKey()));
r.setCertificate(PemUtils.encodeCertificate(m.getCertificate()));
l.add(r);
}
+ for (HmacKeyMetadata m : session.keys().getHmacKeys(realm, true)) {
+ KeysMetadataRepresentation.KeyMetadataRepresentation r = new KeysMetadataRepresentation.KeyMetadataRepresentation();
+ r.setProviderId(m.getProviderId());
+ r.setProviderPriority(m.getProviderPriority());
+ r.setKid(m.getKid());
+ r.setStatus(m.getStatus() != null ? m.getStatus().name() : null);
+ r.setType(AlgorithmType.HMAC.name());
+ l.add(r);
+ }
keys.setKeys(l);
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java b/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java
index 55427f7..8a69998 100644
--- a/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java
@@ -302,6 +302,7 @@ public class RealmAdminResource {
}
}
+ boolean wasDuplicateEmailsAllowed = realm.isDuplicateEmailsAllowed();
RepresentationToModel.updateRealm(rep, realm, session);
// Refresh periodic sync tasks for configured federationProviders
@@ -312,6 +313,12 @@ public class RealmAdminResource {
}
adminEvent.operation(OperationType.UPDATE).representation(StripSecretsUtils.strip(rep)).success();
+
+ if (rep.isDuplicateEmailsAllowed() != null && rep.isDuplicateEmailsAllowed() != wasDuplicateEmailsAllowed) {
+ UserCache cache = session.getProvider(UserCache.class);
+ if (cache != null) cache.clear();
+ }
+
return Response.noContent().build();
} catch (PatternSyntaxException e) {
return ErrorResponse.error("Specified regex pattern(s) is invalid.", Response.Status.BAD_REQUEST);
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java b/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java
index c866ac9..83265cb 100755
--- a/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java
@@ -23,6 +23,7 @@ import org.jboss.resteasy.spi.NotFoundException;
import org.jboss.resteasy.spi.ResteasyProviderFactory;
import org.keycloak.authentication.RequiredActionProvider;
import org.keycloak.common.ClientConnection;
+import org.keycloak.common.Profile;
import org.keycloak.common.util.Time;
import org.keycloak.credential.CredentialModel;
import org.keycloak.email.EmailException;
@@ -70,6 +71,7 @@ import org.keycloak.models.UserManager;
import org.keycloak.services.managers.UserSessionManager;
import org.keycloak.services.resources.AccountService;
import org.keycloak.services.validation.Validation;
+import org.keycloak.utils.ProfileHelper;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
@@ -182,8 +184,10 @@ public class UsersResource {
} catch (ModelReadOnlyException re) {
return ErrorResponse.exists("User is read only!");
} catch (ModelException me) {
+ logger.warn("Could not update user!", me);
return ErrorResponse.exists("Could not update user!");
- } catch (Exception me) { // JPA may be committed by JTA which can't
+ } catch (Exception me) { // JPA
+ logger.warn("Could not update user!", me);// may be committed by JTA which can't
return ErrorResponse.exists("Could not update user!");
}
}
@@ -206,7 +210,7 @@ public class UsersResource {
if (session.users().getUserByUsername(rep.getUsername(), realm) != null) {
return ErrorResponse.exists("User exists with same username");
}
- if (rep.getEmail() != null && session.users().getUserByEmail(rep.getEmail(), realm) != null) {
+ if (rep.getEmail() != null && !realm.isDuplicateEmailsAllowed() && session.users().getUserByEmail(rep.getEmail(), realm) != null) {
return ErrorResponse.exists("User exists with same email");
}
@@ -231,6 +235,7 @@ public class UsersResource {
if (session.getTransactionManager().isActive()) {
session.getTransactionManager().setRollbackOnly();
}
+ logger.warn("Could not create user", me);
return ErrorResponse.exists("Could not create user");
}
}
@@ -316,6 +321,8 @@ public class UsersResource {
@NoCache
@Produces(MediaType.APPLICATION_JSON)
public Map<String, Object> impersonate(final @PathParam("id") String id) {
+ ProfileHelper.requireFeature(Profile.Feature.IMPERSONATION);
+
auth.init(RealmAuth.Resource.IMPERSONATION);
auth.requireManage();
diff --git a/services/src/main/java/org/keycloak/services/resources/Cors.java b/services/src/main/java/org/keycloak/services/resources/Cors.java
index 2cd07b1..f938a5f 100755
--- a/services/src/main/java/org/keycloak/services/resources/Cors.java
+++ b/services/src/main/java/org/keycloak/services/resources/Cors.java
@@ -16,24 +16,27 @@
*/
package org.keycloak.services.resources;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.Response.ResponseBuilder;
+import javax.ws.rs.core.UriInfo;
import org.jboss.logging.Logger;
import org.jboss.resteasy.spi.HttpRequest;
import org.jboss.resteasy.spi.HttpResponse;
import org.keycloak.common.util.CollectionUtil;
+import org.keycloak.common.util.UriUtils;
import org.keycloak.models.ClientModel;
+import org.keycloak.protocol.oidc.utils.WebOriginsUtils;
import org.keycloak.representations.AccessToken;
-import javax.ws.rs.core.Response;
-import javax.ws.rs.core.Response.ResponseBuilder;
-import java.util.Arrays;
-import java.util.HashSet;
-import java.util.Set;
-import java.util.concurrent.TimeUnit;
-
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class Cors {
+
private static final Logger logger = Logger.getLogger(Cors.class);
public static final long DEFAULT_MAX_AGE = TimeUnit.HOURS.toSeconds(1);
@@ -51,6 +54,7 @@ public class Cors {
public static final String ACCESS_CONTROL_MAX_AGE = "Access-Control-Max-Age";
public static final String ACCESS_CONTROL_ALLOW_ORIGIN_WILDCARD = "*";
+ public static final String INCLUDE_REDIRECTS = "+";
private HttpRequest request;
private ResponseBuilder builder;
@@ -88,9 +92,9 @@ public class Cors {
return this;
}
- public Cors allowedOrigins(ClientModel client) {
+ public Cors allowedOrigins(UriInfo uriInfo, ClientModel client) {
if (client != null) {
- allowedOrigins = client.getWebOrigins();
+ allowedOrigins = WebOriginsUtils.resolveValidWebOrigins(uriInfo, client);
}
return this;
}
diff --git a/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java b/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java
index b1f4587..162de45 100755
--- a/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java
+++ b/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java
@@ -898,7 +898,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
}
private Response corsResponse(Response response, ClientModel clientModel) {
- return Cors.add(this.request, Response.fromResponse(response)).auth().allowedOrigins(clientModel).build();
+ return Cors.add(this.request, Response.fromResponse(response)).auth().allowedOrigins(uriInfo, clientModel).build();
}
private void fireErrorEvent(String message, Throwable throwable) {
diff --git a/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java b/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java
index 2bdb129..14df1dc 100755
--- a/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java
+++ b/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java
@@ -715,14 +715,15 @@ public class LoginActionsService {
String keyFromSession = null;
if (code != null) {
clientSession = ClientSessionCode.getClientSession(code, session, realm);
- keyFromSession = clientSession.getNote(Constants.VERIFY_EMAIL_KEY);
+ keyFromSession = clientSession != null ? clientSession.getNote(Constants.VERIFY_EMAIL_KEY) : null;
}
- if (clientSession == null || !key.equals(keyFromSession)) {
+ if (!key.equals(keyFromSession)) {
ServicesLogger.LOGGER.invalidKeyForEmailVerification();
event.error(Errors.INVALID_CODE);
throw new WebApplicationException(ErrorPage.error(session, Messages.STALE_VERIFY_EMAIL_LINK));
}
+
clientSession.removeNote(Constants.VERIFY_EMAIL_KEY);
Checks checks = new Checks();
diff --git a/services/src/main/java/org/keycloak/services/resources/PublicRealmResource.java b/services/src/main/java/org/keycloak/services/resources/PublicRealmResource.java
index 227719d..7526139 100755
--- a/services/src/main/java/org/keycloak/services/resources/PublicRealmResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/PublicRealmResource.java
@@ -93,7 +93,7 @@ public class PublicRealmResource {
rep.setTokenServiceUrl(OIDCLoginProtocolService.tokenServiceBaseUrl(uriInfo).build(realm.getName()).toString());
rep.setAccountServiceUrl(AccountService.accountServiceBaseUrl(uriInfo).build(realm.getName()).toString());
rep.setAdminApiUrl(uriInfo.getBaseUriBuilder().path(AdminRoot.class).build().toString());
- rep.setPublicKeyPem(PemUtils.encodeKey(session.keys().getActiveKey(realm).getPublicKey()));
+ rep.setPublicKeyPem(PemUtils.encodeKey(session.keys().getActiveRsaKey(realm).getPublicKey()));
rep.setNotBefore(realm.getNotBefore());
return rep;
}
diff --git a/services/src/main/java/org/keycloak/services/resources/RealmsResource.java b/services/src/main/java/org/keycloak/services/resources/RealmsResource.java
index 3ee7938..1508042 100755
--- a/services/src/main/java/org/keycloak/services/resources/RealmsResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/RealmsResource.java
@@ -22,6 +22,7 @@ import org.jboss.resteasy.spi.ResteasyProviderFactory;
import org.keycloak.authorization.AuthorizationProvider;
import org.keycloak.authorization.AuthorizationService;
import org.keycloak.common.ClientConnection;
+import org.keycloak.common.Profile;
import org.keycloak.common.util.KeycloakUriBuilder;
import org.keycloak.events.EventBuilder;
import org.keycloak.models.ClientModel;
@@ -264,7 +265,7 @@ public class RealmsResource {
@Path("{realm}/authz")
public Object getAuthorizationService(@PathParam("realm") String name) {
- ProfileHelper.requirePreview();
+ ProfileHelper.requireFeature(Profile.Feature.AUTHORIZATION);
init(name);
AuthorizationProvider authorization = this.session.getProvider(AuthorizationProvider.class);
diff --git a/services/src/main/java/org/keycloak/utils/ProfileHelper.java b/services/src/main/java/org/keycloak/utils/ProfileHelper.java
index 719bd24..b1a29f4 100644
--- a/services/src/main/java/org/keycloak/utils/ProfileHelper.java
+++ b/services/src/main/java/org/keycloak/utils/ProfileHelper.java
@@ -27,9 +27,9 @@ import javax.ws.rs.core.Response;
*/
public class ProfileHelper {
- public static void requirePreview() {
- if (!Profile.isPreviewEnabled()) {
- throw new WebApplicationException("Feature not available in current profile", Response.Status.NOT_IMPLEMENTED);
+ public static void requireFeature(Profile.Feature feature) {
+ if (!Profile.isFeatureEnabled(feature)) {
+ throw new WebApplicationException("Feature not enabled", Response.Status.NOT_IMPLEMENTED);
}
}
diff --git a/services/src/main/resources/META-INF/services/org.keycloak.keys.KeyProviderFactory b/services/src/main/resources/META-INF/services/org.keycloak.keys.KeyProviderFactory
index a606c6e..a57070b 100644
--- a/services/src/main/resources/META-INF/services/org.keycloak.keys.KeyProviderFactory
+++ b/services/src/main/resources/META-INF/services/org.keycloak.keys.KeyProviderFactory
@@ -15,6 +15,7 @@
# limitations under the License.
#
+org.keycloak.keys.GeneratedHmacKeyProviderFactory
org.keycloak.keys.GeneratedRsaKeyProviderFactory
org.keycloak.keys.JavaKeystoreKeyProviderFactory
-org.keycloak.keys.RsaKeyProviderFactory
+org.keycloak.keys.ImportedRsaKeyProviderFactory
\ No newline at end of file
testsuite/integration/pom.xml 27(+0 -27)
diff --git a/testsuite/integration/pom.xml b/testsuite/integration/pom.xml
index 131b7cc..1c29c82 100755
--- a/testsuite/integration/pom.xml
+++ b/testsuite/integration/pom.xml
@@ -591,32 +591,5 @@
</build>
</profile>
- <!-- Ldap profiles -->
- <profile>
- <activation>
- <property>
- <name>ldap.vendor</name>
- <value>msad</value>
- </property>
- </activation>
- <id>msad</id>
- <build>
- <plugins>
- <plugin>
- <groupId>org.apache.maven.plugins</groupId>
- <artifactId>maven-surefire-plugin</artifactId>
- <configuration>
- <includes>
- <include>org/keycloak/testsuite/federation/ldap/base/**</include>
- </includes>
- <excludes>
- <exclude>**/LDAPMultipleAttributesTest.java</exclude>
- </excludes>
- </configuration>
- </plugin>
- </plugins>
- </build>
- </profile>
-
</profiles>
</project>
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/AdapterTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/AdapterTest.java
index 404d24d..1f2f620 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/AdapterTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/AdapterTest.java
@@ -72,6 +72,12 @@ public class AdapterTest {
.name("product-portal").contextPath("/product-portal")
.servletClass(ProductServlet.class).adapterConfigPath(url.getPath())
.role("user").deployApplication();
+
+ url = getClass().getResource("/adapter-test/product-autodetect-bearer-only-keycloak.json");
+ createApplicationDeployment()
+ .name("product-portal-autodetect-bearer-only").contextPath("/product-portal-autodetect-bearer-only")
+ .servletClass(ProductServlet.class).adapterConfigPath(url.getPath())
+ .role("user").deployApplication();
// Test that replacing system properties works for adapters
System.setProperty("app.server.base.url", "http://localhost:8081");
@@ -150,6 +156,11 @@ public class AdapterTest {
}
@Test
+ public void testAutodetectBearerOnly() throws Exception {
+ testStrategy.testAutodetectBearerOnly();
+ }
+
+ @Test
public void testBasicAuthErrorHandling() throws Exception {
testStrategy.testBasicAuthErrorHandling();
}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/AdapterTestStrategy.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/AdapterTestStrategy.java
index 9341df2..bd0a144 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/AdapterTestStrategy.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/AdapterTestStrategy.java
@@ -400,6 +400,55 @@ public class AdapterTestStrategy extends ExternalResource {
Time.setOffset(0);
}
+ public void testAutodetectBearerOnly() throws Exception {
+ Client client = ClientBuilder.newClient();
+
+ // Do not redirect client to login page if it's an XHR
+ WebTarget target = client.target(APP_SERVER_BASE_URL + "/product-portal-autodetect-bearer-only");
+ Response response = target.request().header("X-Requested-With", "XMLHttpRequest").get();
+ Assert.assertEquals(401, response.getStatus());
+ response.close();
+
+ // Do not redirect client to login page if it's a partial Faces request
+ response = target.request().header("Faces-Request", "partial/ajax").get();
+ Assert.assertEquals(401, response.getStatus());
+ response.close();
+
+ // Do not redirect client to login page if it's a SOAP request
+ response = target.request().header("SOAPAction", "").get();
+ Assert.assertEquals(401, response.getStatus());
+ response.close();
+
+ // Do not redirect client to login page if Accept header is missing
+ response = target.request().get();
+ Assert.assertEquals(401, response.getStatus());
+ response.close();
+
+ // Do not redirect client to login page if client does not understand HTML reponses
+ response = target.request().header(HttpHeaders.ACCEPT, "application/json,text/xml").get();
+ Assert.assertEquals(401, response.getStatus());
+ response.close();
+
+ // Redirect client to login page if it's not an XHR
+ response = target.request().header("X-Requested-With", "Dont-Know").header(HttpHeaders.ACCEPT, "*/*").get();
+ Assert.assertEquals(302, response.getStatus());
+ Assert.assertTrue(response.getHeaderString(HttpHeaders.LOCATION).contains("response_type=code"));
+ response.close();
+
+ // Redirect client to login page if client explicitely understands HTML responses
+ response = target.request().header(HttpHeaders.ACCEPT, "text/html,application/xhtml+xml,application/xml;q=0.9").get();
+ Assert.assertEquals(302, response.getStatus());
+ Assert.assertTrue(response.getHeaderString(HttpHeaders.LOCATION).contains("response_type=code"));
+ response.close();
+
+ // Redirect client to login page if client understands all response types
+ response = target.request().header(HttpHeaders.ACCEPT, "*/*").get();
+ Assert.assertEquals(302, response.getStatus());
+ Assert.assertTrue(response.getHeaderString(HttpHeaders.LOCATION).contains("response_type=code"));
+ response.close();
+ client.close();
+ }
+
/**
* KEYCLOAK-518
* @throws Exception
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/authorization/AbstractPhotozAdminTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/authorization/AbstractPhotozAdminTest.java
index c9b69d8..c46b338 100644
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/authorization/AbstractPhotozAdminTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/authorization/AbstractPhotozAdminTest.java
@@ -101,7 +101,7 @@ public abstract class AbstractPhotozAdminTest extends AbstractAuthorizationTest
// during tests we create resource instances, but we need to reload them to get their collections updated
List<ResourcePermission> updatedPermissions = permissions.stream().map(permission -> {
- Resource resource = storeFactory.getResourceStore().findById(permission.getResource().getId());
+ Resource resource = storeFactory.getResourceStore().findById(permission.getResource().getId(), resourceServer.getId());
return new ResourcePermission(resource, permission.getScopes(), permission.getResourceServer());
}).collect(Collectors.toList());
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/authorization/ResourceManagementTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/authorization/ResourceManagementTest.java
index 086b5ac..4a4fc9a 100644
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/authorization/ResourceManagementTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/authorization/ResourceManagementTest.java
@@ -53,7 +53,7 @@ public class ResourceManagementTest extends AbstractPhotozAdminTest {
ResourceRepresentation resource = response.readEntity(ResourceRepresentation.class);
onAuthorizationSession(authorizationProvider -> {
- Resource resourceModel = authorizationProvider.getStoreFactory().getResourceStore().findById(resource.getId());
+ Resource resourceModel = authorizationProvider.getStoreFactory().getResourceStore().findById(resource.getId(), resourceServer.getId());
assertNotNull(resourceModel);
assertEquals(resource.getId(), resourceModel.getId());
@@ -89,7 +89,7 @@ public class ResourceManagementTest extends AbstractPhotozAdminTest {
ResourceRepresentation resource = response.readEntity(ResourceRepresentation.class);
onAuthorizationSession(authorizationProvider -> {
- Resource resourceModel = authorizationProvider.getStoreFactory().getResourceStore().findById(resource.getId());
+ Resource resourceModel = authorizationProvider.getStoreFactory().getResourceStore().findById(resource.getId(), resourceServer.getId());
assertNotNull(resourceModel);
assertEquals(resource.getId(), resourceModel.getId());
@@ -147,7 +147,7 @@ public class ResourceManagementTest extends AbstractPhotozAdminTest {
assertEquals(Status.NO_CONTENT.getStatusCode(), response.getStatus());
onAuthorizationSession(authorizationProvider -> {
- Resource resourceModel = authorizationProvider.getStoreFactory().getResourceStore().findById(resource.getId());
+ Resource resourceModel = authorizationProvider.getStoreFactory().getResourceStore().findById(resource.getId(), resourceServer.getId());
assertNull(resourceModel);
});
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/authorization/ResourcePermissionManagementTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/authorization/ResourcePermissionManagementTest.java
index 9ecbc3d..4708a2a 100644
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/authorization/ResourcePermissionManagementTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/authorization/ResourcePermissionManagementTest.java
@@ -46,8 +46,12 @@ import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
@@ -77,7 +81,7 @@ public class ResourcePermissionManagementTest extends AbstractPhotozAdminTest {
PolicyRepresentation permission = response.readEntity(PolicyRepresentation.class);
onAuthorizationSession(authorizationProvider -> {
- Policy policyModel = authorizationProvider.getStoreFactory().getPolicyStore().findById(permission.getId());
+ Policy policyModel = authorizationProvider.getStoreFactory().getPolicyStore().findById(permission.getId(), resourceServer.getId());
assertNotNull(policyModel);
assertEquals(permission.getId(), policyModel.getId());
@@ -357,7 +361,7 @@ public class ResourcePermissionManagementTest extends AbstractPhotozAdminTest {
PolicyRepresentation permission = response.readEntity(PolicyRepresentation.class);
onAuthorizationSession(authorizationProvider -> {
- Policy policyModel = authorizationProvider.getStoreFactory().getPolicyStore().findById(permission.getId());
+ Policy policyModel = authorizationProvider.getStoreFactory().getPolicyStore().findById(permission.getId(), resourceServer.getId());
assertNotNull(policyModel);
assertEquals(permission.getId(), policyModel.getId());
@@ -430,7 +434,8 @@ public class ResourcePermissionManagementTest extends AbstractPhotozAdminTest {
config.put("defaultResourceType", albumResource.getType());
- String applyPolicies = JsonSerialization.writeValueAsString(new String[]{this.anyUserPolicy.getId(), this.administrationPolicy.getId()});
+ String[] associatedPolicies = {this.anyUserPolicy.getId(), this.administrationPolicy.getId()};
+ String applyPolicies = JsonSerialization.writeValueAsString(associatedPolicies);
config.put("applyPolicies", applyPolicies);
@@ -443,14 +448,15 @@ public class ResourcePermissionManagementTest extends AbstractPhotozAdminTest {
PolicyRepresentation permission = response.readEntity(PolicyRepresentation.class);
onAuthorizationSession(authorizationProvider -> {
- Policy policyModel = authorizationProvider.getStoreFactory().getPolicyStore().findById(permission.getId());
+ Policy policyModel = authorizationProvider.getStoreFactory().getPolicyStore().findById(permission.getId(), resourceServer.getId());
assertNotNull(policyModel);
assertEquals(permission.getId(), policyModel.getId());
assertEquals(permission.getName(), policyModel.getName());
assertEquals(permission.getType(), policyModel.getType());
assertTrue(permission.getConfig().containsValue(albumResource.getType()));
- assertTrue(permission.getConfig().containsValue(applyPolicies));
+ assertTrue(policyModel.getAssociatedPolicies().stream().map(Policy::getId).collect(Collectors.toList()).containsAll(Arrays.asList(associatedPolicies)));
+
assertEquals(resourceServer.getId(), policyModel.getResourceServer().getId());
});
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/authorization/ScopeManagementTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/authorization/ScopeManagementTest.java
index b2e1a42..644883f 100644
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/authorization/ScopeManagementTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/authorization/ScopeManagementTest.java
@@ -57,7 +57,7 @@ public class ScopeManagementTest extends AbstractPhotozAdminTest {
ScopeRepresentation scope = response.readEntity(ScopeRepresentation.class);
onAuthorizationSession(authorizationProvider -> {
- Scope scopeModel = authorizationProvider.getStoreFactory().getScopeStore().findById(scope.getId());
+ Scope scopeModel = authorizationProvider.getStoreFactory().getScopeStore().findById(scope.getId(), resourceServer.getId());
assertNotNull(scopeModel);
assertEquals(scope.getId(), scopeModel.getId());
@@ -86,7 +86,7 @@ public class ScopeManagementTest extends AbstractPhotozAdminTest {
ScopeRepresentation scope = response.readEntity(ScopeRepresentation.class);
onAuthorizationSession(authorizationProvider -> {
- Scope scopeModel = authorizationProvider.getStoreFactory().getScopeStore().findById(scope.getId());
+ Scope scopeModel = authorizationProvider.getStoreFactory().getScopeStore().findById(scope.getId(), resourceServer.getId());
assertNotNull(scopeModel);
assertEquals(scope.getId(), scopeModel.getId());
@@ -138,7 +138,7 @@ public class ScopeManagementTest extends AbstractPhotozAdminTest {
assertEquals(Status.NO_CONTENT.getStatusCode(), response.getStatus());
onAuthorizationSession(authorizationProvider -> {
- Scope scopeModel = authorizationProvider.getStoreFactory().getScopeStore().findById(scope.getId());
+ Scope scopeModel = authorizationProvider.getStoreFactory().getScopeStore().findById(scope.getId(), resourceServer.getId());
assertNull(scopeModel);
});
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/ldap/LDAPBinaryAttributesTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/ldap/LDAPBinaryAttributesTest.java
new file mode 100644
index 0000000..1707aef
--- /dev/null
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/ldap/LDAPBinaryAttributesTest.java
@@ -0,0 +1,287 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.testsuite.federation.storage.ldap;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import javax.ws.rs.ClientErrorException;
+import javax.ws.rs.core.Response;
+
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.FixMethodOrder;
+import org.junit.Test;
+import org.junit.rules.RuleChain;
+import org.junit.rules.TestRule;
+import org.junit.runners.MethodSorters;
+import org.keycloak.admin.client.Keycloak;
+import org.keycloak.common.util.MultivaluedHashMap;
+import org.keycloak.component.ComponentModel;
+import org.keycloak.component.ComponentValidationException;
+import org.keycloak.models.Constants;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.LDAPConstants;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserModel;
+import org.keycloak.models.utils.UserModelDelegate;
+import org.keycloak.representations.idm.ComponentRepresentation;
+import org.keycloak.representations.idm.UserRepresentation;
+import org.keycloak.services.managers.RealmManager;
+import org.keycloak.storage.UserStorageProvider;
+import org.keycloak.storage.UserStorageProviderModel;
+import org.keycloak.storage.ldap.LDAPStorageProvider;
+import org.keycloak.storage.ldap.LDAPStorageProviderFactory;
+import org.keycloak.storage.ldap.LDAPUtils;
+import org.keycloak.storage.ldap.idm.model.LDAPObject;
+import org.keycloak.storage.ldap.mappers.LDAPStorageMapper;
+import org.keycloak.storage.ldap.mappers.UserAttributeLDAPStorageMapper;
+import org.keycloak.testsuite.rule.KeycloakRule;
+import org.keycloak.testsuite.rule.LDAPRule;
+
+import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.MASTER;
+import static org.keycloak.models.AdminRoles.ADMIN;
+import static org.keycloak.testsuite.Constants.AUTH_SERVER_ROOT;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+public class LDAPBinaryAttributesTest {
+
+ private static LDAPRule ldapRule = new LDAPRule();
+
+ private static ComponentModel ldapModel = null;
+
+
+ private static KeycloakRule keycloakRule = new KeycloakRule(new KeycloakRule.KeycloakSetup() {
+
+ @Override
+ public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
+ MultivaluedHashMap<String,String> ldapConfig = LDAPTestUtils.getLdapRuleConfig(ldapRule);
+ ldapConfig.putSingle(LDAPConstants.SYNC_REGISTRATIONS, "true");
+ ldapConfig.putSingle(LDAPConstants.EDIT_MODE, UserStorageProvider.EditMode.WRITABLE.toString());
+ UserStorageProviderModel model = new UserStorageProviderModel();
+ model.setLastSync(0);
+ model.setChangedSyncPeriod(-1);
+ model.setFullSyncPeriod(-1);
+ model.setName("test-ldap");
+ model.setPriority(0);
+ model.setProviderId(LDAPStorageProviderFactory.PROVIDER_NAME);
+ model.setConfig(ldapConfig);
+
+ ldapModel = appRealm.addComponentModel(model);
+ LDAPTestUtils.addZipCodeLDAPMapper(appRealm, ldapModel);
+
+ // Delete all LDAP users and add some new for testing
+ LDAPStorageProvider ldapFedProvider = LDAPTestUtils.getLdapProvider(session, ldapModel);
+ LDAPTestUtils.removeAllLDAPUsers(ldapFedProvider, appRealm);
+
+// LDAPObject john = LDAPTestUtils.addLDAPUser(ldapFedProvider, appRealm, "johnkeycloak", "John", "Doe", "john@email.org", null, "1234");
+// LDAPTestUtils.updateLDAPPassword(ldapFedProvider, john, "Password1");
+//
+// LDAPObject existing = LDAPTestUtils.addLDAPUser(ldapFedProvider, appRealm, "existing", "Existing", "Foo", "existing@email.org", null, "5678");
+
+ appRealm.getClientByClientId("test-app").setDirectAccessGrantsEnabled(true);
+ }
+ });
+
+ @ClassRule
+ public static TestRule chain = RuleChain
+ .outerRule(ldapRule)
+ .around(keycloakRule);
+
+ private static final String JPEG_PHOTO_BASE64 = "/9j/4AAQSkZJRgABAQAAAQABAAD/2wCEAAMDAwMDAwQEBAQFBQUFBQcHBgYHBwsICQgJCAsRCwwLCwwLEQ8SDw4PEg8bFRMTFRsfGhkaHyYiIiYwLTA+PlQBAwMDAwMDBAQEBAUFBQUFBwcGBgcHCwgJCAkICxELDAsLDAsRDxIPDg8SDxsVExMVGx8aGRofJiIiJjAtMD4+VP/CABEIAFIAWAMBIgACEQEDEQH/xAAdAAACAgMBAQEAAAAAAAAAAAAGCAUHAwQJAgAB/9oACAEBAAAAAElmzK1aOaraUmpiktrD10DayAIMkKunPQdk+hrZwkUNkMrM88VDtt7r7C1KCJtprbSBP2J6K+VDqUlwErkDnOm/HF9IPDaWQ+cP85sXS7OCj1Iybjj2zVvJA0b91BqJ1JuQDYfkuSWrGdFzsMsWkaIUGHh4IuY1glqtWrK8G89YeJk+ldfT1pHz9//EABoBAAIDAQEAAAAAAAAAAAAAAAUHAwYIBAD/2gAIAQIQAAAApKICefJOj5haleI6vyhC1+FEa9UydaFaD7hDLtU9jttBsCJni6f/xAAaAQADAQEBAQAAAAAAAAAAAAAFBgcDBAII/9oACAEDEAAAAF5+Mdc4X6LUuz2kyGquiYYI/PzvUctR8d5289ghjS48fuOK/wD/xAA5EAABAwIEBAMGAwcFAAAAAAABAgMEABEFBhIhEzFBURQiMgdCYXGBwSOhsRUkM3KCkdFSU2J04f/aAAgBAQABPwGHLbkIVbmjmPhSF60BSL2VUPDhLYLjL4KuotyP0NQPZq5iWBmUiWPGFarIV/DIHS/MGpeC4lhrnh5MdxLgJBuna/wPWsFyW2/kVaH2/wB7dK5LSyPMDbb6KtWD+zqBKypKTIZQJ89q4dPNG10f+1GydizuAPSSgstwlHiawQVnVby/LrTrfBOkqF6gyWmEJQSoKvfVYUiawVbrubc6bQEr4qSkKpDj/iEMKSdLoDYSm/XkRWSco+Bg4dORxo8lCyJcd1NgqxI68tjSG2Wm/wAFKUi97DYVi2GRcZh8F+9goLSR0IpC02TakqA2rE4acSgPw9WgPJ0qPYE71mL2fQJLanYiOGmJh5DKEep125IualtvxJDrDo0OtLKFjsQd6U8tJ2686deDa/Ii1/STvWU8KwDOOVoaZ8VPioV2VLQdC0WNwQRSU2QEk3sNyetPExzq9zr8KEwpulXTrTWJx0OLQp5IOskC/Sm5iFpSpCr3A/S9IeF9zQJPI2r2g5Tw3AWFS0uPOPzpay3q9KE8zc9Tvzpxv6HrWUcCh5hnmC/iQiv6bskp18XvbcVlXJE3K0xbrOJJfadFnGi2U/UG5peocqxPEmYrCtak3I2FTcckBwp1q9PIck87Hbn3oLllYWTvzvUbFpkE7XIv1+FYFizOIM60W18inqmmeIrnXtTaxmW82p2I61CYFkL2Ui6upI5XrhKS0nWL6VadQ95J7VkzKcZzEor4lsHgkL4KwSrWO1ulJdWbeUHbcjaluqAPkX+VZjxMLkur07Nk7cjYdjyvTSzJcceWLFe4+9Mt6x5eQNOBCiR8enSsqylxcSLSdR1g7X273pmVYedJT9Kx7L+G5mjBp911sp9KkqOx+KTsazDl+Rl3EBHcktvNKuptSFbEfEdCKyEGHNi+OKhZUGFNg/1JJ3BoSU99/lTuzZUs2FutZhaSZT5SggJVcG29r9bU1p8OhQuRalyHgBa4TyriWTfqayjEck4kp1CiA0j63VyplTqPKtQV89qnfsl5jhzyzw1/7igP7XrPOF4fgmMNrg4ozIjSFH8BCwtbI7E9u1ZCwvCpjrcyRKQHW3PLG1WJ+N/8U65o3sBSWCs8R3c9B2rN2BGWy7ISQDpsRb86SuRC1IlNLQb9ep+B60ZUbYnaokOXijwaYQdJ5q90bd6wDBF4JESlJC7+Zdhvc00W3htWbZmHYVhi3J8PxUVR0rb06t+l+3zrFXMLU6VQUvJJcWVBQ0pSOiUi6jt3Jpryea52r2c+KxKap9995TEVvyjWbFajtfvWsL+VKW1iBJQoKZbV03uof4qThEWWyriNJWee++9R8uYTH06YrYIH+kU9BaCNKU2t6aw93iJ0KO6axfFIOFSW1OSEsFwn1GwJHOsdbYzhlybEgy21ugC4QoG+nfSfnU6MAtY9JR7vfferqPQmsOzTOw9bDMdTLZTfhtcklZFrnufnWLe0HM+JLMbi+FSfIpDW3zuedZXQzGyxh/IDw4Wf6/Mo1h8pqbCaktnyOi6T8L1PxlqFj0OE5sJLK7fzDkKOlbYVWM4yzl7FY63dmJKTqI90o62r2pQXnDExpg8aK4gNmytkHoR8DTMx6MVLYdW2u1uo/NNL1JSt3UFq08qCVN+pR2pS9SiR32oyPElp824iLIc/5dlUjOstvLyMKCAAEFBdJ9y/Kms2QMNyvBjofC3ww2opQb8/MAfvWPY/IxzFvFqUW9B/BsfSByFSfaGpzAm+E4WZrTyF2725/Q9qx7NT+ZHY7i20t8JrTpBvc33NSJkzgmKH3THJ1cLUdN/lQTtvUSO2664FXA0LWr6C9KJJNConrP8AIaRzXTPo/t+tO/f71J9X0H6U1T1L+1QNncQ/6n3Ff//EACQQAQEAAgICAQQDAQAAAAAAAAERACExQVGBYXGRobEQwfDR/9oACAEBAAE/EFxCJHa+QcvGUwhESkN9fiYHv0VpOm0o9OFKc/OP0nyMqyNS7O0x2FTnAH6HHF+oMkQP6cPbVCpqdHusdzBsTjjKJKrAZwN411hOmhN3Q+OdYFicQWN6QTJzrwEHC7aJwbcdwysS/AW7GNhVpxF5hwesFHQhHh77IyY3UAIHQHgwuHXP36x4apQUBQcWcXJmcwJakbelduaVYQKsBS3eAVJyB+cQnvn1w6o9ZDI6pnUIiPfdxG9iCBQRXreNy18f2PjFCeB0fH3MMVGwtGz1hMB2JKPunWGBO9cLh1omlDTJZj6GlAPSm3RMCgLAgSfWedY3okyFyEYQWneraZLkQPZNfjHeyDpVdBP7dYuNwjIibANNDHmgaDAeXRptkBMDpGVE6Hqjvltx4aCVkJq9o4gOA4y6pRz7bXiMrUKoBArI5ZdacH0tDvKBFDsjg3oRL8G/tceEiW1Y6UAXW0jxK47AXAL0gOg3HkF1MeStC2JfnHWoCStw/wDe804AlIhCOmmMAu7jZ+Fw0B2U6H2hTGAuDJHQLuYV1xK4W0jhBExMKEcCV+5m01yqkMRn8VrZVUN24oNBOeYF1eI6cEGRpvfJrB+GQLVd9ZIRJROhHod4eCYNA8PTGhag8kNPJivYOADUtVxNVsC44dbHxjhJe2AYOXuc16+cQODu1ej3TF3gwCyreivI5JUksX4fesSG1rUxOwRWcCuJY2pBRrL0AQyrY62UfZjjgyEKuhQpIYLK9dtHzfVC4V2Gib2vOsI+EqqQI4gFwY1Py185FplUDmj8LU84jKWQGhYb+GYoKQdBVa8fLgkXPHI8s+uWSNh+pzh6/EJ6CF0MduPBMS2w88OjhgEW0VUC+Ht6xbrYBeevOWtwmrpdV6RSGDx83G9WqMDRe3oFCj2uUoN4FoGLoFWobz7guMGnj2W4jCzVWD1RwXq8VTsWxxafUEV2R2WOBoHSEnGyczAFEcmv43hrndLdMA4GT4cJ8xi4mEFtpKDrTMB0WxjaWEKwJpfaeX95xkQjKxHJ5FgWWx4WjvAwAztJmx6tsUyaoEOUrgVl3jsSC8V1lgV0fwbzc/pzg9R17zkOp+jGh6Z5/wDGuf2M4nvOf+9ZxOos/wD/xAAvEQABAwMCBQMDAwUAAAAAAAACAQMEAAUREiEGBxMxQSJRcSMzYRQygRUXQpGy/9oACAECAQE/AHIDLhl0i0+VQvHvXF3MCTaZ7kW0voatv+o1FP8ABcKBfhavPM+dcbZESOCsSQki49pzj0dk+Fqy81r3FkSBkfUbkzAdQjVS6I9lQUqw8SWzi6GRRjVSHTrBdiHPaisAEuUNa5mXmBDgLFC4lBubGHmCRDFT8YQsYwtQmHpkpBFOorpepPfPmoPAIq0RPvCCGKYRKv3B71pZV8fqte6ePmuVEmaxxMLDUkWmDTL45RNenslA5lNiT/aVzPmzJt7EHzJ1GQwKKONOd65cxYz9wdUw3ANqfaOQaiC6UHbHxVxYaGzTlfX6aNL37ZqG49CurT0UnWzE8obKay/hPNWtXpdqhSFdV7qsoqmrStqq+cj4WuYPB5SVfu7twJWmAXDKhnT7COKslxnW6cMtvIg0qIW22Pav7i2hwOq7EdF3VujZJiuI+MnrzDVuO30owEmtvuq/lVrh+2G1dbZLfbkDBceDW4iEmlM99SVFVhY7e6l6e5KhLTtmjXAFafQTbwuyjsua454JC4223QLXHajNhJQnhBNO2MZ/NWLl0/Luk39Q2rcQeu2Cki6s9gJK4e5ZXNqUSzxT9KXWbcHOC2T0qlcPWz+k2dmC+YPi1kQXTjLefShJ7pTUiO2pAaIqpjG2MJQft/irl+9Ka+4XwtJ9qmu4/NF99z4D/mv/xAAsEQACAQMCBAUDBQAAAAAAAAABAgMABBEFEgYhMUETFCIycVFSYRUzNGJy/9oACAEDAQE/AE09bmQ+F8kNWhcIsrs9wAYmHp+vMVYcIx291cO+JInQqoPXJ61c8MWzW6Rx5HhoQP7HHKtZ0W5tMJMMhjkN8V5B/uFcM2EodZDAjxMNpY9qtoRyIxtHUVLNawYyBQEM6+nGcVxVFB5F96bmHsI7ZokZ61wdEP0pJcDdJzPqzSnZESprWricTAktWgtNtBfJJ7GtRtYZonWRAQQeXatdsXtr90VNoxyAbIrhDXbaJYrKO1IZurA5yfqajYk7GwAwqWxikOSVNJFHbx5BBP4q9uikEojdC/hnCEg1LNHM5Zwuc9xWnRywSq8UrJjmDitP4ha2neS6kYnwiFB+7NXvFFtawxEOrTPtJUdgetapxVBDZFrdwZPSy5/PUGtS1iO7n8xEGQuPUuT1qEhgx25yerVb9Kv/AHU/8l/8VcftrSe5fmovYK//2Q==";
+
+
+
+ protected Keycloak adminClient;
+
+
+ @Before
+ public void before() {
+ adminClient = Keycloak.getInstance(AUTH_SERVER_ROOT, MASTER, ADMIN, ADMIN, Constants.ADMIN_CLI_CLIENT_ID);
+ }
+
+ @After
+ public void after() {
+ ComponentRepresentation jpegMapper = adminClient.realm("test").components().query(ldapModel.getId(), LDAPStorageMapper.class.getName(), "jpeg-mapper").get(0);
+ adminClient.realm("test").components().component(jpegMapper.getId()).remove();
+
+ adminClient.close();
+ }
+
+
+ // Test invalid mapper configuration - validation exception thrown
+ @Test
+ public void test01InvalidMapperConfiguration() {
+ KeycloakSession session = keycloakRule.startSession();
+ try {
+ RealmManager manager = new RealmManager(session);
+ RealmModel appRealm = manager.getRealm("test");
+
+ ComponentModel ldapComponentMapper = LDAPTestUtils.addUserAttributeMapper(appRealm, ldapModel, "jpeg-mapper", LDAPConstants.JPEG_PHOTO, LDAPConstants.JPEG_PHOTO);
+
+ ldapComponentMapper.put(UserAttributeLDAPStorageMapper.IS_BINARY_ATTRIBUTE, true);
+ try {
+ appRealm.updateComponent(ldapComponentMapper);
+ Assert.fail("Not expected to successfully update mapper");
+ } catch (ComponentValidationException cve) {
+ // Expected
+ }
+
+ } finally {
+ keycloakRule.stopSession(session, true);
+ }
+ }
+
+
+ @Test
+ public void test02ReadOnly() {
+ String mapperId = addPhotoMapper();
+
+ KeycloakSession session = keycloakRule.startSession();
+ try {
+ RealmManager manager = new RealmManager(session);
+ RealmModel appRealm = manager.getRealm("test");
+
+ // Add user directly to LDAP
+ LDAPStorageProvider ldapFedProvider = LDAPTestUtils.getLdapProvider(session, ldapModel);
+ addLDAPUser(ldapFedProvider, appRealm, "johnphoto", "John", "Photo", "john@photo.org", JPEG_PHOTO_BASE64);
+
+ // Set mapper to be read-only
+ ComponentModel ldapComponentMapper = appRealm.getComponent(mapperId);
+ ldapComponentMapper.put(UserAttributeLDAPStorageMapper.READ_ONLY, true);
+ appRealm.updateComponent(ldapComponentMapper);
+ } finally {
+ keycloakRule.stopSession(session, true);
+ }
+
+ // Assert john found
+ getUserAndAssertPhoto("johnphoto", true);
+ }
+
+
+ @Test
+ public void test03WritableMapper() {
+ String mapperId = addPhotoMapper();
+
+ // Create user joe with jpegPHoto
+ UserRepresentation joe = new UserRepresentation();
+ joe.setUsername("joephoto");
+ joe.setEmail("joe@photo.org");
+ joe.setAttributes(Collections.singletonMap(LDAPConstants.JPEG_PHOTO, Arrays.asList(JPEG_PHOTO_BASE64)));
+ Response response = adminClient.realm("test").users().create(joe);
+ response.close();
+
+
+ // Assert he is found including jpegPhoto
+ joe = getUserAndAssertPhoto("joephoto", true);
+
+
+ // Try to update him with some big non-LDAP mapped attribute. It will fail
+ try {
+ joe.getAttributes().put("someOtherPhoto", Arrays.asList(JPEG_PHOTO_BASE64));
+ adminClient.realm("test").users().get(joe.getId()).update(joe);
+ Assert.fail("Not expected to successfully update user");
+ } catch (ClientErrorException cee) {
+ // Expected
+ }
+
+ // Remove jpegPhoto attribute and assert it was successfully removed
+ joe.getAttributes().remove("someOtherPhoto");
+ joe.getAttributes().remove(LDAPConstants.JPEG_PHOTO);
+ adminClient.realm("test").users().get(joe.getId()).update(joe);
+ getUserAndAssertPhoto("joephoto", false);
+ }
+
+
+ private String addPhotoMapper() {
+ KeycloakSession session = keycloakRule.startSession();
+ try {
+ RealmManager manager = new RealmManager(session);
+ RealmModel appRealm = manager.getRealm("test");
+
+ ComponentModel ldapComponentMapper = LDAPTestUtils.addUserAttributeMapper(appRealm, ldapModel, "jpeg-mapper", LDAPConstants.JPEG_PHOTO, LDAPConstants.JPEG_PHOTO);
+
+ ldapComponentMapper.put(UserAttributeLDAPStorageMapper.IS_BINARY_ATTRIBUTE, true);
+ ldapComponentMapper.put(UserAttributeLDAPStorageMapper.ALWAYS_READ_VALUE_FROM_LDAP, true);
+ appRealm.updateComponent(ldapComponentMapper);
+ return ldapComponentMapper.getId();
+ } finally {
+ keycloakRule.stopSession(session, true);
+ }
+ }
+
+
+ private LDAPObject addLDAPUser(LDAPStorageProvider ldapProvider, RealmModel realm, final String username,
+ final String firstName, final String lastName, final String email, String jpegPhoto) {
+ UserModel helperUser = new UserModelDelegate(null) {
+
+ @Override
+ public String getUsername() {
+ return username;
+ }
+
+ @Override
+ public String getFirstName() {
+ return firstName;
+ }
+
+ @Override
+ public String getLastName() {
+ return lastName;
+ }
+
+ @Override
+ public String getEmail() {
+ return email;
+ }
+
+ @Override
+ public List<String> getAttribute(String name) {
+ if (LDAPConstants.JPEG_PHOTO.equals(name)) {
+ return Arrays.asList(jpegPhoto);
+ } else {
+ return Collections.emptyList();
+ }
+ }
+ };
+ return LDAPUtils.addUserToLDAP(ldapProvider, realm, helperUser);
+ }
+
+
+ private UserRepresentation getUserAndAssertPhoto(String username, boolean isPhotoExpected) {
+ List<UserRepresentation> johns = adminClient.realm("test").users().search(username, 0, 10);
+ Assert.assertEquals(1, johns.size());
+ UserRepresentation john = johns.get(0);
+ Assert.assertEquals(username, john.getUsername());
+ Assert.assertTrue(john.getAttributes().containsKey(LDAPConstants.LDAP_ID)); // Doublecheck it's the LDAP mapped user
+
+ if (isPhotoExpected) {
+ Assert.assertEquals(JPEG_PHOTO_BASE64, john.getAttributes().get(LDAPConstants.JPEG_PHOTO).get(0));
+ } else {
+ Assert.assertFalse(john.getAttributes().containsKey(LDAPConstants.JPEG_PHOTO));
+ }
+ return john;
+ }
+
+}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/ldap/LDAPGroupMapperSyncTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/ldap/LDAPGroupMapperSyncTest.java
index 29e79c4..746b302 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/ldap/LDAPGroupMapperSyncTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/ldap/LDAPGroupMapperSyncTest.java
@@ -104,8 +104,8 @@ public class LDAPGroupMapperSyncTest {
LDAPObject group11 = LDAPTestUtils.createLDAPGroup(manager.getSession(), appRealm, ldapModel, "group11");
LDAPObject group12 = LDAPTestUtils.createLDAPGroup(manager.getSession(), appRealm, ldapModel, "group12", descriptionAttrName, "group12 - description");
- LDAPUtils.addMember(ldapFedProvider, MembershipType.DN, LDAPConstants.MEMBER, group1, group11, false);
- LDAPUtils.addMember(ldapFedProvider, MembershipType.DN, LDAPConstants.MEMBER, group1, group12, true);
+ LDAPUtils.addMember(ldapFedProvider, MembershipType.DN, LDAPConstants.MEMBER, "not-used", group1, group11, false);
+ LDAPUtils.addMember(ldapFedProvider, MembershipType.DN, LDAPConstants.MEMBER, "not-used", group1, group12, true);
}
});
@@ -144,7 +144,7 @@ public class LDAPGroupMapperSyncTest {
// Add recursive group mapping to LDAP. Check that sync with preserve group inheritance will fail
LDAPObject group1 = groupMapper.loadLDAPGroupByName("group1");
LDAPObject group12 = groupMapper.loadLDAPGroupByName("group12");
- LDAPUtils.addMember(ldapProvider, MembershipType.DN, LDAPConstants.MEMBER, group12, group1, true);
+ LDAPUtils.addMember(ldapProvider, MembershipType.DN, LDAPConstants.MEMBER, "not-used", group12, group1, true);
try {
new GroupLDAPStorageMapperFactory().create(session, mapperModel).syncDataFromFederationProviderToKeycloak(realm);
@@ -171,7 +171,7 @@ public class LDAPGroupMapperSyncTest {
Assert.assertEquals("group12 - description", kcGroup12.getFirstAttribute(descriptionAttrName));
// Cleanup - remove recursive mapping in LDAP
- LDAPUtils.deleteMember(ldapProvider, MembershipType.DN, LDAPConstants.MEMBER, group12, group1);
+ LDAPUtils.deleteMember(ldapProvider, MembershipType.DN, LDAPConstants.MEMBER, "not-used", group12, group1);
} finally {
keycloakRule.stopSession(session, false);
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/ldap/LDAPGroupMapperTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/ldap/LDAPGroupMapperTest.java
index 022a076..e36f8e8 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/ldap/LDAPGroupMapperTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/ldap/LDAPGroupMapperTest.java
@@ -111,8 +111,8 @@ public class LDAPGroupMapperTest {
LDAPObject group12 = LDAPTestUtils.createLDAPGroup(manager.getSession(), appRealm, ldapModel, "group12", descriptionAttrName, "group12 - description");
LDAPObject groupSpecialCharacters = LDAPTestUtils.createLDAPGroup(manager.getSession(), appRealm, ldapModel, "group-spec,ia*l_characžter)s", descriptionAttrName, "group-special-characters");
- LDAPUtils.addMember(ldapFedProvider, MembershipType.DN, LDAPConstants.MEMBER, group1, group11, false);
- LDAPUtils.addMember(ldapFedProvider, MembershipType.DN, LDAPConstants.MEMBER, group1, group12, true);
+ LDAPUtils.addMember(ldapFedProvider, MembershipType.DN, LDAPConstants.MEMBER, "not-used", group1, group11, false);
+ LDAPUtils.addMember(ldapFedProvider, MembershipType.DN, LDAPConstants.MEMBER, "not-used", group1, group12, true);
// Sync LDAP groups to Keycloak DB
ComponentModel mapperModel = LDAPTestUtils.getSubcomponentByName(appRealm, ldapModel, "groupsMapper");
@@ -366,14 +366,14 @@ public class LDAPGroupMapperTest {
// 2 - Add one existing user rob to LDAP group
LDAPObject jamesLdap = ldapProvider.loadLDAPUserByUsername(appRealm, "jameskeycloak");
- LDAPUtils.addMember(ldapProvider, MembershipType.DN, LDAPConstants.MEMBER, group2, jamesLdap, false);
+ LDAPUtils.addMember(ldapProvider, MembershipType.DN, LDAPConstants.MEMBER, "not-used", group2, jamesLdap, false);
// 3 - Add non-existing user to LDAP group
LDAPDn nonExistentDn = LDAPDn.fromString(ldapProvider.getLdapIdentityStore().getConfig().getUsersDn());
nonExistentDn.addFirst(jamesLdap.getRdnAttributeName(), "nonexistent");
LDAPObject nonExistentLdapUser = new LDAPObject();
nonExistentLdapUser.setDn(nonExistentDn);
- LDAPUtils.addMember(ldapProvider, MembershipType.DN, LDAPConstants.MEMBER, group2, nonExistentLdapUser, true);
+ LDAPUtils.addMember(ldapProvider, MembershipType.DN, LDAPConstants.MEMBER, "not-used", group2, nonExistentLdapUser, true);
// 4 - Check group members. Just existing user rob should be present
groupMapper.syncDataFromFederationProviderToKeycloak(appRealm);
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/ldap/LDAPLegacyImportTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/ldap/LDAPLegacyImportTest.java
index 9e26fcf..a3d9178 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/ldap/LDAPLegacyImportTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/ldap/LDAPLegacyImportTest.java
@@ -69,6 +69,7 @@ import org.openqa.selenium.WebDriver;
import java.io.IOException;
import java.util.List;
+import java.util.Map;
import static org.junit.Assert.assertEquals;
@@ -80,7 +81,11 @@ import static org.junit.Assert.assertEquals;
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
public class LDAPLegacyImportTest {
- private static LDAPRule ldapRule = new LDAPRule();
+ // This test is executed just for the embedded LDAP server
+ private static LDAPRule ldapRule = new LDAPRule((Map<String, String> ldapConfig) -> {
+ String connectionURL = ldapConfig.get(LDAPConstants.CONNECTION_URL);
+ return !"ldap://localhost:10389".equals(connectionURL);
+ });
private static ComponentModel ldapModel = null;
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/ldap/LDAPMultipleAttributesTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/ldap/LDAPMultipleAttributesTest.java
index 7de1ae9..dce0f60 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/ldap/LDAPMultipleAttributesTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/ldap/LDAPMultipleAttributesTest.java
@@ -56,6 +56,7 @@ import java.util.Arrays;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
+import java.util.Map;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
@@ -66,7 +67,15 @@ public class LDAPMultipleAttributesTest {
protected String APP_SERVER_BASE_URL = "http://localhost:8081";
protected String LOGIN_URL = OIDCLoginProtocolService.authUrl(UriBuilder.fromUri(APP_SERVER_BASE_URL + "/auth")).build("test").toString();
- private static LDAPRule ldapRule = new LDAPRule();
+
+ // Skip this test on MSAD due to lack of supported user multivalued attributes
+ private static LDAPRule ldapRule = new LDAPRule((Map<String, String> ldapConfig) -> {
+
+ String vendor = ldapConfig.get(LDAPConstants.VENDOR);
+ return (vendor.equals(LDAPConstants.VENDOR_ACTIVE_DIRECTORY));
+
+ });
+
private static ComponentModel ldapModel = null;
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/SimplePerfTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/SimplePerfTest.java
new file mode 100644
index 0000000..a63b1e0
--- /dev/null
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/SimplePerfTest.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.testsuite.model;
+
+import org.junit.Ignore;
+import org.junit.Rule;
+import org.junit.Test;
+import org.keycloak.common.util.KeyUtils;
+import org.keycloak.jose.jws.JWSBuilder;
+import org.keycloak.protocol.RestartLoginCookie;
+import org.keycloak.testsuite.rule.WebResource;
+import org.keycloak.testsuite.rule.WebRule;
+import org.openqa.selenium.By;
+import org.openqa.selenium.WebDriver;
+
+import javax.crypto.SecretKey;
+import javax.crypto.spec.SecretKeySpec;
+
+import java.security.KeyPair;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+@Ignore
+public class SimplePerfTest {
+
+ @Rule
+ public WebRule webRule = new WebRule(this);
+
+ @WebResource
+ public WebDriver driver;
+
+ public static final String PORT = "8080";
+ public static final int WARMUP = 1000;
+ public static final int COUNT = 10000;
+
+ @Test
+ public void simplePerf() {
+ // Warm-up
+ for (int i = 0; i < WARMUP; i++) {
+ doLoginLogout(PORT);
+ }
+
+ long start = System.currentTimeMillis();
+ long s = start;
+
+ for (int i = 0; i < COUNT; i++) {
+ doLoginLogout(PORT);
+
+ if (i % 100 == 0) {
+ System.out.println(i + " " + (System.currentTimeMillis() - s) + " ms");
+ }
+ s = System.currentTimeMillis();
+ }
+
+ System.out.println("");
+ System.out.println("Average: " + ((System.currentTimeMillis() - start) / COUNT) + " ms");
+ System.out.println("Total: " + ((System.currentTimeMillis() - start)) + " ms");
+
+ }
+
+ private void doLoginLogout(String port) {
+ driver.navigate().to("http://localhost:" + port + "/auth/realms/master/account/");
+
+ driver.findElement(By.id("username")).sendKeys("admin");
+ driver.findElement(By.id("password")).sendKeys("admin");
+ driver.findElement(By.name("login")).click();
+
+ assertEquals("http://localhost:" + port + "/auth/realms/master/account/", driver.getCurrentUrl());
+
+ driver.findElement(By.linkText("Sign Out")).click();
+ }
+
+}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/KerberosRule.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/KerberosRule.java
index 8d46513..38a1df2 100644
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/KerberosRule.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/KerberosRule.java
@@ -36,6 +36,11 @@ public class KerberosRule extends LDAPRule {
private final String configLocation;
public KerberosRule(String configLocation) {
+ this(configLocation, null);
+ }
+
+ public KerberosRule(String configLocation, LDAPRuleCondition condition) {
+ super(condition);
this.configLocation = configLocation;
// Global kerberos configuration
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/LDAPRule.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/LDAPRule.java
index 4bf0e4c..6dbc938 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/LDAPRule.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/LDAPRule.java
@@ -17,7 +17,10 @@
package org.keycloak.testsuite.rule;
-import org.junit.rules.ExternalResource;
+import org.jboss.logging.Logger;
+import org.junit.rules.TestRule;
+import org.junit.runner.Description;
+import org.junit.runners.model.Statement;
import org.keycloak.testsuite.federation.ldap.LDAPTestConfiguration;
import org.keycloak.util.ldap.LDAPEmbeddedServer;
@@ -25,28 +28,76 @@ import java.util.Map;
import java.util.Properties;
/**
+ * This rule handles:
+ * - Reading of LDAP configuration from properties file
+ * - Eventually start+stop of LDAP embedded server.
+ * - Eventually allows to ignore the test if particular condition is not met. This allows to run specific tests just for some LDAP vendors
+ *
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
-public class LDAPRule extends ExternalResource {
+public class LDAPRule implements TestRule {
+
+ private static final Logger logger = Logger.getLogger(LDAPRule.class);
public static final String LDAP_CONNECTION_PROPERTIES_LOCATION = "ldap/ldap-connection.properties";
protected LDAPTestConfiguration ldapTestConfiguration;
protected LDAPEmbeddedServer ldapEmbeddedServer;
+ private final LDAPRuleCondition condition;
+
+
+ public LDAPRule() {
+ this(null);
+ }
+
+ public LDAPRule(LDAPRuleCondition condition) {
+ this.condition = condition;
+ }
+
+
@Override
- protected void before() throws Throwable {
+ public Statement apply(Statement base, Description description) {
+ return new Statement() {
+
+ @Override
+ public void evaluate() throws Throwable {
+ boolean skipTest = before();
+
+ if (skipTest) {
+ logger.infof("Skip %s due to LDAPRuleCondition not met", description.getDisplayName());
+ return;
+ }
+
+ try {
+ base.evaluate();
+ } finally {
+ after();
+ }
+ }
+ };
+ }
+
+
+ // Return true if test should be skipped
+ protected boolean before() throws Throwable {
String connectionPropsLocation = getConnectionPropertiesLocation();
ldapTestConfiguration = LDAPTestConfiguration.readConfiguration(connectionPropsLocation);
+ if (condition != null && condition.skipTest(ldapTestConfiguration.getLDAPConfig())) {
+ return true;
+ }
+
if (ldapTestConfiguration.isStartEmbeddedLdapLerver()) {
ldapEmbeddedServer = createServer();
ldapEmbeddedServer.init();
ldapEmbeddedServer.start();
}
+
+ return false;
}
- @Override
+
protected void after() {
try {
if (ldapEmbeddedServer != null) {
@@ -78,4 +129,12 @@ public class LDAPRule extends ExternalResource {
public int getSleepTime() {
return ldapTestConfiguration.getSleepTime();
}
+
+
+ // Allows to skip particular LDAP test just under specific conditions (eg. some test running just on Active Directory)
+ public interface LDAPRuleCondition {
+
+ boolean skipTest(Map<String, String> ldapConfig);
+
+ }
}
diff --git a/testsuite/integration/src/test/resources/adapter-test/product-autodetect-bearer-only-keycloak.json b/testsuite/integration/src/test/resources/adapter-test/product-autodetect-bearer-only-keycloak.json
new file mode 100644
index 0000000..b92abc6
--- /dev/null
+++ b/testsuite/integration/src/test/resources/adapter-test/product-autodetect-bearer-only-keycloak.json
@@ -0,0 +1,11 @@
+{
+ "realm" : "demo",
+ "resource" : "product-portal",
+ "realm-public-key" : "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB",
+ "auth-server-url" : "http://localhost:8081/auth",
+ "ssl-required" : "external",
+ "credentials" : {
+ "secret": "password"
+ },
+ "autodetect-bearer-only" : true
+}
diff --git a/testsuite/integration/src/test/resources/log4j.properties b/testsuite/integration/src/test/resources/log4j.properties
index 2fa1d70..4cc1f91 100755
--- a/testsuite/integration/src/test/resources/log4j.properties
+++ b/testsuite/integration/src/test/resources/log4j.properties
@@ -58,7 +58,10 @@ log4j.logger.org.keycloak.connections.jpa.DefaultJpaConnectionProviderFactory=${
log4j.logger.org.keycloak.connections.jpa.HibernateStatsReporter=debug
# Enable to view ldap logging
-# log4j.logger.org.keycloak.federation.ldap=trace
+# log4j.logger.org.keycloak.storage.ldap=trace
+
+# Enable to view queries to LDAP
+# log4j.logger.org.keycloak.storage.ldap.idm.store.ldap.LDAPIdentityStore=trace
# Enable to view kerberos/spnego logging
# log4j.logger.org.keycloak.federation.kerberos=trace
diff --git a/testsuite/integration/src/test/resources/META-INF/keycloak-server.json b/testsuite/integration/src/test/resources/META-INF/keycloak-server.json
index 3f4ddd1..27e9f5e 100755
--- a/testsuite/integration/src/test/resources/META-INF/keycloak-server.json
+++ b/testsuite/integration/src/test/resources/META-INF/keycloak-server.json
@@ -64,7 +64,7 @@
"connectionsJpa": {
"default": {
- "url": "${keycloak.connectionsJpa.url:jdbc:h2:mem:test}",
+ "url": "${keycloak.connectionsJpa.url:jdbc:h2:mem:test;DB_CLOSE_DELAY=-1}",
"driver": "${keycloak.connectionsJpa.driver:org.h2.Driver}",
"driverDialect": "${keycloak.connectionsJpa.driverDialect:}",
"user": "${keycloak.connectionsJpa.user:sa}",
diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/authorization/TestPolicyProviderFactory.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/authorization/TestPolicyProviderFactory.java
index 1fa5fc7..b3a0475 100644
--- a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/authorization/TestPolicyProviderFactory.java
+++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/authorization/TestPolicyProviderFactory.java
@@ -18,7 +18,6 @@ package org.keycloak.testsuite.authorization;
import org.keycloak.Config;
import org.keycloak.authorization.AuthorizationProvider;
-import org.keycloak.authorization.model.Policy;
import org.keycloak.authorization.model.ResourceServer;
import org.keycloak.authorization.policy.evaluation.Evaluation;
import org.keycloak.authorization.policy.provider.PolicyProvider;
@@ -43,8 +42,8 @@ public class TestPolicyProviderFactory implements PolicyProviderFactory {
}
@Override
- public PolicyProvider create(Policy policy, AuthorizationProvider authorization) {
- return new TestPolicyProvider(policy, authorization);
+ public PolicyProvider create(AuthorizationProvider authorization) {
+ return new TestPolicyProvider(authorization);
}
@Override
@@ -79,11 +78,9 @@ public class TestPolicyProviderFactory implements PolicyProviderFactory {
private class TestPolicyProvider implements PolicyProvider {
- private final Policy policy;
private final AuthorizationProvider authorization;
- public TestPolicyProvider(Policy policy, AuthorizationProvider authorization) {
- this.policy = policy;
+ public TestPolicyProvider(AuthorizationProvider authorization) {
this.authorization = authorization;
}
diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestingResourceProvider.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestingResourceProvider.java
index 75838c5..28e685e 100644
--- a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestingResourceProvider.java
+++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestingResourceProvider.java
@@ -20,6 +20,7 @@ package org.keycloak.testsuite.rest;
import org.infinispan.Cache;
import org.jboss.resteasy.annotations.cache.NoCache;
import org.jboss.resteasy.spi.BadRequestException;
+import org.keycloak.common.util.MultivaluedHashMap;
import org.keycloak.common.util.Time;
import org.keycloak.component.ComponentModel;
import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
@@ -634,6 +635,14 @@ public class TestingResourceProvider implements RealmResourceProvider {
}
@GET
+ @Path("/component")
+ @Produces(MediaType.APPLICATION_JSON)
+ public MultivaluedHashMap<String, String> getComponentConfig(@QueryParam("componentId") String componentId) {
+ RealmModel realm = session.getContext().getRealm();
+ return realm.getComponent(componentId).getConfig();
+ }
+
+ @GET
@Path("/smtp-config")
@Produces(MediaType.APPLICATION_JSON)
public Map<String, String> getSmtpConfig() {
diff --git a/testsuite/integration-arquillian/test-apps/photoz/photoz-html5-client/src/main/webapp/index.html b/testsuite/integration-arquillian/test-apps/photoz/photoz-html5-client/src/main/webapp/index.html
index bec9fae..077f9dc 100755
--- a/testsuite/integration-arquillian/test-apps/photoz/photoz-html5-client/src/main/webapp/index.html
+++ b/testsuite/integration-arquillian/test-apps/photoz/photoz-html5-client/src/main/webapp/index.html
@@ -19,7 +19,12 @@
<body data-ng-controller="TokenCtrl">
-<a href data-ng-click="showRpt()">Show Requesting Party Token </a> | <a href data-ng-click="showAccessToken()">Show Access Token </a> | <a href data-ng-click="requestEntitlements()">Request Entitlements</a> | <a href="" ng-click="Identity.logout()">Sign Out</a>
+<!--<a href data-ng-click="showRpt()">Show Requesting Party Token </a> | <a href data-ng-click="showAccessToken()">Show Access Token </a> | <a href data-ng-click="requestEntitlements()">Request Entitlements</a> | <a href data-ng-click="requestEntitlement()">Request Entitlement</a> |<a href="" ng-click="Identity.logout()">Sign Out</a>-->
+<a href data-ng-click="showRpt()">Show Requesting Party Token </a> |
+<a href data-ng-click="showAccessToken()">Show Access Token </a> |
+<a id="entitlements" href data-ng-click="requestEntitlements()">Request Entitlements</a> |
+<a id="entitlement" href data-ng-click="requestEntitlement()">Request Entitlement</a> |
+<a href="" ng-click="Identity.logout()">Sign Out</a>
<div id="content-area" class="col-md-9" role="main">
<div id="content" ng-view/>
diff --git a/testsuite/integration-arquillian/test-apps/photoz/photoz-html5-client/src/main/webapp/js/app.js b/testsuite/integration-arquillian/test-apps/photoz/photoz-html5-client/src/main/webapp/js/app.js
index e58c5f5..bf71b43 100755
--- a/testsuite/integration-arquillian/test-apps/photoz/photoz-html5-client/src/main/webapp/js/app.js
+++ b/testsuite/integration-arquillian/test-apps/photoz/photoz-html5-client/src/main/webapp/js/app.js
@@ -62,7 +62,16 @@ module.controller('TokenCtrl', function ($scope, Identity) {
}
$scope.requestEntitlements = function () {
- Identity.authorization.entitlement('photoz-restful-api').then(function (rpt) {});
+ Identity.authorization.entitlement('photoz-restful-api').then(function (rpt) {
+ document.getElementById("output").innerHTML = JSON.stringify(jwt_decode(rpt), null, ' ');
+ });
+ }
+
+ $scope.requestEntitlement = function () {
+ var param={"permissions" : [{"resource_set_name" : "Album Resource"}]};
+ Identity.authorization.entitlement('photoz-restful-api', param).then(function (rpt) {
+ document.getElementById("output").innerHTML = JSON.stringify(jwt_decode(rpt), null, ' ');
+ });
}
$scope.Identity = Identity;
diff --git a/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api-authz-service.json b/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api-authz-service.json
index 1ce85dd..3807df7 100644
--- a/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api-authz-service.json
+++ b/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api-authz-service.json
@@ -47,7 +47,7 @@
"logic": "POSITIVE",
"decisionStrategy": "UNANIMOUS",
"config": {
- "mavenArtifactVersion": "2.1.0-SNAPSHOT",
+ "mavenArtifactVersion": "2.5.0.Final-SNAPSHOT",
"mavenArtifactId": "photoz-authz-policy",
"sessionName": "MainOwnerSession",
"mavenArtifactGroupId": "org.keycloak.testsuite",
diff --git a/testsuite/integration-arquillian/test-apps/servlet-authz/src/main/webapp/WEB-INF/web.xml b/testsuite/integration-arquillian/test-apps/servlet-authz/src/main/webapp/WEB-INF/web.xml
index 14d0615..2378ca5 100644
--- a/testsuite/integration-arquillian/test-apps/servlet-authz/src/main/webapp/WEB-INF/web.xml
+++ b/testsuite/integration-arquillian/test-apps/servlet-authz/src/main/webapp/WEB-INF/web.xml
@@ -13,16 +13,8 @@
</web-resource-collection>
<auth-constraint>
<role-name>user</role-name>
- </auth-constraint>
- </security-constraint>
-
- <security-constraint>
- <web-resource-collection>
- <web-resource-name>All Resources</web-resource-name>
- <url-pattern>/*</url-pattern>
- </web-resource-collection>
- <auth-constraint>
<role-name>admin</role-name>
+ <role-name>user_premium</role-name>
</auth-constraint>
</security-constraint>
@@ -39,6 +31,10 @@
<role-name>user</role-name>
</security-role>
+ <security-role>
+ <role-name>user_premium</role-name>
+ </security-role>
+
<error-page>
<error-code>403</error-code>
<location>/accessDenied.jsp</location>
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/PhotozClientAuthzTestApp.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/PhotozClientAuthzTestApp.java
index fe98f0d..b721166 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/PhotozClientAuthzTestApp.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/PhotozClientAuthzTestApp.java
@@ -54,7 +54,13 @@ public class PhotozClientAuthzTestApp extends AbstractPageWithInjectedUrl {
@FindBy(xpath = "//a[@ng-click = 'Identity.logout()']")
WebElement signOutButton;
-
+
+ @FindBy(id = "entitlement")
+ WebElement entitlement;
+
+ @FindBy(id = "entitlements")
+ WebElement entitlements;
+
public void createAlbum(String name) {
navigateTo();
this.driver.findElement(By.id("create-album")).click();
@@ -85,6 +91,16 @@ public class PhotozClientAuthzTestApp extends AbstractPageWithInjectedUrl {
signOutButton.click(); // Sometimes doesn't work in PhantomJS!
pause(WAIT_AFTER_OPERATION);
}
+
+ public void requestEntitlement() {
+ entitlement.click();
+ pause(WAIT_AFTER_OPERATION);
+ }
+
+ public void requestEntitlements() {
+ entitlements.click();
+ pause(WAIT_AFTER_OPERATION);
+ }
public void login(String username, String password, String... scopes) {
if (scopes.length > 0) {
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/provider/URLProvider.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/provider/URLProvider.java
index 77640aa..e8852bc 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/provider/URLProvider.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/provider/URLProvider.java
@@ -74,7 +74,7 @@ public class URLProvider extends URLResourceProvider {
}
try {
- if (System.getProperty("app.server","").startsWith("eap6")) {
+ if (System.getProperty("app.server.management.protocol","").equals("remote")) {
if (url == null) {
url = new URL("http://localhost:8080/");
}
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/cli/exec/AbstractExec.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/cli/exec/AbstractExec.java
new file mode 100644
index 0000000..91fcb13
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/cli/exec/AbstractExec.java
@@ -0,0 +1,245 @@
+package org.keycloak.testsuite.cli.exec;
+
+import org.keycloak.testsuite.cli.OsArch;
+import org.keycloak.testsuite.cli.OsUtils;
+
+import java.io.BufferedReader;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public abstract class AbstractExec {
+
+ public static final String WORK_DIR = System.getProperty("user.dir");
+
+ public static final OsArch OS_ARCH = OsUtils.determineOSAndArch();
+
+ private long waitTimeout = 30000;
+
+ private Process process;
+
+ private int exitCode = -1;
+
+ private boolean logStreams = Boolean.valueOf(System.getProperty("cli.log.output", "true"));
+
+ protected boolean dumpStreams;
+
+ protected String workDir = WORK_DIR;
+
+ private String env;
+
+ private String argsLine;
+
+ private ByteArrayOutputStream stdout = new ByteArrayOutputStream();
+
+ private ByteArrayOutputStream stderr = new ByteArrayOutputStream();
+
+ private InputStream stdin = new InteractiveInputStream();
+
+ private Throwable err;
+
+ private Thread stdoutRunner;
+
+ private Thread stderrRunner;
+
+ public AbstractExec(String workDir, String argsLine, InputStream stdin) {
+ this(workDir, argsLine, null, stdin);
+ }
+
+ public AbstractExec(String workDir, String argsLine, String env, InputStream stdin) {
+ if (workDir != null) {
+ this.workDir = workDir;
+ }
+
+ this.argsLine = argsLine;
+ this.env = env;
+
+ if (stdin != null) {
+ this.stdin = stdin;
+ }
+ }
+
+ public abstract String getCmd();
+
+ public void execute() {
+ executeAsync();
+ if (err == null) {
+ waitCompletion();
+ }
+ }
+
+
+ public void executeAsync() {
+
+ try {
+ if (OS_ARCH.isWindows()) {
+ String cmd = (env != null ? "set " + env + " & " : "") + fixPath(getCmd()) + " " + fixQuotes(argsLine);
+ System.out.println("Executing: cmd.exe /c " + cmd);
+ process = Runtime.getRuntime().exec(new String[]{"cmd.exe", "/c", cmd}, null, new File(workDir));
+ } else {
+ String cmd = (env != null ? env + " " : "") + getCmd() + " " + argsLine;
+ System.out.println("Executing: sh -c " + cmd);
+ process = Runtime.getRuntime().exec(new String[]{"sh", "-c", cmd}, null, new File(workDir));
+ }
+
+ stdoutRunner = new StreamReaderThread(process.getInputStream(), logStreams ? new LoggingOutputStream("STDOUT", stdout) : stdout);
+ stdoutRunner.start();
+
+ stderrRunner = new StreamReaderThread(process.getErrorStream(), logStreams ? new LoggingOutputStream("STDERR", stderr) : stderr);
+ stderrRunner.start();
+
+ new StreamReaderThread(stdin, process.getOutputStream())
+ .start();
+
+ } catch (Throwable t) {
+ err = t;
+ }
+ }
+
+ private String fixPath(String cmd) {
+ return cmd.replaceAll("/", "\\\\");
+ }
+
+ private String fixQuotes(String argsLine) {
+ argsLine = argsLine + " ";
+ argsLine = argsLine.replaceAll("\"", "\\\\\"");
+ argsLine = argsLine.replaceAll(" '", " \"");
+ argsLine = argsLine.replaceAll("' ", "\" ");
+ return argsLine;
+ }
+
+ public void waitCompletion() {
+
+ // This is necessary to make sure the process isn't stuck reading from stdin
+ if (stdin instanceof InteractiveInputStream) {
+ ((InteractiveInputStream) stdin).close();
+ }
+ try {
+ if (process.waitFor(waitTimeout, TimeUnit.MILLISECONDS)) {
+ exitCode = process.exitValue();
+ if (exitCode != 0) {
+ dumpStreams = true;
+ }
+ // make sure reading output is really done (just in case)
+ stdoutRunner.join(5000);
+ stderrRunner.join(5000);
+ } else {
+ if (process.isAlive()) {
+ process.destroyForcibly();
+ }
+ throw new RuntimeException("Timeout after " + (waitTimeout / 1000) + " seconds.");
+ }
+ } catch (InterruptedException e) {
+ dumpStreams = true;
+ throw new RuntimeException("Interrupted ...", e);
+ } catch (Throwable t) {
+ dumpStreams = true;
+ err = t;
+ } finally {
+ if (!logStreams && dumpStreams) try {
+ System.out.println("STDOUT: ");
+ copyStream(new ByteArrayInputStream(stdout.toByteArray()), System.out);
+ System.out.println("STDERR: ");
+ copyStream(new ByteArrayInputStream(stderr.toByteArray()), System.out);
+ } catch (Exception ignored) {
+ }
+ }
+ }
+
+ public int exitCode() {
+ return exitCode;
+ }
+
+ public Throwable error() {
+ return err;
+ }
+
+ public InputStream stdout() {
+ return new ByteArrayInputStream(stdout.toByteArray());
+ }
+
+ public List<String> stdoutLines() {
+ return parseStreamAsLines(new ByteArrayInputStream(stdout.toByteArray()));
+ }
+
+ public String stdoutString() {
+ return new String(stdout.toByteArray());
+ }
+
+ public InputStream stderr() {
+ return new ByteArrayInputStream(stderr.toByteArray());
+ }
+
+ public List<String> stderrLines() {
+ return parseStreamAsLines(new ByteArrayInputStream(stderr.toByteArray()));
+ }
+
+ public String stderrString() {
+ return new String(stderr.toByteArray());
+ }
+
+ static List<String> parseStreamAsLines(InputStream stream) {
+ List<String> lines = new ArrayList<>();
+ try {
+ BufferedReader reader = new BufferedReader(new InputStreamReader(stream));
+
+ String line;
+ while ((line = reader.readLine()) != null) {
+ lines.add(line);
+ }
+ return lines;
+ } catch (IOException e) {
+ throw new RuntimeException("Unexpected I/O error", e);
+ }
+ }
+
+ public void waitForStdout(String content) {
+ long start = System.currentTimeMillis();
+ while (System.currentTimeMillis() - start < waitTimeout) {
+ if (stdoutString().indexOf(content) != -1) {
+ return;
+ }
+ try {
+ Thread.sleep(10);
+ } catch (InterruptedException e) {
+ throw new RuntimeException("Interrupted ...", e);
+ }
+ }
+
+ throw new RuntimeException("Timed while waiting for content to appear in stdout");
+ }
+
+ public void sendToStdin(String s) {
+ if (stdin instanceof InteractiveInputStream) {
+ ((InteractiveInputStream) stdin).pushBytes(s.getBytes());
+ } else {
+ throw new RuntimeException("Can't push to stdin - not interactive");
+ }
+ }
+
+
+
+ static void copyStream(InputStream is, OutputStream os) throws IOException {
+ byte [] buf = new byte[8192];
+
+ try (InputStream iss = is) {
+ int c;
+ while ((c = iss.read(buf)) != -1) {
+ os.write(buf, 0, c);
+ os.flush();
+ }
+ }
+ }
+
+
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/cli/exec/AbstractExecBuilder.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/cli/exec/AbstractExecBuilder.java
new file mode 100644
index 0000000..97ec772
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/cli/exec/AbstractExecBuilder.java
@@ -0,0 +1,44 @@
+package org.keycloak.testsuite.cli.exec;
+
+import java.io.InputStream;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public abstract class AbstractExecBuilder<T> {
+
+ protected String workDir;
+ protected String argsLine;
+ protected InputStream stdin;
+ protected String env;
+ protected boolean dumpStreams;
+
+ public AbstractExecBuilder<T> workDir(String path) {
+ this.workDir = path;
+ return this;
+ }
+
+ public AbstractExecBuilder<T> argsLine(String cmd) {
+ this.argsLine = cmd;
+ return this;
+ }
+
+ public AbstractExecBuilder<T> stdin(InputStream is) {
+ this.stdin = is;
+ return this;
+ }
+
+ public AbstractExecBuilder<T> env(String env) {
+ this.env = env;
+ return this;
+ }
+
+ public AbstractExecBuilder<T> fullStreamDump() {
+ this.dumpStreams = true;
+ return this;
+ }
+
+ public abstract T execute();
+
+ public abstract T executeAsync();
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/cli/exec/InteractiveInputStream.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/cli/exec/InteractiveInputStream.java
new file mode 100644
index 0000000..cbbea72
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/cli/exec/InteractiveInputStream.java
@@ -0,0 +1,120 @@
+package org.keycloak.testsuite.cli.exec;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InterruptedIOException;
+import java.util.LinkedList;
+
+class InteractiveInputStream extends InputStream {
+
+ private LinkedList<Byte> queue = new LinkedList<>();
+
+ private Thread consumer;
+
+ private boolean closed;
+
+ @Override
+ public int read(byte b[]) throws IOException {
+ return read(b, 0, b.length);
+ }
+
+ @Override
+ public synchronized int read(byte[] b, int off, int len) throws IOException {
+
+ Byte current = null;
+ int rc = 0;
+ try {
+ consumer = Thread.currentThread();
+
+ do {
+ current = queue.poll();
+ if (current == null) {
+ if (rc > 0) {
+ return rc;
+ } else {
+ do {
+ if (closed) {
+ return -1;
+ }
+ wait();
+ }
+ while ((current = queue.poll()) == null);
+ }
+ }
+
+ b[off + rc] = current;
+ rc++;
+ } while (rc < len);
+
+ } catch (InterruptedException e) {
+ throw new InterruptedIOException("Signalled to exit");
+ } finally {
+ consumer = null;
+ }
+ return rc;
+ }
+
+ @Override
+ public long skip(long n) throws IOException {
+ return super.skip(n);
+ }
+
+ @Override
+ public int available() throws IOException {
+ return super.available();
+ }
+
+ @Override
+ public synchronized void mark(int readlimit) {
+ super.mark(readlimit);
+ }
+
+ @Override
+ public synchronized void reset() throws IOException {
+ super.reset();
+ }
+
+ @Override
+ public boolean markSupported() {
+ return super.markSupported();
+ }
+
+ @Override
+ public synchronized int read() throws IOException {
+ // when input is available pass it on
+ Byte current;
+ try {
+ consumer = Thread.currentThread();
+
+ while ((current = queue.poll()) == null) {
+ // we don't check for closed before making sure
+ // that there is nothing more to read
+ if (closed) {
+ return -1;
+ }
+ wait();
+ }
+
+ } catch (InterruptedException e) {
+ throw new InterruptedIOException("Signalled to exit");
+ } finally {
+ consumer = null;
+ }
+ return current;
+ }
+
+ @Override
+ public synchronized void close() {
+ closed = true;
+ if (consumer != null) {
+ consumer.interrupt();
+ }
+ }
+
+ public synchronized void pushBytes(byte [] buff) {
+ for (byte b : buff) {
+ queue.add(b);
+ }
+ notify();
+ }
+}
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/cli/exec/LoggingOutputStream.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/cli/exec/LoggingOutputStream.java
new file mode 100644
index 0000000..e13fbe3
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/cli/exec/LoggingOutputStream.java
@@ -0,0 +1,53 @@
+package org.keycloak.testsuite.cli.exec;
+
+import java.io.ByteArrayOutputStream;
+import java.io.FilterOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+
+class LoggingOutputStream extends FilterOutputStream {
+
+ private ByteArrayOutputStream buffer = new ByteArrayOutputStream();
+ private String name;
+
+ public LoggingOutputStream(String name, OutputStream os) {
+ super(os);
+ this.name = name;
+ }
+
+ @Override
+ public void write(int b) throws IOException {
+ super.write(b);
+ if (b == 10) {
+ log();
+ } else {
+ buffer.write(b);
+ }
+ }
+
+ @Override
+ public void write(byte[] buf) throws IOException {
+ write(buf, 0, buf.length);
+ }
+
+ @Override
+ public void write(byte[] buf, int offs, int len) throws IOException {
+ for (int i = 0; i < len; i++) {
+ write(buf[offs+i]);
+ }
+ }
+
+ @Override
+ public void close() throws IOException {
+ super.close();
+ if (buffer.size() > 0) {
+ log();
+ }
+ }
+
+ private void log() {
+ String log = new String(buffer.toByteArray());
+ buffer.reset();
+ System.out.println("[" + name + "] " + log);
+ }
+}
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/cli/exec/NullInputStream.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/cli/exec/NullInputStream.java
new file mode 100644
index 0000000..cb34314
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/cli/exec/NullInputStream.java
@@ -0,0 +1,12 @@
+package org.keycloak.testsuite.cli.exec;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+class NullInputStream extends InputStream {
+
+ @Override
+ public int read() throws IOException {
+ return -1;
+ }
+}
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/cli/exec/StreamReaderThread.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/cli/exec/StreamReaderThread.java
new file mode 100644
index 0000000..c7518d5
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/cli/exec/StreamReaderThread.java
@@ -0,0 +1,33 @@
+package org.keycloak.testsuite.cli.exec;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+import static org.keycloak.testsuite.cli.exec.AbstractExec.copyStream;
+
+class StreamReaderThread extends Thread {
+
+ private InputStream is;
+ private OutputStream os;
+
+ StreamReaderThread(InputStream is, OutputStream os) {
+ this.is = is;
+ this.os = os;
+ }
+
+ public void run() {
+ try {
+ copyStream(is, os);
+ } catch (IOException e) {
+ throw new RuntimeException("Unexpected I/O error", e);
+ } finally {
+ try {
+ os.close();
+ } catch (IOException ignored) {
+ System.err.print("IGNORED: error while closing output stream: ");
+ ignored.printStackTrace();
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/cli/KcAdmExec.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/cli/KcAdmExec.java
new file mode 100644
index 0000000..3ab6b34
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/cli/KcAdmExec.java
@@ -0,0 +1,58 @@
+package org.keycloak.testsuite.cli;
+
+import org.keycloak.testsuite.cli.exec.AbstractExec;
+import org.keycloak.testsuite.cli.exec.AbstractExecBuilder;
+
+import java.io.InputStream;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public class KcAdmExec extends AbstractExec {
+
+ public static final String WORK_DIR = System.getProperty("user.dir") + "/target/containers/keycloak-client-tools";
+
+ public static final String CMD = OS_ARCH.isWindows() ? "kcadm.bat" : "kcadm.sh";
+
+ private KcAdmExec(String workDir, String argsLine, InputStream stdin) {
+ this(workDir, argsLine, null, stdin);
+ }
+
+ private KcAdmExec(String workDir, String argsLine, String env, InputStream stdin) {
+ super(workDir, argsLine, env, stdin);
+ }
+
+ @Override
+ public String getCmd() {
+ return "bin/" + CMD;
+ }
+
+ public static KcAdmExec.Builder newBuilder() {
+ return (KcAdmExec.Builder) new KcAdmExec.Builder().workDir(WORK_DIR);
+ }
+
+ public static KcAdmExec execute(String args) {
+ return newBuilder()
+ .argsLine(args)
+ .execute();
+ }
+
+ public static class Builder extends AbstractExecBuilder<KcAdmExec> {
+
+ @Override
+ public KcAdmExec execute() {
+ KcAdmExec exe = new KcAdmExec(workDir, argsLine, env, stdin);
+ exe.dumpStreams = dumpStreams;
+ exe.execute();
+ return exe;
+ }
+
+ @Override
+ public KcAdmExec executeAsync() {
+ KcAdmExec exe = new KcAdmExec(workDir, argsLine, env, stdin);
+ exe.dumpStreams = dumpStreams;
+ exe.executeAsync();
+ return exe;
+ }
+ }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/cli/KcRegExec.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/cli/KcRegExec.java
index 5f20c2a..56039af 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/cli/KcRegExec.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/cli/KcRegExec.java
@@ -1,5 +1,8 @@
package org.keycloak.testsuite.cli;
+import org.keycloak.testsuite.cli.exec.AbstractExec;
+import org.keycloak.testsuite.cli.exec.AbstractExecBuilder;
+
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
@@ -18,57 +21,27 @@ import java.util.concurrent.TimeUnit;
/**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
-public class KcRegExec {
+public class KcRegExec extends AbstractExec {
public static final String WORK_DIR = System.getProperty("user.dir") + "/target/containers/keycloak-client-tools";
- public static final OsArch OS_ARCH = OsUtils.determineOSAndArch();
-
public static final String CMD = OS_ARCH.isWindows() ? "kcreg.bat" : "kcreg.sh";
- private long waitTimeout = 30000;
-
- private Process process;
-
- private int exitCode = -1;
-
- private boolean logStreams = Boolean.valueOf(System.getProperty("cli.log.output", "true"));
-
- private boolean dumpStreams;
-
- private String workDir = WORK_DIR;
-
- private String env;
-
- private String argsLine;
-
- private ByteArrayOutputStream stdout = new ByteArrayOutputStream();
-
- private ByteArrayOutputStream stderr = new ByteArrayOutputStream();
-
- private InputStream stdin = new InteractiveInputStream();
-
- private Throwable err;
-
private KcRegExec(String workDir, String argsLine, InputStream stdin) {
this(workDir, argsLine, null, stdin);
}
private KcRegExec(String workDir, String argsLine, String env, InputStream stdin) {
- if (workDir != null) {
- this.workDir = workDir;
- }
-
- this.argsLine = argsLine;
- this.env = env;
+ super(workDir, argsLine, env, stdin);
+ }
- if (stdin != null) {
- this.stdin = stdin;
- }
+ @Override
+ public String getCmd() {
+ return "bin/" + CMD;
}
- public static Builder newBuilder() {
- return new Builder();
+ public static KcRegExec.Builder newBuilder() {
+ return (KcRegExec.Builder) new KcRegExec.Builder().workDir(WORK_DIR);
}
public static KcRegExec execute(String args) {
@@ -77,225 +50,9 @@ public class KcRegExec {
.execute();
}
- public void execute() {
- executeAsync();
- if (err == null) {
- waitCompletion();
- }
- }
-
-
- public void executeAsync() {
-
- try {
- if (OS_ARCH.isWindows()) {
- String cmd = (env != null ? "set " + env + " & " : "") + "bin\\" + CMD + " " + fixQuotes(argsLine);
- System.out.println("Executing: cmd.exe /c " + cmd);
- process = Runtime.getRuntime().exec(new String[]{"cmd.exe", "/c", cmd}, null, new File(workDir));
- } else {
- String cmd = (env != null ? env + " " : "") + "bin/" + CMD + " " + argsLine;
- System.out.println("Executing: sh -c " + cmd);
- process = Runtime.getRuntime().exec(new String[]{"sh", "-c", cmd}, null, new File(workDir));
- }
-
- new StreamReaderThread(process.getInputStream(), logStreams ? new LoggingOutputStream("STDOUT", stdout) : stdout)
- .start();
-
- new StreamReaderThread(process.getErrorStream(), logStreams ? new LoggingOutputStream("STDERR", stderr) : stderr)
- .start();
-
- new StreamReaderThread(stdin, process.getOutputStream())
- .start();
-
- } catch (Throwable t) {
- err = t;
- }
- }
-
- private String fixQuotes(String argsLine) {
- argsLine = argsLine + " ";
- argsLine = argsLine.replaceAll("\"", "\\\\\"");
- argsLine = argsLine.replaceAll(" '", " \"");
- argsLine = argsLine.replaceAll("' ", "\" ");
- return argsLine;
- }
-
- public void waitCompletion() {
-
- //if (stdin instanceof InteractiveInputStream) {
- // ((InteractiveInputStream) stdin).close();
- //}
- try {
- if (process.waitFor(waitTimeout, TimeUnit.MILLISECONDS)) {
- exitCode = process.exitValue();
- if (exitCode != 0) {
- dumpStreams = true;
- }
- } else {
- if (process.isAlive()) {
- process.destroyForcibly();
- }
- throw new RuntimeException("Timeout after " + (waitTimeout / 1000) + " seconds.");
- }
- } catch (InterruptedException e) {
- dumpStreams = true;
- throw new RuntimeException("Interrupted ...", e);
- } catch (Throwable t) {
- dumpStreams = true;
- err = t;
- } finally {
- if (!logStreams && dumpStreams) try {
- System.out.println("STDOUT: ");
- copyStream(new ByteArrayInputStream(stdout.toByteArray()), System.out);
- System.out.println("STDERR: ");
- copyStream(new ByteArrayInputStream(stderr.toByteArray()), System.out);
- } catch (Exception ignored) {
- }
- }
- }
-
- public int exitCode() {
- return exitCode;
- }
-
- public Throwable error() {
- return err;
- }
-
- public InputStream stdout() {
- return new ByteArrayInputStream(stdout.toByteArray());
- }
-
- public List<String> stdoutLines() {
- return parseStreamAsLines(new ByteArrayInputStream(stdout.toByteArray()));
- }
-
- public String stdoutString() {
- return new String(stdout.toByteArray());
- }
-
- public InputStream stderr() {
- return new ByteArrayInputStream(stderr.toByteArray());
- }
-
- public List<String> stderrLines() {
- return parseStreamAsLines(new ByteArrayInputStream(stderr.toByteArray()));
- }
-
- public String stderrString() {
- return new String(stderr.toByteArray());
- }
-
- static List<String> parseStreamAsLines(InputStream stream) {
- List<String> lines = new ArrayList<>();
- try {
- BufferedReader reader = new BufferedReader(new InputStreamReader(stream));
-
- String line;
- while ((line = reader.readLine()) != null) {
- lines.add(line);
- }
- return lines;
- } catch (IOException e) {
- throw new RuntimeException("Unexpected I/O error", e);
- }
- }
-
- public void waitForStdout(String content) {
- long start = System.currentTimeMillis();
- while (System.currentTimeMillis() - start < waitTimeout) {
- if (stdoutString().indexOf(content) != -1) {
- return;
- }
- try {
- Thread.sleep(10);
- } catch (InterruptedException e) {
- throw new RuntimeException("Interrupted ...", e);
- }
- }
-
- throw new RuntimeException("Timed while waiting for content to appear in stdout");
- }
-
- public void sendToStdin(String s) {
- if (stdin instanceof InteractiveInputStream) {
- ((InteractiveInputStream) stdin).pushBytes(s.getBytes());
- } else {
- throw new RuntimeException("Can't push to stdin - not interactive");
- }
- }
-
- static class StreamReaderThread extends Thread {
-
- private InputStream is;
- private OutputStream os;
-
- StreamReaderThread(InputStream is, OutputStream os) {
- this.is = is;
- this.os = os;
- }
-
- public void run() {
- try {
- copyStream(is, os);
- } catch (IOException e) {
- throw new RuntimeException("Unexpected I/O error", e);
- } finally {
- try {
- os.close();
- } catch (IOException ignored) {
- System.err.print("IGNORED: error while closing output stream: ");
- ignored.printStackTrace();
- }
- }
- }
- }
-
- static void copyStream(InputStream is, OutputStream os) throws IOException {
- byte [] buf = new byte[8192];
-
- try (InputStream iss = is) {
- int c;
- while ((c = iss.read(buf)) != -1) {
- os.write(buf, 0, c);
- os.flush();
- }
- }
- }
-
- public static class Builder {
-
- private String workDir;
- private String argsLine;
- private InputStream stdin;
- private String env;
- private boolean dumpStreams;
-
- public Builder workDir(String path) {
- this.workDir = path;
- return this;
- }
-
- public Builder argsLine(String cmd) {
- this.argsLine = cmd;
- return this;
- }
-
- public Builder stdin(InputStream is) {
- this.stdin = is;
- return this;
- }
-
- public Builder env(String env) {
- this.env = env;
- return this;
- }
-
- public Builder fullStreamDump() {
- this.dumpStreams = true;
- return this;
- }
+ public static class Builder extends AbstractExecBuilder<KcRegExec> {
+ @Override
public KcRegExec execute() {
KcRegExec exe = new KcRegExec(workDir, argsLine, env, stdin);
exe.dumpStreams = dumpStreams;
@@ -303,6 +60,7 @@ public class KcRegExec {
return exe;
}
+ @Override
public KcRegExec executeAsync() {
KcRegExec exe = new KcRegExec(workDir, argsLine, env, stdin);
exe.dumpStreams = dumpStreams;
@@ -311,177 +69,4 @@ public class KcRegExec {
}
}
- static class NullInputStream extends InputStream {
-
- @Override
- public int read() throws IOException {
- return -1;
- }
- }
-
- static class InteractiveInputStream extends InputStream {
-
- private LinkedList<Byte> queue = new LinkedList<>();
-
- private Thread consumer;
-
- private boolean closed;
-
- @Override
- public int read(byte b[]) throws IOException {
- return read(b, 0, b.length);
- }
-
- @Override
- public synchronized int read(byte[] b, int off, int len) throws IOException {
-
- Byte current = null;
- int rc = 0;
- try {
- consumer = Thread.currentThread();
-
- do {
- current = queue.poll();
- if (current == null) {
- if (rc > 0) {
- return rc;
- } else {
- do {
- if (closed) {
- return -1;
- }
- wait();
- }
- while ((current = queue.poll()) == null);
- }
- }
-
- b[off + rc] = current;
- rc++;
- } while (rc < len);
-
- } catch (InterruptedException e) {
- throw new InterruptedIOException("Signalled to exit");
- } finally {
- consumer = null;
- }
- return rc;
- }
-
- @Override
- public long skip(long n) throws IOException {
- return super.skip(n);
- }
-
- @Override
- public int available() throws IOException {
- return super.available();
- }
-
- @Override
- public synchronized void mark(int readlimit) {
- super.mark(readlimit);
- }
-
- @Override
- public synchronized void reset() throws IOException {
- super.reset();
- }
-
- @Override
- public boolean markSupported() {
- return super.markSupported();
- }
-
- @Override
- public synchronized int read() throws IOException {
- if (closed) {
- return -1;
- }
-
- // when input is available pass it on
- Byte current;
- try {
- consumer = Thread.currentThread();
-
- while ((current = queue.poll()) == null) {
- wait();
- if (closed) {
- return -1;
- }
- }
-
- } catch (InterruptedException e) {
- throw new InterruptedIOException("Signalled to exit");
- } finally {
- consumer = null;
- }
- return current;
- }
-
- @Override
- public synchronized void close() {
- closed = true;
- new RuntimeException("IIS || close").printStackTrace();
- if (consumer != null) {
- consumer.interrupt();
- }
- }
-
- public synchronized void pushBytes(byte [] buff) {
- for (byte b : buff) {
- queue.add(b);
- }
- notify();
- }
- }
-
-
- static class LoggingOutputStream extends FilterOutputStream {
-
- private ByteArrayOutputStream buffer = new ByteArrayOutputStream();
- private String name;
-
- public LoggingOutputStream(String name, OutputStream os) {
- super(os);
- this.name = name;
- }
-
- @Override
- public void write(int b) throws IOException {
- super.write(b);
- if (b == 10) {
- log();
- } else {
- buffer.write(b);
- }
- }
-
- @Override
- public void write(byte[] buf) throws IOException {
- write(buf, 0, buf.length);
- }
-
- @Override
- public void write(byte[] buf, int offs, int len) throws IOException {
- for (int i = 0; i < len; i++) {
- write(buf[offs+i]);
- }
- }
-
- @Override
- public void close() throws IOException {
- super.close();
- if (buffer.size() > 0) {
- log();
- }
- }
-
- private void log() {
- String log = new String(buffer.toByteArray());
- buffer.reset();
- System.out.println("[" + name + "] " + log);
- }
- }
-
}
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestingResource.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestingResource.java
index 68a5836..bed2d32 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestingResource.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestingResource.java
@@ -18,6 +18,7 @@
package org.keycloak.testsuite.client.resources;
import org.jboss.resteasy.annotations.cache.NoCache;
+import org.keycloak.common.util.MultivaluedHashMap;
import org.keycloak.representations.idm.AdminEventRepresentation;
import org.keycloak.representations.idm.AuthenticationFlowRepresentation;
import org.keycloak.representations.idm.EventRepresentation;
@@ -251,4 +252,8 @@ public interface TestingResource {
@Produces(MediaType.APPLICATION_JSON)
Map<String, String> getIdentityProviderConfig(@QueryParam("alias") String alias);
+ @GET
+ @Path("/component")
+ @Produces(MediaType.APPLICATION_JSON)
+ MultivaluedHashMap<String, String> getComponentConfig(@QueryParam("componentId") String componentId);
}
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/ProfileAssume.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/ProfileAssume.java
index 7fbf59b..9dd598f 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/ProfileAssume.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/ProfileAssume.java
@@ -26,11 +26,11 @@ import org.keycloak.common.Profile;
public class ProfileAssume {
public static void assumePreview() {
- Assume.assumeTrue("Ignoring test as community/preview profile is not enabled", Profile.isPreviewEnabled());
+ Assume.assumeTrue("Ignoring test as community/preview profile is not enabled", !Profile.getName().equals("product"));
}
public static void assumePreviewDisabled() {
- Assume.assumeFalse("Ignoring test as community/preview profile is enabled", Profile.isPreviewEnabled());
+ Assume.assumeFalse("Ignoring test as community/preview profile is enabled", !Profile.getName().equals("product"));
}
}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountTest.java
index b9afd9b..de17221 100755
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountTest.java
@@ -22,9 +22,11 @@ import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
+
import org.keycloak.events.Details;
import org.keycloak.events.Errors;
import org.keycloak.events.EventType;
+import org.keycloak.models.PasswordPolicy;
import org.keycloak.models.utils.TimeBasedOTP;
import org.keycloak.representations.idm.EventRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
@@ -50,6 +52,7 @@ import org.keycloak.testsuite.util.IdentityProviderBuilder;
import org.keycloak.testsuite.util.OAuthClient;
import org.keycloak.testsuite.util.RealmBuilder;
import org.keycloak.testsuite.util.UserBuilder;
+
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
@@ -363,33 +366,90 @@ public class AccountTest extends TestRealmKeycloakTest {
events.expectAccount(EventType.UPDATE_PASSWORD).assertEvent();
}
+ private void assertChangePasswordSucceeds(String currentPassword, String newPassword) {
+ changePasswordPage.changePassword(currentPassword, newPassword, newPassword);
+ Assert.assertEquals("Your password has been updated.", profilePage.getSuccess());
+ events.expectAccount(EventType.UPDATE_PASSWORD).assertEvent();
+ }
+
+ private void assertChangePasswordFails(String currentPassword, String newPassword) {
+ changePasswordPage.changePassword(currentPassword, newPassword, newPassword);
+ Assert.assertThat(profilePage.getError(), containsString("Invalid password: must not be equal to any of last"));
+ events.expectAccount(EventType.UPDATE_PASSWORD_ERROR).error(Errors.PASSWORD_REJECTED).assertEvent();
+ }
+
+ @Test
+ public void changePasswordWithPasswordHistoryPolicyThreePasswords() {
+ setPasswordPolicy(PasswordPolicy.PASSWORD_HISTORY_ID + "(3)");
+
+ changePasswordPage.open();
+ loginPage.login("test-user@localhost", "password");
+ events.expectLogin().client("account").detail(Details.REDIRECT_URI, ACCOUNT_REDIRECT + "?path=password").assertEvent();
+
+ assertChangePasswordFails ("password", "password"); // current: password
+ assertChangePasswordSucceeds("password", "password1"); // current: password
+
+ assertChangePasswordFails ("password1", "password"); // current: password1, history: password
+ assertChangePasswordFails ("password1", "password1"); // current: password1, history: password
+ assertChangePasswordSucceeds("password1", "password2"); // current: password1, history: password
+
+ assertChangePasswordFails ("password2", "password"); // current: password2, history: password, password1
+ assertChangePasswordFails ("password2", "password1"); // current: password2, history: password, password1
+ assertChangePasswordFails ("password2", "password2"); // current: password2, history: password, password1
+ assertChangePasswordSucceeds("password2", "password3"); // current: password2, history: password, password1
+
+ assertChangePasswordSucceeds("password3", "password"); // current: password3, history: password1, password2
+ }
+
@Test
- public void changePasswordWithPasswordHistoryPolicy() {
- setPasswordPolicy("passwordHistory(2)");
+ public void changePasswordWithPasswordHistoryPolicyTwoPasswords() {
+ setPasswordPolicy(PasswordPolicy.PASSWORD_HISTORY_ID + "(2)");
changePasswordPage.open();
loginPage.login("test-user@localhost", "password");
events.expectLogin().client("account").detail(Details.REDIRECT_URI, ACCOUNT_REDIRECT + "?path=password").assertEvent();
- changePasswordPage.changePassword("password", "password", "password");
- Assert.assertEquals("Invalid password: must not be equal to any of last 2 passwords.", profilePage.getError());
- events.expectAccount(EventType.UPDATE_PASSWORD_ERROR).error(Errors.PASSWORD_REJECTED).assertEvent();
+ assertChangePasswordFails ("password", "password"); // current: password
+ assertChangePasswordSucceeds("password", "password1"); // current: password
- changePasswordPage.changePassword("password", "password1", "password1");
- Assert.assertEquals("Your password has been updated.", profilePage.getSuccess());
- events.expectAccount(EventType.UPDATE_PASSWORD).assertEvent();
+ assertChangePasswordFails ("password1", "password"); // current: password1, history: password
+ assertChangePasswordFails ("password1", "password1"); // current: password1, history: password
+ assertChangePasswordSucceeds("password1", "password2"); // current: password1, history: password
- changePasswordPage.changePassword("password1", "password", "password");
- Assert.assertEquals("Invalid password: must not be equal to any of last 2 passwords.", profilePage.getError());
- events.expectAccount(EventType.UPDATE_PASSWORD_ERROR).error(Errors.PASSWORD_REJECTED).assertEvent();
+ assertChangePasswordFails ("password2", "password1"); // current: password2, history: password1
+ assertChangePasswordFails ("password2", "password2"); // current: password2, history: password1
+ assertChangePasswordSucceeds("password2", "password"); // current: password2, history: password1
+ }
- changePasswordPage.changePassword("password1", "password1", "password1");
- Assert.assertEquals("Invalid password: must not be equal to any of last 2 passwords.", profilePage.getError());
- events.expectAccount(EventType.UPDATE_PASSWORD_ERROR).error(Errors.PASSWORD_REJECTED).assertEvent();
+ @Test
+ public void changePasswordWithPasswordHistoryPolicyOnePwds() {
+ // One password means only the active password is checked
+ setPasswordPolicy(PasswordPolicy.PASSWORD_HISTORY_ID + "(1)");
- changePasswordPage.changePassword("password1", "password2", "password2");
- Assert.assertEquals("Your password has been updated.", profilePage.getSuccess());
- events.expectAccount(EventType.UPDATE_PASSWORD).assertEvent();
+ changePasswordPage.open();
+ loginPage.login("test-user@localhost", "password");
+ events.expectLogin().client("account").detail(Details.REDIRECT_URI, ACCOUNT_REDIRECT + "?path=password").assertEvent();
+
+ assertChangePasswordFails ("password", "password"); // current: password
+ assertChangePasswordSucceeds("password", "password1"); // current: password
+
+ assertChangePasswordFails ("password1", "password1"); // current: password1
+ assertChangePasswordSucceeds("password1", "password"); // current: password1
+ }
+
+ @Test
+ public void changePasswordWithPasswordHistoryPolicyZeroPwdsInHistory() {
+ setPasswordPolicy(PasswordPolicy.PASSWORD_HISTORY_ID + "(0)");
+
+ changePasswordPage.open();
+ loginPage.login("test-user@localhost", "password");
+ events.expectLogin().client("account").detail(Details.REDIRECT_URI, ACCOUNT_REDIRECT + "?path=password").assertEvent();
+
+ assertChangePasswordFails ("password", "password"); // current: password
+ assertChangePasswordSucceeds("password", "password1"); // current: password
+
+ assertChangePasswordFails ("password1", "password1"); // current: password1
+ assertChangePasswordSucceeds("password1", "password"); // current: password1
}
@Test
@@ -489,6 +549,12 @@ public class AccountTest extends TestRealmKeycloakTest {
testRealm().update(testRealm);
}
+ private void setDuplicateEmailsAllowed(boolean allowed) {
+ RealmRepresentation testRealm = testRealm().toRepresentation();
+ testRealm.setDuplicateEmailsAllowed(allowed);
+ testRealm().update(testRealm);
+ }
+
@Test
public void changeUsername() {
// allow to edit the username in realm
@@ -599,7 +665,7 @@ public class AccountTest extends TestRealmKeycloakTest {
// KEYCLOAK-1534
@Test
- public void changeEmailToExisting() {
+ public void changeEmailToExistingForbidden() {
profilePage.open();
loginPage.login("test-user@localhost", "password");
@@ -633,6 +699,24 @@ public class AccountTest extends TestRealmKeycloakTest {
profilePage.updateProfile("Tom", "Brady", "test-user@localhost");
events.expectAccount(EventType.UPDATE_PROFILE).assertEvent();
}
+
+ @Test
+ public void changeEmailToExistingAllowed() {
+ setDuplicateEmailsAllowed(true);
+
+ profilePage.open();
+ loginPage.login("test-user@localhost", "password");
+
+ events.expectLogin().client("account").detail(Details.REDIRECT_URI, ACCOUNT_REDIRECT).assertEvent();
+
+ Assert.assertEquals("test-user@localhost", profilePage.getUsername());
+ Assert.assertEquals("test-user@localhost", profilePage.getEmail());
+
+ // Change to the email, which some other user has
+ profilePage.updateProfile("New first", "New last", "test-user-no-access@localhost");
+
+ Assert.assertEquals("Your account has been updated.", profilePage.getSuccess());
+ }
@Test
public void setupTotp() {
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionEmailVerificationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionEmailVerificationTest.java
index e68af58..7aa3160 100755
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionEmailVerificationTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionEmailVerificationTest.java
@@ -264,7 +264,7 @@ public class RequiredActionEmailVerificationTest extends TestRealmKeycloakTest {
.clearDetails()
.assertEvent();
- String badKeyURL = KeycloakUriBuilder.fromUri(resendEmailLink).queryParam("key", "foo").build().toString();
+ String badKeyURL = KeycloakUriBuilder.fromUri(resendEmailLink).replaceQueryParam("key", "foo").build().toString();
driver.navigate().to(badKeyURL);
events.expectRequiredAction(EventType.VERIFY_EMAIL_ERROR)
@@ -276,6 +276,37 @@ public class RequiredActionEmailVerificationTest extends TestRealmKeycloakTest {
.assertEvent();
}
+ @Test
+ public void verifyEmailBadCode() throws IOException, MessagingException {
+ loginPage.open();
+ loginPage.login("test-user@localhost", "password");
+
+ Assert.assertTrue(verifyEmailPage.isCurrent());
+
+ Assert.assertEquals(1, greenMail.getReceivedMessages().length);
+
+ MimeMessage message = greenMail.getReceivedMessages()[0];
+
+ String verificationUrl = getPasswordResetEmailLink(message);
+
+ verificationUrl = KeycloakUriBuilder.fromUri(verificationUrl).replaceQueryParam("code", "foo").build().toString();
+
+ events.poll();
+
+ driver.navigate().to(verificationUrl.trim());
+
+ assertEquals("The link you clicked is a old stale link and is no longer valid. Maybe you have already verified your email?", errorPage.getError());
+
+ events.expectRequiredAction(EventType.VERIFY_EMAIL_ERROR)
+ .error(Errors.INVALID_CODE)
+ .client((String)null)
+ .user((String)null)
+ .session((String)null)
+ .clearDetails()
+ .assertEvent();
+ }
+
+
public static String getPasswordResetEmailLink(MimeMessage message) throws IOException, MessagingException {
Multipart multipart = (Multipart) message.getContent();
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractPhotozExampleAdapterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractPhotozExampleAdapterTest.java
index 55eccaa..d6f9134 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractPhotozExampleAdapterTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractPhotozExampleAdapterTest.java
@@ -609,6 +609,27 @@ public abstract class AbstractPhotozExampleAdapterTest extends AbstractExampleAd
this.deployer.undeploy(RESOURCE_SERVER_ID);
}
}
+
+ //KEYCLOAK-3777
+ @Test
+ public void testEntitlementRequest() {
+ try {
+ this.deployer.deploy(RESOURCE_SERVER_ID);
+
+ clientPage.navigateTo();
+ loginToClientPage("admin", "admin");
+
+ clientPage.requestEntitlements();
+ assertTrue(driver.getPageSource().contains("urn:photoz.com:scopes:album:admin:manage"));
+
+ clientPage.requestEntitlement();
+ String pageSource = driver.getPageSource();
+ assertTrue(pageSource.contains("urn:photoz.com:scopes:album:view"));
+ assertFalse(pageSource.contains("urn:photoz.com:scopes:album:admin:manage"));
+ } finally {
+ this.deployer.undeploy(RESOURCE_SERVER_ID);
+ }
+ }
private void importResourceServerSettings() throws FileNotFoundException {
getAuthorizationResource().importSettings(loadJson(new FileInputStream(new File(TEST_APPS_HOME_DIR + "/photoz/photoz-restful-api-authz-service.json")), ResourceServerRepresentation.class));
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractOIDCPublicKeyRotationAdapterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractOIDCPublicKeyRotationAdapterTest.java
index c1d5b57..fc6bd67 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractOIDCPublicKeyRotationAdapterTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractOIDCPublicKeyRotationAdapterTest.java
@@ -19,6 +19,7 @@ package org.keycloak.testsuite.adapter.servlet;
import java.io.IOException;
import java.io.InputStream;
+import java.util.Map;
import java.util.concurrent.TimeUnit;
import javax.ws.rs.core.Response;
@@ -41,6 +42,7 @@ import org.keycloak.common.util.MultivaluedHashMap;
import org.keycloak.common.util.StreamUtil;
import org.keycloak.common.util.Time;
import org.keycloak.constants.AdapterConstants;
+import org.keycloak.jose.jws.AlgorithmType;
import org.keycloak.keys.KeyProvider;
import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
@@ -48,6 +50,7 @@ import org.keycloak.representations.AccessToken;
import org.keycloak.representations.adapters.action.GlobalRequestResult;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.ComponentRepresentation;
+import org.keycloak.representations.idm.KeysMetadataRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.testsuite.adapter.AbstractServletsAdapterTest;
import org.keycloak.testsuite.adapter.filter.AdapterActionsFilter;
@@ -58,9 +61,7 @@ import org.keycloak.testsuite.admin.ApiUtil;
import org.keycloak.testsuite.util.URLAssert;
import org.openqa.selenium.By;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.*;
import static org.keycloak.testsuite.auth.page.AuthRealm.DEMO;
import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlEquals;
import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlStartsWith;
@@ -201,9 +202,9 @@ public abstract class AbstractOIDCPublicKeyRotationAdapterTest extends AbstractS
Assert.assertEquals(200, status);
// Re-generate realm public key and remove the old key
- String oldKeyId = getActiveKeyId();
+ String oldActiveKeyProviderId = getActiveKeyProvider();
generateNewRealmKey();
- adminClient.realm(DEMO).components().component(oldKeyId).remove();
+ adminClient.realm(DEMO).components().component(oldActiveKeyProviderId).remove();
// Send REST request to the customer-db app. Should be still succcessfully authenticated as the JWKPublicKeyLocator cache is still valid
status = invokeRESTEndpoint(accessTokenString);
@@ -237,7 +238,8 @@ public abstract class AbstractOIDCPublicKeyRotationAdapterTest extends AbstractS
String accessTokenString = tokenMinTTLPage.getAccessTokenString();
// Generate new realm public key
- String oldKeyId = getActiveKeyId();
+ String oldActiveKeyProviderId = getActiveKeyProvider();
+
generateNewRealmKey();
// Send REST request to customer-db app. It should be successfully authenticated even that token is signed by the old key
@@ -245,7 +247,7 @@ public abstract class AbstractOIDCPublicKeyRotationAdapterTest extends AbstractS
Assert.assertEquals(200, status);
// Remove the old realm key now
- adminClient.realm(DEMO).components().component(oldKeyId).remove();
+ adminClient.realm(DEMO).components().component(oldActiveKeyProviderId).remove();
// Set some offset to ensure pushing notBefore will pass
setAdapterAndServerTimeOffset(130, customerDb.toString() + "/unsecured/foo", tokenMinTTLPage.toString() + "/unsecured/foo");
@@ -295,13 +297,17 @@ public abstract class AbstractOIDCPublicKeyRotationAdapterTest extends AbstractS
response.close();
}
- private String getActiveKeyId() {
- String realmId = adminClient.realm(DEMO).toRepresentation().getId();
- return adminClient.realm(DEMO).components().query(realmId, KeyProvider.class.getName())
- .get(0).getId();
+ private String getActiveKeyProvider() {
+ KeysMetadataRepresentation keyMetadata = adminClient.realm(DEMO).keys().getKeyMetadata();
+ String activeKid = keyMetadata.getActive().get(AlgorithmType.RSA.name());
+ for (KeysMetadataRepresentation.KeyMetadataRepresentation rep : keyMetadata.getKeys()) {
+ if (rep.getKid().equals(activeKid)) {
+ return rep.getProviderId();
+ }
+ }
+ return null;
}
-
private int invokeRESTEndpoint(String accessTokenString) {
HttpClient client = new DefaultHttpClient();
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractSAMLServletsAdapterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractSAMLServletsAdapterTest.java
index 5daec87..52ea7f7 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractSAMLServletsAdapterTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractSAMLServletsAdapterTest.java
@@ -30,7 +30,8 @@ import org.keycloak.common.util.KeyUtils;
import org.keycloak.common.util.PemUtils;
import org.keycloak.keys.Attributes;
import org.keycloak.keys.KeyProvider;
-import org.keycloak.keys.RsaKeyProviderFactory;
+import org.keycloak.keys.ImportedRsaKeyProviderFactory;
+import org.keycloak.protocol.saml.SamlConfigAttributes;
import org.keycloak.representations.idm.ComponentRepresentation;
import org.keycloak.protocol.saml.mappers.AttributeStatementHelper;
import org.keycloak.protocol.saml.mappers.RoleListMapper;
@@ -42,6 +43,7 @@ import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.saml.BaseSAML2BindingBuilder;
import org.keycloak.saml.SAML2ErrorResponseBuilder;
import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
+import org.keycloak.saml.common.util.XmlKeyInfoKeyNameTransformer;
import org.keycloak.testsuite.adapter.AbstractServletsAdapterTest;
import org.keycloak.testsuite.adapter.page.BadAssertionSalesPostSig;
import org.keycloak.testsuite.adapter.page.BadClientSalesPostSigServlet;
@@ -71,6 +73,7 @@ import org.keycloak.testsuite.auth.page.login.Login;
import org.keycloak.testsuite.auth.page.login.SAMLIDPInitiatedLogin;
import org.keycloak.testsuite.page.AbstractPage;
import org.keycloak.testsuite.util.IOUtil;
+import org.keycloak.testsuite.util.RealmBuilder;
import org.keycloak.testsuite.util.UserBuilder;
import org.openqa.selenium.By;
@@ -439,20 +442,21 @@ public abstract class AbstractSAMLServletsAdapterTest extends AbstractServletsAd
testSuccessfulAndUnauthorizedLogin(employeeSigServletPage, testRealmSAMLRedirectLoginPage);
}
+ private static final KeyPair NEW_KEY_PAIR = KeyUtils.generateRsaKeyPair(1024);
+ private static final String NEW_KEY_PRIVATE_KEY_PEM = PemUtils.encodeKey(NEW_KEY_PAIR.getPrivate());
+
private PublicKey createKeys(String priority) throws Exception {
- KeyPair keyPair = KeyUtils.generateRsaKeyPair(1024);
- String privateKeyPem = PemUtils.encodeKey(keyPair.getPrivate());
- PublicKey publicKey = keyPair.getPublic();
+ PublicKey publicKey = NEW_KEY_PAIR.getPublic();
ComponentRepresentation rep = new ComponentRepresentation();
rep.setName("mycomponent");
rep.setParentId("demo");
- rep.setProviderId(RsaKeyProviderFactory.ID);
+ rep.setProviderId(ImportedRsaKeyProviderFactory.ID);
rep.setProviderType(KeyProvider.class.getName());
org.keycloak.common.util.MultivaluedHashMap config = new org.keycloak.common.util.MultivaluedHashMap();
config.addFirst("priority", priority);
- config.addFirst(Attributes.PRIVATE_KEY_KEY, privateKeyPem);
+ config.addFirst(Attributes.PRIVATE_KEY_KEY, NEW_KEY_PRIVATE_KEY_PEM);
rep.setConfig(config);
testRealmResource().components().add(rep);
@@ -493,11 +497,53 @@ public abstract class AbstractSAMLServletsAdapterTest extends AbstractServletsAd
}
@Test
+ public void employeeSigPostNoIdpKeyTestNoKeyNameInKeyInfo() throws Exception {
+ RealmRepresentation r = testRealmResource().toRepresentation();
+ r.getAttributes().put(SamlConfigAttributes.SAML_SERVER_SIGNATURE_KEYINFO_KEY_NAME_TRANSFORMER, XmlKeyInfoKeyNameTransformer.NONE.name());
+ testRotatedKeysPropagated(employeeSigPostNoIdpKeyServletPage, testRealmSAMLPostLoginPage);
+ }
+
+ @Test
+ public void employeeSigPostNoIdpKeyTestCertSubjectAsKeyNameInKeyInfo() throws Exception {
+ RealmRepresentation r = testRealmResource().toRepresentation();
+ r.getAttributes().put(SamlConfigAttributes.SAML_SERVER_SIGNATURE_KEYINFO_KEY_NAME_TRANSFORMER, XmlKeyInfoKeyNameTransformer.CERT_SUBJECT.name());
+ testRotatedKeysPropagated(employeeSigPostNoIdpKeyServletPage, testRealmSAMLPostLoginPage);
+ }
+
+ @Test
+ public void employeeSigPostNoIdpKeyTestKeyIdAsKeyNameInKeyInfo() throws Exception {
+ RealmRepresentation r = testRealmResource().toRepresentation();
+ r.getAttributes().put(SamlConfigAttributes.SAML_SERVER_SIGNATURE_KEYINFO_KEY_NAME_TRANSFORMER, XmlKeyInfoKeyNameTransformer.KEY_ID.name());
+ testRotatedKeysPropagated(employeeSigPostNoIdpKeyServletPage, testRealmSAMLPostLoginPage);
+ }
+
+ @Test
public void employeeSigRedirNoIdpKeyTest() throws Exception {
testRotatedKeysPropagated(employeeSigRedirNoIdpKeyServletPage, testRealmSAMLRedirectLoginPage);
}
@Test
+ public void employeeSigRedirNoIdpKeyTestNoKeyNameInKeyInfo() throws Exception {
+ RealmRepresentation r = testRealmResource().toRepresentation();
+ r.getAttributes().put(SamlConfigAttributes.SAML_SERVER_SIGNATURE_KEYINFO_KEY_NAME_TRANSFORMER, XmlKeyInfoKeyNameTransformer.NONE.name());
+ testRotatedKeysPropagated(employeeSigRedirNoIdpKeyServletPage, testRealmSAMLRedirectLoginPage);
+ }
+
+ @Test
+ public void employeeSigRedirNoIdpKeyTestCertSubjectAsKeyNameInKeyInfo() throws Exception {
+ RealmRepresentation r = testRealmResource().toRepresentation();
+ r.getAttributes().put(SamlConfigAttributes.SAML_SERVER_SIGNATURE_KEYINFO_KEY_NAME_TRANSFORMER, XmlKeyInfoKeyNameTransformer.CERT_SUBJECT.name());
+ testRotatedKeysPropagated(employeeSigRedirNoIdpKeyServletPage, testRealmSAMLRedirectLoginPage);
+ }
+
+ @Test
+ public void employeeSigRedirNoIdpKeyTestKeyIdAsKeyNameInKeyInfo() throws Exception {
+ RealmRepresentation r = testRealmResource().toRepresentation();
+ r.getAttributes().put(SamlConfigAttributes.SAML_SERVER_SIGNATURE_KEYINFO_KEY_NAME_TRANSFORMER, XmlKeyInfoKeyNameTransformer.KEY_ID.name());
+ testRotatedKeysPropagated(employeeSigRedirNoIdpKeyServletPage, testRealmSAMLRedirectLoginPage);
+ }
+
+ @Test
public void employeeSigRedirOptNoIdpKeyTest() throws Exception {
testRotatedKeysPropagated(employeeSigRedirOptNoIdpKeyServletPage, testRealmSAMLRedirectLoginPage);
}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/ProvidersTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/ProvidersTest.java
index 18c1616..fc8b25b 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/ProvidersTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/ProvidersTest.java
@@ -19,6 +19,7 @@ package org.keycloak.testsuite.admin.authentication;
import org.junit.Test;
import org.keycloak.authentication.authenticators.broker.IdpCreateUserIfUniqueAuthenticatorFactory;
+import org.keycloak.common.Profile;
import org.keycloak.representations.idm.AuthenticatorConfigInfoRepresentation;
import org.keycloak.representations.idm.ConfigPropertyRepresentation;
import org.keycloak.testsuite.Assert;
@@ -32,8 +33,6 @@ import java.util.LinkedList;
import java.util.List;
import java.util.Map;
-import static org.keycloak.common.Profile.isPreviewEnabled;
-
/**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
@@ -137,7 +136,7 @@ public class ProvidersTest extends AbstractAuthenticationTest {
"Validates a OTP on a separate OTP form. Only shown if required based on the configured conditions.");
addProviderInfo(result, "auth-cookie", "Cookie", "Validates the SSO cookie set by the auth server.");
addProviderInfo(result, "auth-otp-form", "OTP Form", "Validates a OTP on a separate OTP form.");
- if (isPreviewEnabled()) {
+ if (Profile.isFeatureEnabled(Profile.Feature.SCRIPTS)) {
addProviderInfo(result, "auth-script-based", "Script", "Script based authentication. Allows to define custom authentication logic via JavaScript.");
}
addProviderInfo(result, "auth-spnego", "Kerberos", "Initiates the SPNEGO protocol. Most often used with Kerberos.");
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/AbstractClientTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/AbstractClientTest.java
index 02a2cdb..eed784b 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/AbstractClientTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/AbstractClientTest.java
@@ -84,12 +84,32 @@ public abstract class AbstractClientTest extends AbstractAuthTest {
}
protected String createOidcClient(String name) {
+ return createClient(createOidcClientRep(name));
+ }
+
+ protected String createOidcBearerOnlyClient(String name) {
+ ClientRepresentation clientRep = createOidcClientRep(name);
+ clientRep.setBearerOnly(Boolean.TRUE);
+ clientRep.setPublicClient(Boolean.FALSE);
+ return createClient(clientRep);
+ }
+
+ protected String createOidcBearerOnlyClientWithAuthz(String name) {
+ ClientRepresentation clientRep = createOidcClientRep(name);
+ clientRep.setBearerOnly(Boolean.TRUE);
+ clientRep.setPublicClient(Boolean.FALSE);
+ clientRep.setAuthorizationServicesEnabled(Boolean.TRUE);
+ clientRep.setServiceAccountsEnabled(Boolean.TRUE);
+ return createClient(clientRep);
+ }
+
+ protected ClientRepresentation createOidcClientRep(String name) {
ClientRepresentation clientRep = new ClientRepresentation();
clientRep.setClientId(name);
clientRep.setName(name);
clientRep.setRootUrl("foo");
- clientRep.setProtocol("openid-connect");
- return createClient(clientRep);
+ clientRep.setProtocol("openid-connect");
+ return clientRep;
}
protected String createSamlClient(String name) {
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/EnforcerConfigTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/EnforcerConfigTest.java
index f1e407f..081bb79 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/EnforcerConfigTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/EnforcerConfigTest.java
@@ -27,6 +27,7 @@ import org.keycloak.testsuite.AbstractKeycloakTest;
import org.keycloak.testsuite.ProfileAssume;
import java.util.List;
+import java.util.Map;
import static org.junit.Assert.assertEquals;
import static org.keycloak.testsuite.util.IOUtil.loadRealm;
@@ -49,8 +50,8 @@ public class EnforcerConfigTest extends AbstractKeycloakTest {
public void testMultiplePathsWithSameName() throws Exception{
KeycloakDeployment deployment = KeycloakDeploymentBuilder.build(getClass().getResourceAsStream("/authorization-test/enforcer-config-paths-same-name.json"));
PolicyEnforcer policyEnforcer = deployment.getPolicyEnforcer();
- List<PolicyEnforcerConfig.PathConfig> paths = policyEnforcer.getPaths();
+ Map<String, PolicyEnforcerConfig.PathConfig> paths = policyEnforcer.getPaths();
assertEquals(1, paths.size());
- assertEquals(4, paths.get(0).getMethods().size());
+ assertEquals(4, paths.values().iterator().next().getMethods().size());
}
}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/GenericPolicyManagementTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/GenericPolicyManagementTest.java
index e6f83d8..a1a2bfc 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/GenericPolicyManagementTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/GenericPolicyManagementTest.java
@@ -38,6 +38,7 @@ import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.function.Function;
import java.util.stream.Collectors;
import static org.junit.Assert.assertEquals;
@@ -71,7 +72,7 @@ public class GenericPolicyManagementTest extends AbstractAuthorizationTest {
PolicyRepresentation newPolicy = createTestingPolicy().toRepresentation();
assertEquals("Test Generic Policy", newPolicy.getName());
- assertEquals("test", newPolicy.getType());
+ assertEquals("scope", newPolicy.getType());
assertEquals(Logic.POSITIVE, newPolicy.getLogic());
assertEquals(DecisionStrategy.UNANIMOUS, newPolicy.getDecisionStrategy());
assertEquals("configuration for A", newPolicy.getConfig().get("configA"));
@@ -98,28 +99,28 @@ public class GenericPolicyManagementTest extends AbstractAuthorizationTest {
@Test
public void testUpdate() {
PolicyResource policyResource = createTestingPolicy();
- PolicyRepresentation resource = policyResource.toRepresentation();
+ PolicyRepresentation policy = policyResource.toRepresentation();
- resource.setName("changed");
- resource.setLogic(Logic.NEGATIVE);
- resource.setDecisionStrategy(DecisionStrategy.AFFIRMATIVE);
- resource.getConfig().put("configA", "changed configuration for A");
- resource.getConfig().remove("configB");
- resource.getConfig().put("configC", "changed configuration for C");
+ policy.setName("changed");
+ policy.setLogic(Logic.NEGATIVE);
+ policy.setDecisionStrategy(DecisionStrategy.AFFIRMATIVE);
+ policy.getConfig().put("configA", "changed configuration for A");
+ policy.getConfig().remove("configB");
+ policy.getConfig().put("configC", "changed configuration for C");
- policyResource.update(resource);
+ policyResource.update(policy);
- resource = policyResource.toRepresentation();
+ policy = policyResource.toRepresentation();
- assertEquals("changed", resource.getName());
- assertEquals(Logic.NEGATIVE, resource.getLogic());
+ assertEquals("changed", policy.getName());
+ assertEquals(Logic.NEGATIVE, policy.getLogic());
- assertEquals(DecisionStrategy.AFFIRMATIVE, resource.getDecisionStrategy());
- assertEquals("changed configuration for A", resource.getConfig().get("configA"));
- assertNull(resource.getConfig().get("configB"));
- assertEquals("changed configuration for C", resource.getConfig().get("configC"));
+ assertEquals(DecisionStrategy.AFFIRMATIVE, policy.getDecisionStrategy());
+ assertEquals("changed configuration for A", policy.getConfig().get("configA"));
+ assertNull(policy.getConfig().get("configB"));
+ assertEquals("changed configuration for C", policy.getConfig().get("configC"));
- Map<String, String> config = resource.getConfig();
+ Map<String, String> config = policy.getConfig();
config.put("applyPolicies", buildConfigOption(findPolicyByName("Test Associated C").getId()));
@@ -127,22 +128,25 @@ public class GenericPolicyManagementTest extends AbstractAuthorizationTest {
config.put("scopes", buildConfigOption(findScopeByName("Test Scope A").getId()));
- policyResource.update(resource);
+ policyResource.update(policy);
- resource = policyResource.toRepresentation();
- config = resource.getConfig();
+ policy = policyResource.toRepresentation();
+ config = policy.getConfig();
- assertAssociatedPolicy("Test Associated C", resource);
- assertFalse(config.get("applyPolicies").contains(findPolicyByName("Test Associated A").getId()));
- assertFalse(config.get("applyPolicies").contains(findPolicyByName("Test Associated B").getId()));
+ assertAssociatedPolicy("Test Associated C", policy);
+ List<PolicyRepresentation> associatedPolicies = getClientResource().authorization().policies().policy(policy.getId()).associatedPolicies();
+ assertFalse(associatedPolicies.stream().filter(associated -> associated.getId().equals(findPolicyByName("Test Associated A").getId())).findFirst().isPresent());
+ assertFalse(associatedPolicies.stream().filter(associated -> associated.getId().equals(findPolicyByName("Test Associated B").getId())).findFirst().isPresent());
- assertAssociatedResource("Test Resource B", resource);
- assertFalse(config.get("resources").contains(findResourceByName("Test Resource A").getId()));
- assertFalse(config.get("resources").contains(findResourceByName("Test Resource C").getId()));
+ assertAssociatedResource("Test Resource B", policy);
+ List<ResourceRepresentation> resources = policyResource.resources();
+ assertFalse(resources.contains(findResourceByName("Test Resource A")));
+ assertFalse(resources.contains(findResourceByName("Test Resource C")));
- assertAssociatedScope("Test Scope A", resource);
- assertFalse(config.get("scopes").contains(findScopeByName("Test Scope B").getId()));
- assertFalse(config.get("scopes").contains(findScopeByName("Test Scope C").getId()));
+ assertAssociatedScope("Test Scope A", policy);
+ List<ScopeRepresentation> scopes = getClientResource().authorization().policies().policy(policy.getId()).scopes();
+ assertFalse(scopes.contains(findScopeByName("Test Scope B").getId()));
+ assertFalse(scopes.contains(findScopeByName("Test Scope C").getId()));
}
@Test
@@ -186,7 +190,7 @@ public class GenericPolicyManagementTest extends AbstractAuthorizationTest {
PolicyRepresentation newPolicy = new PolicyRepresentation();
newPolicy.setName(name);
- newPolicy.setType("test");
+ newPolicy.setType("scope");
newPolicy.setConfig(config);
PoliciesResource policies = getClientResource().authorization().policies();
@@ -264,27 +268,38 @@ public class GenericPolicyManagementTest extends AbstractAuthorizationTest {
private void assertAssociatedPolicy(String associatedPolicyName, PolicyRepresentation dependentPolicy) {
PolicyRepresentation associatedPolicy = findPolicyByName(associatedPolicyName);
+ PoliciesResource policies = getClientResource().authorization().policies();
+ associatedPolicy = policies.policy(associatedPolicy.getId()).toRepresentation();
assertNotNull(associatedPolicy);
- assertTrue(dependentPolicy.getConfig().get("applyPolicies").contains(associatedPolicy.getId()));
- assertEquals(1, associatedPolicy.getDependentPolicies().size());
- assertEquals(dependentPolicy.getId(), associatedPolicy.getDependentPolicies().get(0).getId());
+ PolicyRepresentation finalAssociatedPolicy = associatedPolicy;
+ PolicyResource policyResource = policies.policy(dependentPolicy.getId());
+ List<PolicyRepresentation> associatedPolicies = policyResource.associatedPolicies();
+ assertTrue(associatedPolicies.stream().filter(associated -> associated.getId().equals(finalAssociatedPolicy.getId())).findFirst().isPresent());
+ List<PolicyRepresentation> dependentPolicies = policies.policy(associatedPolicy.getId()).dependentPolicies();
+ assertEquals(1, dependentPolicies.size());
+ assertEquals(dependentPolicy.getId(), dependentPolicies.get(0).getId());
}
private void assertAssociatedResource(String resourceName, PolicyRepresentation policy) {
ResourceRepresentation resource = findResourceByName(resourceName);
assertNotNull(resource);
- assertTrue(policy.getConfig().get("resources").contains(resource.getId()));
- assertEquals(1, resource.getPolicies().size());
- assertTrue(resource.getPolicies().stream().map(PolicyRepresentation::getId).collect(Collectors.toList())
+ List<ResourceRepresentation> resources = getClientResource().authorization().policies().policy(policy.getId()).resources();
+ assertTrue(resources.contains(resource));
+ List<PolicyRepresentation> policies = getClientResource().authorization().resources().resource(resource.getId()).permissions();
+ assertEquals(1, policies.size());
+ assertTrue(policies.stream().map(PolicyRepresentation::getId).collect(Collectors.toList())
.contains(policy.getId()));
}
private void assertAssociatedScope(String scopeName, PolicyRepresentation policy) {
ScopeRepresentation scope = findScopeByName(scopeName);
+ scope = getClientResource().authorization().scopes().scope(scope.getId()).toRepresentation();
assertNotNull(scope);
- assertTrue(policy.getConfig().get("scopes").contains(scope.getId()));
- assertEquals(1, scope.getPolicies().size());
- assertTrue(scope.getPolicies().stream().map(PolicyRepresentation::getId).collect(Collectors.toList())
+ List<ScopeRepresentation> scopes = getClientResource().authorization().policies().policy(policy.getId()).scopes();
+ assertTrue(scopes.stream().map((Function<ScopeRepresentation, String>) rep -> rep.getId()).collect(Collectors.toList()).contains(scope.getId()));
+ List<PolicyRepresentation> permissions = getClientResource().authorization().scopes().scope(scope.getId()).permissions();
+ assertEquals(1, permissions.size());
+ assertTrue(permissions.stream().map(PolicyRepresentation::getId).collect(Collectors.toList())
.contains(policy.getId()));
}
}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/ClientDescriptionConverterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/ClientDescriptionConverterTest.java
new file mode 100644
index 0000000..2718d3a
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/ClientDescriptionConverterTest.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2016 Red Hat Inc. and/or its affiliates and other contributors
+ * as indicated by the @author tags. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package org.keycloak.testsuite.admin.client;
+
+import org.keycloak.testsuite.AbstractAuthTest;
+
+import java.io.IOException;
+import java.io.InputStream;
+import org.apache.commons.io.IOUtils;
+import org.junit.Test;
+
+/**
+ *
+ * @author hmlnarik
+ */
+public class ClientDescriptionConverterTest extends AbstractAuthTest {
+
+ // https://issues.jboss.org/browse/KEYCLOAK-4040
+ @Test
+ public void testOrganizationDetailsMetadata() throws IOException {
+ try (InputStream is = ClientDescriptionConverterTest.class.getResourceAsStream("KEYCLOAK-4040-sharefile-metadata.xml")) {
+ String data = IOUtils.toString(is, "UTF-8");
+ testRealmResource().convertClientDescription(data);
+ }
+ }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/InstallationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/InstallationTest.java
index 4328c8f..a33b2a1 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/InstallationTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/InstallationTest.java
@@ -35,17 +35,28 @@ import static org.hamcrest.Matchers.*;
public class InstallationTest extends AbstractClientTest {
private static final String OIDC_NAME = "oidcInstallationClient";
+ private static final String OIDC_NAME_BEARER_ONLY_NAME = "oidcInstallationClientBearerOnly";
+ private static final String OIDC_NAME_BEARER_ONLY_WITH_AUTHZ_NAME = "oidcInstallationClientBearerOnlyWithAuthz";
private static final String SAML_NAME = "samlInstallationClient";
private ClientResource oidcClient;
private String oidcClientId;
+ private ClientResource oidcBearerOnlyClient;
+ private String oidcBearerOnlyClientId;
+ private ClientResource oidcBearerOnlyClientWithAuthz;
+ private String oidcBearerOnlyClientWithAuthzId;
private ClientResource samlClient;
private String samlClientId;
@Before
public void createClients() {
oidcClientId = createOidcClient(OIDC_NAME);
+ oidcBearerOnlyClientId = createOidcBearerOnlyClient(OIDC_NAME_BEARER_ONLY_NAME);
+ oidcBearerOnlyClientWithAuthzId = createOidcBearerOnlyClientWithAuthz(OIDC_NAME_BEARER_ONLY_WITH_AUTHZ_NAME);
+
oidcClient = findClientResource(OIDC_NAME);
+ oidcBearerOnlyClient = findClientResource(OIDC_NAME_BEARER_ONLY_NAME);
+ oidcBearerOnlyClientWithAuthz = findClientResource(OIDC_NAME_BEARER_ONLY_WITH_AUTHZ_NAME);
samlClientId = createSamlClient(SAML_NAME);
samlClient = findClientResource(SAML_NAME);
@@ -54,6 +65,8 @@ public class InstallationTest extends AbstractClientTest {
@After
public void tearDown() {
removeClient(oidcClientId);
+ removeClient(oidcBearerOnlyClientId);
+ removeClient(oidcBearerOnlyClientWithAuthzId);
removeClient(samlClientId);
}
@@ -78,6 +91,25 @@ public class InstallationTest extends AbstractClientTest {
assertOidcInstallationConfig(json);
}
+ @Test
+ public void testOidcBearerOnlyJson() {
+ String json = oidcBearerOnlyClient.getInstallationProvider("keycloak-oidc-keycloak-json");
+ assertOidcInstallationConfig(json);
+ assertThat(json, containsString("bearer-only"));
+ assertThat(json, not(containsString("public-client")));
+ assertThat(json, not(containsString("credentials")));
+ }
+
+ @Test
+ public void testOidcBearerOnlyWithAuthzJson() {
+ String json = oidcBearerOnlyClientWithAuthz.getInstallationProvider("keycloak-oidc-keycloak-json");
+ assertOidcInstallationConfig(json);
+ assertThat(json, containsString("bearer-only"));
+ assertThat(json, not(containsString("public-client")));
+ assertThat(json, containsString("credentials"));
+ assertThat(json, containsString("secret"));
+ }
+
private void assertOidcInstallationConfig(String config) {
assertThat(config, containsString("master"));
assertThat(config, not(containsString(ApiUtil.findActiveKey(testRealmResource()).getPublicKey())));
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/group/GroupMappersTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/group/GroupMappersTest.java
index a1e6b0a..b1338e9 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/group/GroupMappersTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/group/GroupMappersTest.java
@@ -34,9 +34,6 @@ import org.keycloak.representations.idm.UserRepresentation;
import java.util.*;
-import static org.hamcrest.CoreMatchers.hasItems;
-import static org.hamcrest.MatcherAssert.assertThat;
-
/**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
@@ -119,7 +116,7 @@ public class GroupMappersTest extends AbstractGroupTest {
Assert.assertNotNull(groups);
Assert.assertTrue(groups.size() == 1);
Assert.assertEquals("topGroup", groups.get(0));
- Assert.assertEquals(Collections.singletonList("true"), token.getOtherClaims().get("topAttribute"));
+ Assert.assertEquals("true", token.getOtherClaims().get("topAttribute"));
}
{
UserRepresentation user = realm.users().search("level2GroupUser", -1, -1).get(0);
@@ -132,8 +129,8 @@ public class GroupMappersTest extends AbstractGroupTest {
Assert.assertNotNull(groups);
Assert.assertTrue(groups.size() == 1);
Assert.assertEquals("level2group", groups.get(0));
- Assert.assertEquals(Collections.singletonList("true"), token.getOtherClaims().get("topAttribute"));
- Assert.assertEquals(Collections.singletonList("true"), token.getOtherClaims().get("level2Attribute"));
+ Assert.assertEquals("true", token.getOtherClaims().get("topAttribute"));
+ Assert.assertEquals("true", token.getOtherClaims().get("level2Attribute"));
}
}
}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/group/GroupTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/group/GroupTest.java
index 91d299f..79908d8 100755
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/group/GroupTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/group/GroupTest.java
@@ -152,6 +152,32 @@ public class GroupTest extends AbstractGroupTest {
}
@Test
+ public void doNotAllowSameGroupNameAtSameLevel() throws Exception {
+ RealmResource realm = adminClient.realms().realm("test");
+
+ GroupRepresentation topGroup = new GroupRepresentation();
+ topGroup.setName("top");
+ topGroup = createGroup(realm, topGroup);
+
+ GroupRepresentation anotherTopGroup = new GroupRepresentation();
+ anotherTopGroup.setName("top");
+ Response response = realm.groups().add(anotherTopGroup);
+ assertEquals(409, response.getStatus()); // conflict status 409 - same name not allowed
+
+ GroupRepresentation level2Group = new GroupRepresentation();
+ level2Group.setName("level2");
+ response = realm.groups().group(topGroup.getId()).subGroup(level2Group);
+ response.close();
+ assertEquals(201, response.getStatus()); // created status
+
+ GroupRepresentation anotherlevel2Group = new GroupRepresentation();
+ anotherlevel2Group.setName("level2");
+ response = realm.groups().group(topGroup.getId()).subGroup(anotherlevel2Group);
+ response.close();
+ assertEquals(409, response.getStatus()); // conflict status 409 - same name not allowed
+ }
+
+ @Test
public void createAndTestGroups() throws Exception {
RealmResource realm = adminClient.realms().realm("test");
{
@@ -179,7 +205,7 @@ public class GroupTest extends AbstractGroupTest {
GroupRepresentation topGroup = new GroupRepresentation();
topGroup.setName("top");
topGroup = createGroup(realm, topGroup);
-
+
List<RoleRepresentation> roles = new LinkedList<>();
roles.add(topRole);
realm.groups().group(topGroup.getId()).roles().realmLevel().add(roles);
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/partialimport/PartialImportTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/partialimport/PartialImportTest.java
index 783adbf..e6f7348 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/partialimport/PartialImportTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/partialimport/PartialImportTest.java
@@ -56,6 +56,7 @@ import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
+import static org.keycloak.testsuite.auth.page.AuthRealm.MASTER;
/**
* Tests for the partial import endpoint in admin client. Also tests the
@@ -86,6 +87,7 @@ public class PartialImportTest extends AbstractAuthTest {
public void initAdminEvents() {
RealmRepresentation realmRep = RealmBuilder.edit(testRealmResource().toRepresentation()).testEventListener().build();
realmId = realmRep.getId();
+ realmRep.setDuplicateEmailsAllowed(false);
adminClient.realm(realmRep.getRealm()).update(realmRep);
piRep = new PartialImportRepresentation();
@@ -321,6 +323,40 @@ public class PartialImportTest extends AbstractAuthTest {
}
@Test
+ public void testAddUsersWithDuplicateEmailsForbidden() {
+ assertAdminEvents.clear();
+
+ setFail();
+ addUsers();
+
+ UserRepresentation user = createUserRepresentation(USER_PREFIX + 999, USER_PREFIX + 1 + "@foo.com", "foo", "bar", true);
+ piRep.getUsers().add(user);
+
+ Response response = testRealmResource().partialImport(piRep);
+ assertEquals(409, response.getStatus());
+ }
+
+ @Test
+ public void testAddUsersWithDuplicateEmailsAllowed() {
+
+ RealmRepresentation realmRep = new RealmRepresentation();
+ realmRep.setDuplicateEmailsAllowed(true);
+ adminClient.realm(realmId).update(realmRep);
+
+ assertAdminEvents.clear();
+
+ setFail();
+ addUsers();
+ doImport();
+
+ UserRepresentation user = createUserRepresentation(USER_PREFIX + 999, USER_PREFIX + 1 + "@foo.com", "foo", "bar", true);
+ piRep.setUsers(Arrays.asList(user));
+
+ PartialImportResults results = doImport();
+ assertEquals(1, results.getAdded());
+ }
+
+ @Test
public void testAddUsersWithTermsAndConditions() {
assertAdminEvents.clear();
@@ -590,4 +626,23 @@ public class PartialImportTest extends AbstractAuthTest {
assertEquals(NUM_ENTITIES * NUM_RESOURCE_TYPES, results.getOverwritten());
}
+ //KEYCLOAK-3042
+ @Test
+ public void testOverwriteExistingClientWithRoles() {
+ setOverwrite();
+
+ ClientRepresentation client = adminClient.realm(MASTER).clients().findByClientId("broker").get(0);
+ List<RoleRepresentation> clientRoles = adminClient.realm(MASTER).clients().get(client.getId()).roles().list();
+
+ Map<String, List<RoleRepresentation>> clients = new HashMap<>();
+ clients.put(client.getClientId(), clientRoles);
+
+ RolesRepresentation roles = new RolesRepresentation();
+ roles.setClient(clients);
+
+ piRep.setClients(Arrays.asList(client));
+ piRep.setRoles(roles);
+
+ doImport();
+ }
}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/PermissionsTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/PermissionsTest.java
index b23194a..18277d2 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/PermissionsTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/PermissionsTest.java
@@ -1173,7 +1173,9 @@ public class PermissionsTest extends AbstractKeycloakTest {
}, Resource.USER, false);
invoke(new InvocationWithResponse() {
public void invoke(RealmResource realm, AtomicReference<Response> response) {
- response.set(realm.groups().add(new GroupRepresentation()));
+ GroupRepresentation group = new GroupRepresentation();
+ group.setName("mygroup");
+ response.set(realm.groups().add(group));
}
}, Resource.USER, true);
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/realm/RealmTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/realm/RealmTest.java
index 26cf87b..d0a8bde 100755
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/realm/RealmTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/realm/RealmTest.java
@@ -412,6 +412,8 @@ public class RealmTest extends AbstractAdminTest {
if (realm.isRegistrationEmailAsUsername() != null) assertEquals(realm.isRegistrationEmailAsUsername(), storedRealm.isRegistrationEmailAsUsername());
if (realm.isRememberMe() != null) assertEquals(realm.isRememberMe(), storedRealm.isRememberMe());
if (realm.isVerifyEmail() != null) assertEquals(realm.isVerifyEmail(), storedRealm.isVerifyEmail());
+ if (realm.isLoginWithEmailAllowed() != null) assertEquals(realm.isLoginWithEmailAllowed(), storedRealm.isLoginWithEmailAllowed());
+ if (realm.isDuplicateEmailsAllowed() != null) assertEquals(realm.isDuplicateEmailsAllowed(), storedRealm.isDuplicateEmailsAllowed());
if (realm.isResetPasswordAllowed() != null) assertEquals(realm.isResetPasswordAllowed(), storedRealm.isResetPasswordAllowed());
if (realm.isEditUsernameAllowed() != null) assertEquals(realm.isEditUsernameAllowed(), storedRealm.isEditUsernameAllowed());
if (realm.getSslRequired() != null) assertEquals(realm.getSslRequired(), storedRealm.getSslRequired());
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerConfiguration.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerConfiguration.java
index 1fdb896..aa3e56e 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerConfiguration.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerConfiguration.java
@@ -87,7 +87,7 @@ public class KcOidcBrokerConfiguration implements BrokerConfiguration {
userAttrMapperConfig.put(OIDCAttributeMapperHelper.INCLUDE_IN_ACCESS_TOKEN, "true");
userAttrMapperConfig.put(OIDCAttributeMapperHelper.INCLUDE_IN_ID_TOKEN, "true");
userAttrMapperConfig.put(OIDCAttributeMapperHelper.INCLUDE_IN_USERINFO, "true");
-// userAttrMapperConfig.put(ProtocolMapperUtils.MULTIVALUED, "true");
+ userAttrMapperConfig.put(ProtocolMapperUtils.MULTIVALUED, "true");
client.setProtocolMappers(Arrays.asList(emailMapper, userAttrMapper));
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/AbstractCliTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/AbstractCliTest.java
new file mode 100644
index 0000000..8df18ae
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/AbstractCliTest.java
@@ -0,0 +1,54 @@
+package org.keycloak.testsuite.cli;
+
+import org.junit.Assert;
+import org.keycloak.testsuite.AbstractKeycloakTest;
+import org.keycloak.testsuite.cli.exec.AbstractExec;
+
+import java.util.List;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public abstract class AbstractCliTest extends AbstractKeycloakTest {
+
+
+ public void assertExitCodeAndStdOutSize(AbstractExec exe, int exitCode, int stdOutLineCount) {
+ assertExitCodeAndStreamSizes(exe, exitCode, stdOutLineCount, -1);
+ }
+
+ public void assertExitCodeAndStdErrSize(AbstractExec exe, int exitCode, int stdErrLineCount) {
+ assertExitCodeAndStreamSizes(exe, exitCode, -1, stdErrLineCount);
+ }
+
+ public void assertExitCodeAndStreamSizes(AbstractExec exe, int exitCode, int stdOutLineCount, int stdErrLineCount) {
+ Assert.assertEquals("exitCode == " + exitCode, exitCode, exe.exitCode());
+ if (stdOutLineCount != -1) {
+ try {
+ assertLineCount("stdout output", exe.stdoutLines(), stdOutLineCount);
+ } catch (Throwable e) {
+ throw new AssertionError("STDOUT: " + exe.stdoutString(), e);
+ }
+ }
+ if (stdErrLineCount != -1) {
+ try {
+ assertLineCount("stderr output", exe.stderrLines(), stdErrLineCount);
+ } catch (Throwable e) {
+ throw new AssertionError("STDERR: " + exe.stderrString(), e);
+ }
+ }
+ }
+
+ private void assertLineCount(String label, List<String> lines, int count) {
+ if (lines.size() == count) {
+ return;
+ }
+ // there is some kind of race condition in 'kcreg' that results in intermittent extra empty line
+ if (lines.size() == count + 1) {
+ if ("".equals(lines.get(lines.size()-1))) {
+ return;
+ }
+ }
+ Assert.assertTrue(label + " has " + lines.size() + " lines (expected: " + count + ")", lines.size() == count);
+ }
+
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/admin/AbstractAdmCliTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/admin/AbstractAdmCliTest.java
new file mode 100644
index 0000000..d86ddf1
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/admin/AbstractAdmCliTest.java
@@ -0,0 +1,387 @@
+package org.keycloak.testsuite.cli.admin;
+
+import org.junit.Assert;
+import org.keycloak.authentication.authenticators.client.ClientIdAndSecretAuthenticator;
+import org.keycloak.authentication.authenticators.client.JWTClientAuthenticator;
+import org.keycloak.client.admin.cli.config.ConfigData;
+import org.keycloak.client.admin.cli.config.FileConfigHandler;
+import org.keycloak.client.admin.cli.config.RealmConfigData;
+import org.keycloak.representations.idm.ClientRepresentation;
+import org.keycloak.representations.idm.RealmRepresentation;
+import org.keycloak.representations.idm.UserRepresentation;
+import org.keycloak.testsuite.cli.AbstractCliTest;
+import org.keycloak.testsuite.cli.KcAdmExec;
+import org.keycloak.testsuite.util.ClientBuilder;
+import org.keycloak.testsuite.util.UserBuilder;
+import org.keycloak.util.JsonSerialization;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.charset.Charset;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+
+import static org.keycloak.testsuite.admin.AbstractAdminTest.loadJson;
+import static org.keycloak.testsuite.cli.KcAdmExec.WORK_DIR;
+import static org.keycloak.testsuite.cli.KcAdmExec.execute;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public abstract class AbstractAdmCliTest extends AbstractCliTest {
+
+ protected String serverUrl = isAuthServerSSL() ?
+ "https://localhost:" + getAuthServerHttpsPort() + "/auth" :
+ "http://localhost:" + getAuthServerHttpPort() + "/auth";
+
+ static boolean runIntermittentlyFailingTests() {
+ return "true".equals(System.getProperty("test.intermittent"));
+ }
+
+ static boolean isAuthServerSSL() {
+ return "true".equals(System.getProperty("auth.server.ssl.required"));
+ }
+
+ static File getDefaultConfigFilePath() {
+ return new File(System.getProperty("user.home") + "/.keycloak/kcadm.config");
+ }
+
+ static int getAuthServerHttpsPort() {
+ try {
+ return Integer.valueOf(System.getProperty("auth.server.https.port"));
+ } catch (Exception e) {
+ throw new RuntimeException("System property 'auth.server.https.port' not set or invalid: '"
+ + System.getProperty("auth.server.https.port") + "'");
+ }
+ }
+
+ static int getAuthServerHttpPort() {
+ try {
+ return Integer.valueOf(System.getProperty("auth.server.http.port"));
+ } catch (Exception e) {
+ throw new RuntimeException("System property 'auth.server.http.port' not set or invalid: '"
+ + System.getProperty("auth.server.http.port") + "'");
+ }
+ }
+
+ @Override
+ public void addTestRealms(List<RealmRepresentation> testRealms) {
+
+ RealmRepresentation realmRepresentation = loadJson(getClass().getResourceAsStream("/testrealm.json"), RealmRepresentation.class);
+ testRealms.add(realmRepresentation);
+
+ // create admin user account with permissions to manage clients
+ UserRepresentation admin = UserBuilder.create()
+ .username("user1")
+ .password("userpass")
+ .enabled(true)
+ .build();
+ HashMap<String, List<String>> clientRoles = new HashMap<>();
+ clientRoles.put("realm-management", Arrays.asList("realm-admin"));
+ admin.setClientRoles(clientRoles);
+ realmRepresentation.getUsers().add(admin);
+
+
+
+ // create client with service account to use Signed JWT credentials with
+ ClientRepresentation regClient = ClientBuilder.create()
+ .clientId("admin-cli-jwt")
+ .attribute(JWTClientAuthenticator.CERTIFICATE_ATTR, "MIICnTCCAYUCBgFXUhpRTTANBgkqhkiG9w0BAQsFADASMRAwDgYDVQQDDAdyZWctY2xpMB4XDTE2MDkyMjEzMzIxOFoXDTI2MDkyMjEzMzM1OFowEjEQMA4GA1UEAwwHcmVnLWNsaTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMHZn/0Bk1M9oKcTHxzn2cGvBWwO1m6OVLQ8LSVwNIf4ixfGkVIkhI5iEGYND+uD8ame54ZPClTVxMra3JldClLIG+L+ymnbT2vKIhEsVvCROs9PnYxbFALt1dXneLIio2uzF+d7/zQWlmeaWfNunSJT1aHNJDkGgDeUuQa25b0IMqsFjsN8Dg4ATkA97r3wKn4Tp3SE7sTM/B2pmra4atNxGeShVrgihqUiQ/PwDiDGwry64AsexkZnQsCR3bJWBAVUiHef3JWzTfWWN5bfCBG6Mnq1xw7YN+YpV1nR3CGmcKJuLe6aTe7Ps8hYejYiQA7Mp7ZQsoImsVFV5HDOlb0CAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAZl8XvLfKXTPYvq/QyHOg7EDlAdlV3HkmHP9SBAV4BccmHmorMkm5I6I21UA5mfju+0nhbEd0bm0kvJFxIfNU6lJyyVvQx3Gns37KYUOzIV/ocWZuOTBLp5tfIBYbBwfE/s1J4PhpA/3WhBY9JKiLvdJfxECGIgaLs2M0UsylW/7o04+18Od8j/m7crQc7fpe5gJB5m/+hxUDowIjG5CumffX9OHYGDvHBpaUl7QNSGgjP8Bn9ogmIMUBJ7XSYUcohKuk2Cnj6p+GlLuqHbOISUXLVjf0DxhCu6diVxvacKbgAZmyCIO1tGL/UVRxg9GOYdCiC9vHfPuZ8US+ZB0P9g==")
+ .authenticatorType(JWTClientAuthenticator.PROVIDER_ID)
+ .serviceAccount()
+ .build();
+
+ realmRepresentation.getClients().add(regClient);
+
+ // create service account for client reg-cli with permissions to manage clients
+ addServiceAccount(realmRepresentation, "admin-cli-jwt");
+
+
+
+ // create client to use with user account - enable direct grants
+ regClient = ClientBuilder.create()
+ .clientId("admin-cli-jwt-direct")
+ .attribute(JWTClientAuthenticator.CERTIFICATE_ATTR, "MIICnTCCAYUCBgFXUhpRTTANBgkqhkiG9w0BAQsFADASMRAwDgYDVQQDDAdyZWctY2xpMB4XDTE2MDkyMjEzMzIxOFoXDTI2MDkyMjEzMzM1OFowEjEQMA4GA1UEAwwHcmVnLWNsaTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMHZn/0Bk1M9oKcTHxzn2cGvBWwO1m6OVLQ8LSVwNIf4ixfGkVIkhI5iEGYND+uD8ame54ZPClTVxMra3JldClLIG+L+ymnbT2vKIhEsVvCROs9PnYxbFALt1dXneLIio2uzF+d7/zQWlmeaWfNunSJT1aHNJDkGgDeUuQa25b0IMqsFjsN8Dg4ATkA97r3wKn4Tp3SE7sTM/B2pmra4atNxGeShVrgihqUiQ/PwDiDGwry64AsexkZnQsCR3bJWBAVUiHef3JWzTfWWN5bfCBG6Mnq1xw7YN+YpV1nR3CGmcKJuLe6aTe7Ps8hYejYiQA7Mp7ZQsoImsVFV5HDOlb0CAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAZl8XvLfKXTPYvq/QyHOg7EDlAdlV3HkmHP9SBAV4BccmHmorMkm5I6I21UA5mfju+0nhbEd0bm0kvJFxIfNU6lJyyVvQx3Gns37KYUOzIV/ocWZuOTBLp5tfIBYbBwfE/s1J4PhpA/3WhBY9JKiLvdJfxECGIgaLs2M0UsylW/7o04+18Od8j/m7crQc7fpe5gJB5m/+hxUDowIjG5CumffX9OHYGDvHBpaUl7QNSGgjP8Bn9ogmIMUBJ7XSYUcohKuk2Cnj6p+GlLuqHbOISUXLVjf0DxhCu6diVxvacKbgAZmyCIO1tGL/UVRxg9GOYdCiC9vHfPuZ8US+ZB0P9g==")
+ .authenticatorType(JWTClientAuthenticator.PROVIDER_ID)
+ .directAccessGrants()
+ .build();
+
+ realmRepresentation.getClients().add(regClient);
+
+
+
+
+ // create client with service account to use client secret with
+ regClient = ClientBuilder.create()
+ .clientId("admin-cli-secret")
+ .secret("password")
+ .authenticatorType(ClientIdAndSecretAuthenticator.PROVIDER_ID)
+ .serviceAccount()
+ .build();
+
+ realmRepresentation.getClients().add(regClient);
+
+ // create service account for client reg-cli with permissions to manage clients
+ addServiceAccount(realmRepresentation, "admin-cli-secret");
+
+
+
+
+ // create client to use with user account - enable direct grants
+ regClient = ClientBuilder.create()
+ .clientId("admin-cli-secret-direct")
+ .secret("password")
+ .authenticatorType(ClientIdAndSecretAuthenticator.PROVIDER_ID)
+ .directAccessGrants()
+ .build();
+
+ realmRepresentation.getClients().add(regClient);
+
+ }
+
+ FileConfigHandler initCustomConfigFile() {
+ String filename = UUID.randomUUID().toString() + ".config";
+ File cfgFile = new File(WORK_DIR + "/" + filename);
+ FileConfigHandler handler = new FileConfigHandler();
+ handler.setConfigFile(cfgFile.getAbsolutePath());
+ return handler;
+ }
+
+ void assertFieldsEqualWithExclusions(ConfigData config1, ConfigData config2, String ... excluded) {
+
+ HashSet<String> exclusions = new HashSet<>(Arrays.asList(excluded));
+
+ if (!exclusions.contains("serverUrl")) {
+ Assert.assertEquals("serverUrl", config1.getServerUrl(), config2.getServerUrl());
+ }
+ if (!exclusions.contains("realm")) {
+ Assert.assertEquals("realm", config1.getRealm(), config2.getRealm());
+ }
+ if (!exclusions.contains("truststore")) {
+ Assert.assertEquals("truststore", config1.getTruststore(), config2.getTruststore());
+ }
+ if (!exclusions.contains("endpoints")) {
+ Map<String, Map<String, RealmConfigData>> endp1 = config1.getEndpoints();
+ Map<String, Map<String, RealmConfigData>> endp2 = config2.getEndpoints();
+
+ Iterator<Map.Entry<String, Map<String, RealmConfigData>>> it1 = endp1.entrySet().iterator();
+ Iterator<Map.Entry<String, Map<String, RealmConfigData>>> it2 = endp2.entrySet().iterator();
+
+ while (it1.hasNext()) {
+ Map.Entry<String, Map<String, RealmConfigData>> ent1 = it1.next();
+ Map.Entry<String, Map<String, RealmConfigData>> ent2 = it2.next();
+
+ String serverUrl = ent1.getKey();
+ String endpskey = "endpoints." + serverUrl;
+ if (!exclusions.contains(endpskey)) {
+ Assert.assertEquals(endpskey, ent1.getKey(), ent2.getKey());
+
+ Map<String, RealmConfigData> realms1 = ent1.getValue();
+ Map<String, RealmConfigData> realms2 = ent2.getValue();
+
+ Iterator<Map.Entry<String, RealmConfigData>> rit1 = realms1.entrySet().iterator();
+ Iterator<Map.Entry<String, RealmConfigData>> rit2 = realms2.entrySet().iterator();
+
+ while (rit1.hasNext()) {
+ Map.Entry<String, RealmConfigData> rent1 = rit1.next();
+ Map.Entry<String, RealmConfigData> rent2 = rit2.next();
+
+ String realm = rent1.getKey();
+ String rkey = endpskey + "." + realm;
+ if (!exclusions.contains(endpskey)) {
+ Assert.assertEquals(rkey, rent1.getKey(), rent2.getKey());
+
+ RealmConfigData rdata1 = rent1.getValue();
+ RealmConfigData rdata2 = rent2.getValue();
+
+ assertFieldsEqualWithExclusions(serverUrl, realm, rdata1, rdata2, excluded);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ void assertFieldsEqualWithExclusions(String server, String realm, RealmConfigData data1, RealmConfigData data2, String ... excluded) {
+
+ HashSet<String> exclusions = new HashSet<>(Arrays.asList(excluded));
+
+ String pfix = "";
+ if (server != null || realm != null) {
+ pfix = "endpoints." + server + "." + realm + ".";
+ }
+
+ String ekey = pfix + "serverUrl";
+ if (!exclusions.contains(ekey)) {
+ Assert.assertEquals(ekey, data1.serverUrl(), data2.serverUrl());
+ }
+
+ ekey = pfix + "realm";
+ if (!exclusions.contains(ekey)) {
+ Assert.assertEquals(ekey, data1.realm(), data2.realm());
+ }
+
+ ekey = pfix + "clientId";
+ if (!exclusions.contains(ekey)) {
+ Assert.assertEquals(ekey, data1.getClientId(), data2.getClientId());
+ }
+
+ ekey = pfix + "token";
+ if (!exclusions.contains(ekey)) {
+ Assert.assertEquals(ekey, data1.getToken(), data2.getToken());
+ }
+
+ ekey = pfix + "refreshToken";
+ if (!exclusions.contains(ekey)) {
+ Assert.assertEquals(ekey, data1.getRefreshToken(), data2.getRefreshToken());
+ }
+
+ ekey = pfix + "expiresAt";
+ if (!exclusions.contains(ekey)) {
+ Assert.assertEquals(ekey, data1.getExpiresAt(), data2.getExpiresAt());
+ }
+
+ ekey = pfix + "refreshExpiresAt";
+ if (!exclusions.contains(ekey)) {
+ Assert.assertEquals(ekey, data1.getRefreshExpiresAt(), data2.getRefreshExpiresAt());
+ }
+
+ ekey = pfix + "secret";
+ if (!exclusions.contains(ekey)) {
+ Assert.assertEquals(ekey, data1.getSecret(), data2.getSecret());
+ }
+
+ ekey = pfix + "signingToken";
+ if (!exclusions.contains(ekey)) {
+ Assert.assertEquals(ekey, data1.getSigningToken(), data2.getSigningToken());
+ }
+
+ ekey = pfix + "sigExpiresAt";
+ if (!exclusions.contains(ekey)) {
+ Assert.assertEquals(ekey, data1.getSigExpiresAt(), data2.getSigExpiresAt());
+ }
+ }
+
+ void testCRUDWithOnTheFlyAuth(String serverUrl, String credentials, String extraOptions, String loginMessage) throws IOException {
+
+ File configFile = getDefaultConfigFilePath();
+ long lastModified = configFile.exists() ? configFile.lastModified() : 0;
+
+ // This test assumes it is the only user of any instance of on the system
+ KcAdmExec exe = execute("create clients --no-config --server " + serverUrl +
+ " --realm test " + credentials + " " + extraOptions + " -s clientId=test-client -o");
+
+ Assert.assertEquals("exitCode == 0", 0, exe.exitCode());
+ Assert.assertEquals("login message", loginMessage, exe.stderrLines().get(0));
+
+ ClientRepresentation client = JsonSerialization.readValue(exe.stdout(), ClientRepresentation.class);
+ Assert.assertEquals("clientId", "test-client", client.getClientId());
+
+ long lastModified2 = configFile.exists() ? configFile.lastModified() : 0;
+ Assert.assertEquals("config file not modified", lastModified, lastModified2);
+
+
+
+
+ exe = execute("get clients/" + client.getId() + " --no-config --server " + serverUrl + " --realm test " + credentials + " " + extraOptions);
+
+ assertExitCodeAndStdErrSize(exe, 0, 1);
+
+ ClientRepresentation client2 = JsonSerialization.readValue(exe.stdout(), ClientRepresentation.class);
+ Assert.assertEquals("clientId", "test-client", client2.getClientId());
+
+ lastModified2 = configFile.exists() ? configFile.lastModified() : 0;
+ Assert.assertEquals("config file not modified", lastModified, lastModified2);
+
+
+
+
+ exe = execute("update clients/" + client.getId() + " --no-config --server " + serverUrl + " --realm test " +
+ credentials + " " + extraOptions + " -s enabled=false -o");
+
+ assertExitCodeAndStdErrSize(exe, 0, 1);
+
+ ClientRepresentation client4 = JsonSerialization.readValue(exe.stdout(), ClientRepresentation.class);
+ Assert.assertEquals("clientId", "test-client", client4.getClientId());
+ Assert.assertFalse("enabled", client4.isEnabled());
+
+ lastModified2 = configFile.exists() ? configFile.lastModified() : 0;
+ Assert.assertEquals("config file not modified", lastModified, lastModified2);
+
+
+
+
+ exe = execute("delete clients/" + client.getId() + " --no-config --server " + serverUrl + " --realm test " + credentials + " " + extraOptions);
+
+ assertExitCodeAndStreamSizes(exe, 0, 0, 1);
+
+ lastModified2 = configFile.exists() ? configFile.lastModified() : 0;
+ Assert.assertEquals("config file not modified", lastModified, lastModified2);
+
+
+
+
+ // subsequent delete should fail
+ exe = execute("delete clients/" + client.getId() + " --no-config --server " + serverUrl + " --realm test " + credentials + " " + extraOptions);
+
+ assertExitCodeAndStreamSizes(exe, 1, 0, 2);
+ String resourceUri = serverUrl + "/admin/realms/test/clients/" + client.getId();
+ Assert.assertEquals("error message", "Resource not found for url: " + resourceUri, exe.stderrLines().get(1));
+
+ lastModified2 = configFile.exists() ? configFile.lastModified() : 0;
+ Assert.assertEquals("config file not modified", lastModified, lastModified2);
+ }
+
+ File initTempFile(String extension) throws IOException {
+ return initTempFile(extension, null);
+ }
+
+ File initTempFile(String extension, String content) throws IOException {
+ String filename = UUID.randomUUID().toString() + extension;
+ File file = new File(KcAdmExec.WORK_DIR + "/" + filename);
+ if (content != null) {
+ OutputStream os = new FileOutputStream(file);
+ os.write(content.getBytes(Charset.forName("iso_8859_1")));
+ os.close();
+ }
+ return file;
+ }
+
+ void addServiceAccount(RealmRepresentation realm, String clientId) {
+
+ UserRepresentation account = UserBuilder.create()
+ .username("service-account-" + clientId)
+ .enabled(true)
+ .serviceAccountId(clientId)
+ .build();
+
+ HashMap<String, List<String>> clientRoles = new HashMap<>();
+ clientRoles.put("realm-management", Arrays.asList("realm-admin"));
+
+ account.setClientRoles(clientRoles);
+
+ realm.getUsers().add(account);
+ }
+
+ void loginAsUser(File configFile, String server, String realm, String user, String password) {
+
+ KcAdmExec exe = KcAdmExec.execute("config credentials --server " + server + " --realm " + realm +
+ " --user " + user + " --password " + password + " --config " + configFile.getAbsolutePath());
+
+ Assert.assertEquals("exitCode == 0", 0, exe.exitCode());
+
+ List<String> lines = exe.stdoutLines();
+ Assert.assertTrue("stdout output empty", lines.size() == 0);
+
+ lines = exe.stderrLines();
+ Assert.assertTrue("stderr output one line", lines.size() == 1);
+ Assert.assertEquals("stderr first line", "Logging into " + server + " as user " + user + " of realm " + realm, lines.get(0));
+ }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/admin/KcAdmCreateTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/admin/KcAdmCreateTest.java
new file mode 100644
index 0000000..2481b58
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/admin/KcAdmCreateTest.java
@@ -0,0 +1,131 @@
+package org.keycloak.testsuite.cli.admin;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.keycloak.client.admin.cli.config.FileConfigHandler;
+import org.keycloak.representations.idm.ClientRepresentation;
+import org.keycloak.testsuite.cli.KcAdmExec;
+import org.keycloak.testsuite.util.TempFileResource;
+import org.keycloak.util.JsonSerialization;
+
+import java.io.IOException;
+import java.util.Arrays;
+
+import static org.keycloak.testsuite.cli.KcAdmExec.execute;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public class KcAdmCreateTest extends AbstractAdmCliTest {
+
+ @Test
+ public void testCreateWithRealmOverride() throws IOException {
+
+ FileConfigHandler handler = initCustomConfigFile();
+
+ try (TempFileResource configFile = new TempFileResource(handler.getConfigFile())) {
+
+ // authenticate as a regular user against one realm
+ KcAdmExec exe = execute("config credentials -x --config '" + configFile.getName() +
+ "' --server " + serverUrl + " --realm master --user admin --password admin");
+
+ assertExitCodeAndStreamSizes(exe, 0, 0, 1);
+
+ exe = execute("create clients --config '" + configFile.getName() + "' --server " + serverUrl + " -r test -s clientId=my_first_client");
+
+ assertExitCodeAndStreamSizes(exe, 0, 0, 1);
+ }
+ }
+
+
+ @Test
+ public void testCreateThoroughly() throws IOException {
+
+ FileConfigHandler handler = initCustomConfigFile();
+
+ try (TempFileResource configFile = new TempFileResource(handler.getConfigFile())) {
+
+ final String realm = "test";
+
+ // authenticate as a regular user against one realm
+ KcAdmExec exe = KcAdmExec.execute("config credentials -x --config '" + configFile.getName() +
+ "' --server " + serverUrl + " --realm master --user admin --password admin");
+
+ assertExitCodeAndStreamSizes(exe, 0, 0, 1);
+
+ // create configuration from file using stdin redirect ... output an object
+ String content = "{\n" +
+ " \"clientId\": \"my_client\",\n" +
+ " \"enabled\": true,\n" +
+ " \"redirectUris\": [\"http://localhost:8980/myapp/*\"],\n" +
+ " \"serviceAccountsEnabled\": true,\n" +
+ " \"name\": \"My Client App\",\n" +
+ " \"implicitFlowEnabled\": false,\n" +
+ " \"publicClient\": true,\n" +
+ " \"webOrigins\": [\"http://localhost:8980/myapp\"],\n" +
+ " \"consentRequired\": false,\n" +
+ " \"baseUrl\": \"http://localhost:8980/myapp\",\n" +
+ " \"bearerOnly\": true,\n" +
+ " \"standardFlowEnabled\": true\n" +
+ "}";
+
+ try (TempFileResource tmpFile = new TempFileResource(initTempFile(".json", content))) {
+
+ exe = execute("create clients --config '" + configFile.getName() + "' -o -f - < '" + tmpFile.getName() + "'");
+
+ assertExitCodeAndStdErrSize(exe, 0, 0);
+
+ ClientRepresentation client = JsonSerialization.readValue(exe.stdout(), ClientRepresentation.class);
+ Assert.assertNotNull("id", client.getId());
+ Assert.assertEquals("clientId", "my_client", client.getClientId());
+ Assert.assertEquals("enabled", true, client.isEnabled());
+ Assert.assertEquals("redirectUris", Arrays.asList("http://localhost:8980/myapp/*"), client.getRedirectUris());
+ Assert.assertEquals("serviceAccountsEnabled", true, client.isServiceAccountsEnabled());
+ Assert.assertEquals("name", "My Client App", client.getName());
+ Assert.assertEquals("implicitFlowEnabled", false, client.isImplicitFlowEnabled());
+ Assert.assertEquals("publicClient", true, client.isPublicClient());
+ // note there is no server-side check if protocol is supported
+ Assert.assertEquals("webOrigins", Arrays.asList("http://localhost:8980/myapp"), client.getWebOrigins());
+ Assert.assertEquals("consentRequired", false, client.isConsentRequired());
+ Assert.assertEquals("baseUrl", "http://localhost:8980/myapp", client.getBaseUrl());
+ Assert.assertEquals("bearerOnly", true, client.isStandardFlowEnabled());
+ Assert.assertFalse("mappers not empty", client.getProtocolMappers().isEmpty());
+
+ // create configuration from file as a template and override clientId and other attributes ... output an object
+ exe = execute("create clients --config '" + configFile.getName() + "' -o -f '" + tmpFile.getName() +
+ "' -s clientId=my_client2 -s enabled=false -s 'redirectUris=[\"http://localhost:8980/myapp2/*\"]'" +
+ " -s 'name=My Client App II' -s 'webOrigins=[\"http://localhost:8980/myapp2\"]'" +
+ " -s baseUrl=http://localhost:8980/myapp2 -s rootUrl=http://localhost:8980/myapp2");
+
+ assertExitCodeAndStdErrSize(exe, 0, 0);
+
+ ClientRepresentation client2 = JsonSerialization.readValue(exe.stdout(), ClientRepresentation.class);
+ Assert.assertNotNull("id", client2.getId());
+ Assert.assertEquals("clientId", "my_client2", client2.getClientId());
+ Assert.assertEquals("enabled", false, client2.isEnabled());
+ Assert.assertEquals("redirectUris", Arrays.asList("http://localhost:8980/myapp2/*"), client2.getRedirectUris());
+ Assert.assertEquals("serviceAccountsEnabled", true, client2.isServiceAccountsEnabled());
+ Assert.assertEquals("name", "My Client App II", client2.getName());
+ Assert.assertEquals("implicitFlowEnabled", false, client2.isImplicitFlowEnabled());
+ Assert.assertEquals("publicClient", true, client2.isPublicClient());
+ Assert.assertEquals("webOrigins", Arrays.asList("http://localhost:8980/myapp2"), client2.getWebOrigins());
+ Assert.assertEquals("consentRequired", false, client2.isConsentRequired());
+ Assert.assertEquals("baseUrl", "http://localhost:8980/myapp2", client2.getBaseUrl());
+ Assert.assertEquals("rootUrl", "http://localhost:8980/myapp2", client2.getRootUrl());
+ Assert.assertEquals("bearerOnly", true, client2.isStandardFlowEnabled());
+ Assert.assertFalse("mappers not empty", client2.getProtocolMappers().isEmpty());
+ }
+
+ // simple create, output an id
+ exe = execute("create clients --config '" + configFile.getName() + "' -i -s clientId=my_client3");
+
+ assertExitCodeAndStreamSizes(exe, 0, 1, 0);
+
+ // simple create, default output
+ exe = execute("create clients --config '" + configFile.getName() + "' -s clientId=my_client4");
+
+ assertExitCodeAndStreamSizes(exe, 0, 0, 1);
+ Assert.assertTrue("only id returned", exe.stderrLines().get(0).startsWith("Created new client with id '"));
+ }
+ }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/admin/KcAdmTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/admin/KcAdmTest.java
new file mode 100644
index 0000000..fe2caa4
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/admin/KcAdmTest.java
@@ -0,0 +1,561 @@
+package org.keycloak.testsuite.cli.admin;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.keycloak.client.admin.cli.config.ConfigData;
+import org.keycloak.client.admin.cli.config.FileConfigHandler;
+import org.keycloak.client.admin.cli.config.RealmConfigData;
+import org.keycloak.representations.idm.ClientRepresentation;
+import org.keycloak.testsuite.cli.KcAdmExec;
+import org.keycloak.testsuite.util.TempFileResource;
+import org.keycloak.util.JsonSerialization;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.List;
+import java.util.UUID;
+
+import static org.keycloak.client.admin.cli.util.OsUtil.CMD;
+import static org.keycloak.client.admin.cli.util.OsUtil.EOL;
+import static org.keycloak.testsuite.cli.KcAdmExec.execute;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public class KcAdmTest extends AbstractAdmCliTest {
+
+ @Test
+ public void testBadCommand() {
+ /*
+ * Test most basic execution with non-existent command
+ */
+ KcAdmExec exe = execute("nonexistent");
+
+ assertExitCodeAndStreamSizes(exe, 1, 0, 1);
+ Assert.assertEquals("stderr first line", "Unknown command: nonexistent", exe.stderrLines().get(0));
+ }
+
+
+ @Test
+ public void testNoArgs() {
+ /*
+ * Test (sub)commands without any arguments
+ */
+ KcAdmExec exe = KcAdmExec.execute("");
+
+ assertExitCodeAndStdErrSize(exe, 1, 0);
+
+ List<String> lines = exe.stdoutLines();
+ Assert.assertTrue("stdout output not empty", lines.size() > 0);
+ Assert.assertEquals("stdout first line", "Keycloak Admin CLI", lines.get(0));
+ Assert.assertEquals("stdout one but last line", "Use '" + KcAdmExec.CMD + " help <command>' for more information about a given command.", lines.get(lines.size() - 2));
+ Assert.assertEquals("stdout last line", "", lines.get(lines.size() - 1));
+
+
+ /*
+ * Test commands without arguments
+ */
+ exe = KcAdmExec.execute("config");
+ assertExitCodeAndStreamSizes(exe, 1, 0, 1);
+ Assert.assertEquals("error message",
+ "Sub-command required by '" + CMD + " config' - one of: 'credentials', 'truststore'",
+ exe.stderrLines().get(0));
+
+ exe = KcAdmExec.execute("config credentials");
+ assertExitCodeAndStdErrSize(exe, 1, 0);
+ Assert.assertTrue("help message returned", exe.stdoutLines().size() > 10);
+ Assert.assertEquals("help message", "Usage: " + CMD + " config credentials --server SERVER_URL --realm REALM --user USER [--password PASSWORD] [ARGUMENTS]", exe.stdoutLines().get(0));
+
+ exe = KcAdmExec.execute("config truststore");
+ assertExitCodeAndStdErrSize(exe, 1, 0);
+ Assert.assertTrue("help message returned", exe.stdoutLines().size() > 10);
+ Assert.assertEquals("help message", "Usage: " + CMD + " config truststore [TRUSTSTORE | --delete] [--trustpass PASSWORD] [ARGUMENTS]", exe.stdoutLines().get(0));
+
+ exe = KcAdmExec.execute("create");
+ assertExitCodeAndStdErrSize(exe, 1, 0);
+ Assert.assertTrue("help message returned", exe.stdoutLines().size() > 10);
+ Assert.assertEquals("help message", "Usage: " + CMD + " create ENDPOINT_URI [ARGUMENTS]", exe.stdoutLines().get(0));
+ //Assert.assertEquals("error message", "No file nor attribute values specified", exe.stderrLines().get(0));
+
+ exe = KcAdmExec.execute("get");
+ assertExitCodeAndStdErrSize(exe, 1, 0);
+ Assert.assertTrue("help message returned", exe.stdoutLines().size() > 10);
+ Assert.assertEquals("help message", "Usage: " + CMD + " get ENDPOINT_URI [ARGUMENTS]", exe.stdoutLines().get(0));
+ //Assert.assertEquals("error message", "CLIENT not specified", exe.stderrLines().get(0));
+
+ exe = KcAdmExec.execute("update");
+ assertExitCodeAndStdErrSize(exe, 1, 0);
+ Assert.assertTrue("help message returned", exe.stdoutLines().size() > 10);
+ Assert.assertEquals("help message", "Usage: " + CMD + " update ENDPOINT_URI [ARGUMENTS]", exe.stdoutLines().get(0));
+ //Assert.assertEquals("error message", "No file nor attribute values specified", exe.stderrLines().get(0));
+
+ exe = KcAdmExec.execute("delete");
+ assertExitCodeAndStdErrSize(exe, 1, 0);
+ Assert.assertTrue("help message returned", exe.stdoutLines().size() > 10);
+ Assert.assertEquals("help message", "Usage: " + CMD + " delete ENDPOINT_URI [ARGUMENTS]", exe.stdoutLines().get(0));
+ //Assert.assertEquals("error message", "CLIENT not specified", exe.stderrLines().get(0));
+
+ //exe = KcAdmExec.execute("get-roles");
+ //assertExitCodeAndStdErrSize(exe, 0, 0);
+ //try {
+ // JsonNode node = JsonSerialization.readValue(exe.stdout(), JsonNode.class);
+ // Assert.assertTrue("is JSON array", node.isArray());
+ //} catch (IOException e) {
+ // throw new AssertionError("Response should be a JSON array", e);
+ //}
+
+ //Assert.assertTrue("JSON message returned", exe.stdoutLines().size() > 10);
+ //Assert.assertTrue("help message returned", exe.stdoutLines().size() > 10);
+ //Assert.assertEquals("help message", "Usage: " + CMD + " get-roles [--cclientid CLIENT_ID | --cid ID] [ARGUMENTS]", exe.stdoutLines().get(0));
+
+ exe = KcAdmExec.execute("add-roles");
+ assertExitCodeAndStdErrSize(exe, 1, 0);
+ Assert.assertTrue("help message returned", exe.stdoutLines().size() > 10);
+ Assert.assertEquals("help message", "Usage: " + CMD + " add-roles (--uusername USERNAME | --uid ID) [--cclientid CLIENT_ID | --cid ID] (--rolename NAME | --roleid ID)+ [ARGUMENTS]", exe.stdoutLines().get(0));
+ //Assert.assertEquals("error message", "CLIENT not specified", exe.stderrLines().get(0));
+
+ exe = KcAdmExec.execute("remove-roles");
+ assertExitCodeAndStdErrSize(exe, 1, 0);
+ Assert.assertTrue("help message returned", exe.stdoutLines().size() > 10);
+ Assert.assertEquals("help message", "Usage: " + CMD + " remove-roles (--uusername USERNAME | --uid ID) [--cclientid CLIENT_ID | --cid ID] (--rolename NAME | --roleid ID)+ [ARGUMENTS]", exe.stdoutLines().get(0));
+
+ exe = KcAdmExec.execute("set-password");
+ assertExitCodeAndStdErrSize(exe, 1, 0);
+ Assert.assertTrue("help message returned", exe.stdoutLines().size() > 10);
+ Assert.assertEquals("help message", "Usage: " + CMD + " set-password (--username USERNAME | --userid ID) [--password PASSWORD] [ARGUMENTS]", exe.stdoutLines().get(0));
+ //Assert.assertEquals("error message", "CLIENT not specified", exe.stderrLines().get(0));
+
+ exe = KcAdmExec.execute("help");
+ assertExitCodeAndStdErrSize(exe, 0, 0);
+ lines = exe.stdoutLines();
+ Assert.assertTrue("stdout output not empty", lines.size() > 0);
+ Assert.assertEquals("stdout first line", "Keycloak Admin CLI", lines.get(0));
+ Assert.assertEquals("stdout one but last line", "Use '" + KcAdmExec.CMD + " help <command>' for more information about a given command.", lines.get(lines.size() - 2));
+ Assert.assertEquals("stdout last line", "", lines.get(lines.size() - 1));
+ }
+
+ @Test
+ public void testHelpGlobalOption() {
+ /*
+ * Test --help for all commands
+ */
+ KcAdmExec exe = KcAdmExec.execute("--help");
+ assertExitCodeAndStdErrSize(exe, 0, 0);
+ Assert.assertEquals("stdout first line", "Keycloak Admin CLI", exe.stdoutLines().get(0));
+
+ exe = KcAdmExec.execute("create --help");
+ assertExitCodeAndStdErrSize(exe, 0, 0);
+ Assert.assertEquals("stdout first line", "Usage: " + CMD + " create ENDPOINT_URI [ARGUMENTS]", exe.stdoutLines().get(0));
+
+ exe = KcAdmExec.execute("get --help");
+ assertExitCodeAndStdErrSize(exe, 0, 0);
+ Assert.assertEquals("stdout first line", "Usage: " + CMD + " get ENDPOINT_URI [ARGUMENTS]", exe.stdoutLines().get(0));
+
+ exe = KcAdmExec.execute("update --help");
+ assertExitCodeAndStdErrSize(exe, 0, 0);
+ Assert.assertEquals("stdout first line", "Usage: " + CMD + " update ENDPOINT_URI [ARGUMENTS]", exe.stdoutLines().get(0));
+
+ exe = KcAdmExec.execute("delete --help");
+ assertExitCodeAndStdErrSize(exe, 0, 0);
+ Assert.assertEquals("stdout first line", "Usage: " + CMD + " delete ENDPOINT_URI [ARGUMENTS]", exe.stdoutLines().get(0));
+
+ exe = KcAdmExec.execute("get-roles --help");
+ assertExitCodeAndStdErrSize(exe, 0, 0);
+ Assert.assertEquals("stdout first line", "Usage: " + CMD + " get-roles [--cclientid CLIENT_ID | --cid ID] [ARGUMENTS]", exe.stdoutLines().get(0));
+
+ exe = KcAdmExec.execute("add-roles --help");
+ assertExitCodeAndStdErrSize(exe, 0, 0);
+ Assert.assertEquals("stdout first line", "Usage: " + CMD + " add-roles (--uusername USERNAME | --uid ID) [--cclientid CLIENT_ID | --cid ID] (--rolename NAME | --roleid ID)+ [ARGUMENTS]", exe.stdoutLines().get(0));
+
+ exe = KcAdmExec.execute("remove-roles --help");
+ assertExitCodeAndStdErrSize(exe, 0, 0);
+ Assert.assertEquals("stdout first line", "Usage: " + CMD + " remove-roles (--uusername USERNAME | --uid ID) [--cclientid CLIENT_ID | --cid ID] (--rolename NAME | --roleid ID)+ [ARGUMENTS]", exe.stdoutLines().get(0));
+
+ exe = KcAdmExec.execute("set-password --help");
+ assertExitCodeAndStdErrSize(exe, 0, 0);
+ Assert.assertEquals("stdout first line", "Usage: " + CMD + " set-password (--username USERNAME | --userid ID) [--password PASSWORD] [ARGUMENTS]", exe.stdoutLines().get(0));
+
+ exe = KcAdmExec.execute("config --help");
+ assertExitCodeAndStdErrSize(exe, 0, 0);
+ Assert.assertEquals("stdout first line", "Usage: " + CMD + " config SUB_COMMAND [ARGUMENTS]", exe.stdoutLines().get(0));
+
+ exe = KcAdmExec.execute("config credentials --help");
+ assertExitCodeAndStdErrSize(exe, 0, 0);
+ Assert.assertEquals("stdout first line",
+ "Usage: " + CMD + " config credentials --server SERVER_URL --realm REALM --user USER [--password PASSWORD] [ARGUMENTS]",
+ exe.stdoutLines().get(0));
+
+ exe = KcAdmExec.execute("config truststore --help");
+ assertExitCodeAndStdErrSize(exe, 0, 0);
+ Assert.assertEquals("stdout first line",
+ "Usage: " + CMD + " config truststore [TRUSTSTORE | --delete] [--trustpass PASSWORD] [ARGUMENTS]",
+ exe.stdoutLines().get(0));
+ }
+
+ @Test
+ public void testBadOptionInPlaceOfCommand() {
+ /*
+ * Test most basic execution with non-existent option
+ */
+ KcAdmExec exe = KcAdmExec.execute("--nonexistent");
+
+ assertExitCodeAndStreamSizes(exe, 1, 0, 1);
+ Assert.assertEquals("stderr first line", "Unknown command: --nonexistent", exe.stderrLines().get(0));
+ }
+
+ @Test
+ public void testBadOption() {
+ /*
+ * Test sub-command execution with non-existent option
+ */
+
+ KcAdmExec exe = KcAdmExec.execute("get users --nonexistent");
+
+ assertExitCodeAndStreamSizes(exe, 1, 0, 2);
+ Assert.assertEquals("stderr first line", "Invalid option: --nonexistent", exe.stderrLines().get(0));
+ Assert.assertEquals("try help", "Try '" + CMD + " help get' for more information", exe.stderrLines().get(1));
+
+ // set-password doesn't use @Arguments injection thus unsupported options are handled by Aesh
+ exe = KcAdmExec.execute("set-password --nonexistent");
+
+ assertExitCodeAndStreamSizes(exe, 1, 0, 2);
+ Assert.assertEquals("stderr first line", "Invalid option: --nonexistent", exe.stderrLines().get(0));
+ Assert.assertEquals("try help", "Try '" + CMD + " help set-password' for more information", exe.stderrLines().get(1));
+ }
+
+ @Test
+ public void testCredentialsServerAndRealmWithDefaultConfig() {
+ /*
+ * Test without --server specified
+ */
+ KcAdmExec exe = KcAdmExec.execute("config credentials --server " + serverUrl + " --realm master");
+
+ assertExitCodeAndStreamSizes(exe, 0, 0, 0);
+ }
+
+ @Test
+ public void testCredentialsNoServerWithDefaultConfig() {
+ /*
+ * Test without --server specified
+ */
+ KcAdmExec exe = KcAdmExec.execute("config credentials --realm master --user admin --password admin");
+
+ assertExitCodeAndStreamSizes(exe, 1, 0, 2);
+ Assert.assertEquals("stderr first line", "Required option not specified: --server", exe.stderrLines().get(0));
+ Assert.assertEquals("try help", "Try '" + CMD + " help config credentials' for more information", exe.stderrLines().get(1));
+ }
+
+ @Test
+ public void testCredentialsNoRealmWithDefaultConfig() {
+ /*
+ * Test without --server specified
+ */
+ KcAdmExec exe = KcAdmExec.execute("config credentials --server " + serverUrl + " --user admin --password admin");
+
+ assertExitCodeAndStreamSizes(exe, 1, 0, 2);
+ Assert.assertEquals("stderr first line", "Required option not specified: --realm", exe.stderrLines().get(0));
+ Assert.assertEquals("try help", "Try '" + CMD + " help config credentials' for more information", exe.stderrLines().get(1));
+ }
+
+ @Test
+ public void testUserLoginWithDefaultConfig() {
+ /*
+ * Test most basic user login, using the default admin-cli as a client
+ */
+ KcAdmExec exe = KcAdmExec.execute("config credentials --server " + serverUrl + " --realm master --user admin --password admin");
+
+ assertExitCodeAndStreamSizes(exe, 0, 0, 1);
+ Assert.assertEquals("stderr first line", "Logging into " + serverUrl + " as user admin of realm master", exe.stderrLines().get(0));
+ }
+
+ @Test
+ public void testUserLoginWithDefaultConfigInteractive() throws IOException {
+ /*
+ * Test user login with interaction - provide user password after prompted for it
+ */
+
+ if (!runIntermittentlyFailingTests()) {
+ System.out.println("TEST SKIPPED - This test currently suffers from intermittent failures. Use -Dtest.intermittent=true to run it.");
+ return;
+ }
+
+ KcAdmExec exe = KcAdmExec.newBuilder()
+ .argsLine("config credentials --server " + serverUrl + " --realm master --user admin")
+ .executeAsync();
+
+ exe.waitForStdout("Enter password: ");
+ exe.sendToStdin("admin" + EOL);
+ exe.waitCompletion();
+
+ assertExitCodeAndStreamSizes(exe, 0, 1, 1);
+ Assert.assertEquals("stderr first line", "Logging into " + serverUrl + " as user admin of realm master", exe.stderrLines().get(0));
+
+
+ /*
+ * Run the test one more time with stdin redirect
+ */
+ File tmpFile = new File(KcAdmExec.WORK_DIR + "/" + UUID.randomUUID().toString() + ".tmp");
+ try {
+ FileOutputStream tmpos = new FileOutputStream(tmpFile);
+ tmpos.write("admin".getBytes());
+ tmpos.write(EOL.getBytes());
+ tmpos.close();
+
+ exe = KcAdmExec.execute("config credentials --server " + serverUrl + " --realm master --user admin < '" + tmpFile.getName() + "'");
+
+ assertExitCodeAndStreamSizes(exe, 0, 1, 1);
+ Assert.assertTrue("Enter password prompt", exe.stdoutLines().get(0).startsWith("Enter password: "));
+ Assert.assertEquals("stderr first line", "Logging into " + serverUrl + " as user admin of realm master", exe.stderrLines().get(0));
+
+ } finally {
+ tmpFile.delete();
+ }
+ }
+
+ @Test
+ public void testClientLoginWithDefaultConfigInteractive() throws IOException {
+ /*
+ * Test client login with interaction - login using service account, and provide a client secret after prompted for it
+ */
+
+ if (!runIntermittentlyFailingTests()) {
+ System.out.println("TEST SKIPPED - This test currently suffers from intermittent failures. Use -Dtest.intermittent=true to run it.");
+ return;
+ }
+
+ // use -Dtest.intermittent=true to run this test
+ KcAdmExec exe = KcAdmExec.newBuilder()
+ .argsLine("config credentials --server " + serverUrl + " --realm test --client admin-cli-secret")
+ .executeAsync();
+
+ exe.waitForStdout("Enter client secret: ");
+ exe.sendToStdin("password" + EOL);
+ exe.waitCompletion();
+
+ assertExitCodeAndStreamSizes(exe, 0, 1, 1);
+ Assert.assertEquals("stderr first line", "Logging into " + serverUrl + " as service-account-admin-cli-secret of realm test", exe.stderrLines().get(0));
+
+ /*
+ * Run the test one more time with stdin redirect
+ */
+ File tmpFile = new File(KcAdmExec.WORK_DIR + "/" + UUID.randomUUID().toString() + ".tmp");
+ try {
+ FileOutputStream tmpos = new FileOutputStream(tmpFile);
+ tmpos.write("password".getBytes());
+ tmpos.write(EOL.getBytes());
+ tmpos.close();
+
+ exe = KcAdmExec.newBuilder()
+ .argsLine("config credentials --server " + serverUrl + " --realm test --client admin-cli-secret < '" + tmpFile.getName() + "'")
+ .execute();
+
+ assertExitCodeAndStreamSizes(exe, 0, 1, 1);
+ Assert.assertTrue("Enter client secret prompt", exe.stdoutLines().get(0).startsWith("Enter client secret: "));
+ Assert.assertEquals("stderr first line", "Logging into " + serverUrl + " as service-account-admin-cli-secret of realm test", exe.stderrLines().get(0));
+ } finally {
+ tmpFile.delete();
+ }
+ }
+
+ @Test
+ public void testUserLoginWithCustomConfig() {
+ /*
+ * Test user login using a custom config file
+ */
+ FileConfigHandler handler = initCustomConfigFile();
+
+ File configFile = new File(handler.getConfigFile());
+ try {
+ KcAdmExec exe = KcAdmExec.execute("config credentials --server " + serverUrl + " --realm master" +
+ " --user admin --password admin --config '" + configFile.getName() + "'");
+
+ assertExitCodeAndStreamSizes(exe, 0, 0, 1);
+ Assert.assertEquals("stderr first line", "Logging into " + serverUrl + " as user admin of realm master", exe.stderrLines().get(0));
+
+ // make sure the config file exists, and has the right content
+ ConfigData config = handler.loadConfig();
+ Assert.assertEquals("serverUrl", serverUrl, config.getServerUrl());
+ Assert.assertEquals("realm", "master", config.getRealm());
+ RealmConfigData realmcfg = config.sessionRealmConfigData();
+ Assert.assertNotNull("realm config data no null", realmcfg);
+ Assert.assertEquals("realm cfg serverUrl", serverUrl, realmcfg.serverUrl());
+ Assert.assertEquals("realm cfg realm", "master", realmcfg.realm());
+ Assert.assertEquals("client id", "admin-cli", realmcfg.getClientId());
+ Assert.assertNotNull("token not null", realmcfg.getToken());
+ Assert.assertNotNull("refresh token not null", realmcfg.getRefreshToken());
+ Assert.assertNotNull("token expires not null", realmcfg.getExpiresAt());
+ Assert.assertNotNull("token expires in future", realmcfg.getExpiresAt() > System.currentTimeMillis());
+ Assert.assertNotNull("refresh token expires not null", realmcfg.getRefreshExpiresAt());
+ Assert.assertNotNull("refresh token expires in future", realmcfg.getRefreshExpiresAt() > System.currentTimeMillis());
+
+ } finally {
+ configFile.delete();
+ }
+ }
+
+ @Test
+ public void testCustomConfigLoginCreateDelete() throws IOException {
+ /*
+ * Test user login, create, delete session using a custom config file
+ */
+
+ // prepare for loading a config file
+ FileConfigHandler handler = initCustomConfigFile();
+
+ try (TempFileResource configFile = new TempFileResource(handler.getConfigFile())) {
+
+ KcAdmExec exe = KcAdmExec.execute("config credentials --server " + serverUrl +
+ " --realm master --user admin --password admin --config '" + configFile.getName() + "'");
+
+ assertExitCodeAndStreamSizes(exe, 0, 0, 1);
+
+ // remember the state of config file
+ ConfigData config1 = handler.loadConfig();
+
+
+
+
+ exe = KcAdmExec.execute("create --config '" + configFile.getName() + "' clients -s clientId=test-client -o");
+
+ assertExitCodeAndStdErrSize(exe, 0, 0);
+
+ // check changes to config file
+ ConfigData config2 = handler.loadConfig();
+ assertFieldsEqualWithExclusions(config1, config2);
+
+
+ ClientRepresentation client = JsonSerialization.readValue(exe.stdout(), ClientRepresentation.class);
+ Assert.assertEquals("clientId", "test-client", client.getClientId());
+
+
+
+ exe = KcAdmExec.execute("delete clients/" + client.getId() + " --config '" + configFile.getName() + "'");
+
+ assertExitCodeAndStreamSizes(exe, 0, 0, 0);
+
+ // check changes to config file
+ ConfigData config3 = handler.loadConfig();
+ assertFieldsEqualWithExclusions(config2, config3);
+ }
+ }
+
+ @Test
+ public void testCRUDWithOnTheFlyUserAuth() throws IOException {
+ /*
+ * Test create, get, update, and delete using on-the-fly authentication - without using any config file.
+ * Login is performed by each operation again, and again using username, and password.
+ */
+ testCRUDWithOnTheFlyAuth(serverUrl, "--user user1 --password userpass", "",
+ "Logging into " + serverUrl + " as user user1 of realm test");
+ }
+
+ @Test
+ public void testCRUDWithOnTheFlyUserAuthWithClientSecret() throws IOException {
+ /*
+ * Test create, get, update, and delete using on-the-fly authentication - without using any config file.
+ * Login is performed by each operation again, and again using username, password, and client secret.
+ */
+ // try client without direct grants enabled
+ KcAdmExec exe = KcAdmExec.execute("get clients --no-config --server " + serverUrl + " --realm test" +
+ " --user user1 --password userpass --client admin-cli-secret --secret password");
+
+ assertExitCodeAndStreamSizes(exe, 1, 0, 2);
+ Assert.assertEquals("login message", "Logging into " + serverUrl + " as user user1 of realm test", exe.stderrLines().get(0));
+ Assert.assertEquals("error message", "Client not allowed for direct access grants [invalid_grant]", exe.stderrLines().get(1));
+
+
+ // try wrong user password
+ exe = KcAdmExec.execute("get clients --no-config --server " + serverUrl + " --realm test" +
+ " --user user1 --password wrong --client admin-cli-secret-direct --secret password");
+
+ assertExitCodeAndStreamSizes(exe, 1, 0, 2);
+ Assert.assertEquals("login message", "Logging into " + serverUrl + " as user user1 of realm test", exe.stderrLines().get(0));
+ Assert.assertEquals("error message", "Invalid user credentials [invalid_grant]", exe.stderrLines().get(1));
+
+
+ // try wrong client secret
+ exe = KcAdmExec.execute("get clients --no-config --server " + serverUrl + " --realm test" +
+ " --user user1 --password userpass --client admin-cli-secret-direct --secret wrong");
+
+ assertExitCodeAndStreamSizes(exe, 1, 0, 2);
+ Assert.assertEquals("login message", "Logging into " + serverUrl + " as user user1 of realm test", exe.stderrLines().get(0));
+ Assert.assertEquals("error message", "Invalid client secret [unauthorized_client]", exe.stderrLines().get(1));
+
+
+ // try whole CRUD
+ testCRUDWithOnTheFlyAuth(serverUrl, "--user user1 --password userpass --client admin-cli-secret-direct --secret password", "",
+ "Logging into " + serverUrl + " as user user1 of realm test");
+ }
+
+ @Test
+ public void testCRUDWithOnTheFlyUserAuthWithSignedJwtClient() throws IOException {
+ /*
+ * Test create, get, update, and delete using on-the-fly authentication - without using any config file.
+ * Login is performed by each operation again, and again using username, password, and client JWT signature.
+ */
+ File keystore = new File(System.getProperty("user.dir") + "/src/test/resources/cli/kcadm/admin-cli-keystore.jks");
+ Assert.assertTrue("admin-cli-keystore.jks exists", keystore.isFile());
+
+ // try client without direct grants enabled
+ KcAdmExec exe = KcAdmExec.execute("get clients --no-config --server " + serverUrl + " --realm test" +
+ " --user user1 --password userpass --client admin-cli-jwt --keystore '" + keystore.getAbsolutePath() + "'" +
+ " --storepass storepass --keypass keypass --alias admin-cli");
+
+ assertExitCodeAndStreamSizes(exe, 1, 0, 2);
+ Assert.assertEquals("login message", "Logging into " + serverUrl + " as user user1 of realm test", exe.stderrLines().get(0));
+ Assert.assertEquals("error message", "Client not allowed for direct access grants [invalid_grant]", exe.stderrLines().get(1));
+
+
+ // try wrong user password
+ exe = KcAdmExec.execute("get clients --no-config --server " + serverUrl + " --realm test" +
+ " --user user1 --password wrong --client admin-cli-jwt-direct --keystore '" + keystore.getAbsolutePath() + "'" +
+ " --storepass storepass --keypass keypass --alias admin-cli");
+
+ assertExitCodeAndStreamSizes(exe, 1, 0, 2);
+ Assert.assertEquals("login message", "Logging into " + serverUrl + " as user user1 of realm test", exe.stderrLines().get(0));
+ Assert.assertEquals("error message", "Invalid user credentials [invalid_grant]", exe.stderrLines().get(1));
+
+
+ // try wrong storepass
+ exe = KcAdmExec.execute("get clients --no-config --server " + serverUrl + " --realm test" +
+ " --user user1 --password userpass --client admin-cli-jwt-direct --keystore '" + keystore.getAbsolutePath() + "'" +
+ " --storepass wrong --keypass keypass --alias admin-cli");
+
+ assertExitCodeAndStreamSizes(exe, 1, 0, 2);
+ Assert.assertEquals("login message", "Logging into " + serverUrl + " as user user1 of realm test", exe.stderrLines().get(0));
+ Assert.assertEquals("error message", "Failed to load private key: Keystore was tampered with, or password was incorrect", exe.stderrLines().get(1));
+
+
+ // try whole CRUD
+ testCRUDWithOnTheFlyAuth(serverUrl,
+ "--user user1 --password userpass --client admin-cli-jwt-direct --keystore '" + keystore.getAbsolutePath() + "'" +
+ " --storepass storepass --keypass keypass --alias admin-cli", "",
+ "Logging into " + serverUrl + " as user user1 of realm test");
+
+ }
+
+ @Test
+ public void testCRUDWithOnTheFlyServiceAccountWithClientSecret() throws IOException {
+ /*
+ * Test create, get, update, and delete using on-the-fly authentication - without using any config file.
+ * Login is performed by each operation again, and again using only client secret - service account is used.
+ */
+ testCRUDWithOnTheFlyAuth(serverUrl, "--client admin-cli-secret --secret password", "",
+ "Logging into " + serverUrl + " as service-account-admin-cli-secret of realm test");
+ }
+
+ @Test
+ public void testCRUDWithOnTheFlyServiceAccountWithSignedJwtClient() throws IOException {
+ /*
+ * Test create, get, update, and delete using on-the-fly authentication - without using any config file.
+ * Login is performed by each operation again, and again using only client JWT signature - service account is used.
+ */
+ File keystore = new File(System.getProperty("user.dir") + "/src/test/resources/cli/kcadm/admin-cli-keystore.jks");
+ Assert.assertTrue("admin-cli-keystore.jks exists", keystore.isFile());
+
+ testCRUDWithOnTheFlyAuth(serverUrl,
+ "--client admin-cli-jwt --keystore '" + keystore.getAbsolutePath() + "' --storepass storepass --keypass keypass --alias admin-cli", "",
+ "Logging into " + serverUrl + " as service-account-admin-cli-jwt of realm test");
+ }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/admin/KcAdmTruststoreTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/admin/KcAdmTruststoreTest.java
new file mode 100644
index 0000000..1346442
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/admin/KcAdmTruststoreTest.java
@@ -0,0 +1,115 @@
+package org.keycloak.testsuite.cli.admin;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.keycloak.client.admin.cli.config.ConfigData;
+import org.keycloak.client.admin.cli.config.FileConfigHandler;
+import org.keycloak.testsuite.cli.KcAdmExec;
+import org.keycloak.testsuite.util.TempFileResource;
+
+import java.io.File;
+import java.io.IOException;
+
+import static org.keycloak.client.admin.cli.util.ConfigUtil.DEFAULT_CONFIG_FILE_PATH;
+import static org.keycloak.client.admin.cli.util.OsUtil.EOL;
+import static org.keycloak.testsuite.cli.KcAdmExec.CMD;
+import static org.keycloak.testsuite.cli.KcAdmExec.execute;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public class KcAdmTruststoreTest extends AbstractAdmCliTest {
+
+ @Test
+ public void testTruststore() throws IOException {
+
+ // only run this test if ssl protected keycloak server is available
+ if (!isAuthServerSSL()) {
+ System.out.println("TEST SKIPPED - This test requires HTTPS. Run with '-Pauth-server-wildfly -Dauth.server.ssl.required=true'");
+ return;
+ }
+
+ File truststore = new File("src/test/resources/keystore/keycloak.truststore");
+
+ FileConfigHandler handler = initCustomConfigFile();
+
+ try (TempFileResource configFile = new TempFileResource(handler.getConfigFile())) {
+
+ if (runIntermittentlyFailingTests()) {
+ // configure truststore
+ KcAdmExec exe = execute("config truststore --config '" + configFile.getName() + "' '" + truststore.getAbsolutePath() + "'");
+
+ assertExitCodeAndStreamSizes(exe, 0, 0, 0);
+
+
+ // perform authentication against server - asks for password, then for truststore password
+ exe = KcAdmExec.newBuilder()
+ .argsLine("config credentials --server " + serverUrl + " --realm test --user user1" +
+ " --config '" + configFile.getName() + "'")
+ .executeAsync();
+
+ exe.waitForStdout("Enter password: ");
+ exe.sendToStdin("userpass" + EOL);
+ exe.waitForStdout("Enter truststore password: ");
+ exe.sendToStdin("secret" + EOL);
+ exe.waitCompletion();
+
+ assertExitCodeAndStreamSizes(exe, 0, 2, 1);
+
+
+ // configure truststore with password
+ exe = execute("config truststore --config '" + configFile.getName() + "' --trustpass secret '" + truststore.getAbsolutePath() + "'");
+
+ assertExitCodeAndStreamSizes(exe, 0, 0, 0);
+
+ // perform authentication against server - asks for password, then for truststore password
+ exe = KcAdmExec.newBuilder()
+ .argsLine("config credentials --server " + serverUrl + " --realm test --user user1" +
+ " --config '" + configFile.getName() + "'")
+ .executeAsync();
+
+ exe.waitForStdout("Enter password: ");
+ exe.sendToStdin("userpass" + EOL);
+ exe.waitCompletion();
+
+ assertExitCodeAndStreamSizes(exe, 0, 1, 1);
+
+ } else {
+ System.out.println("TEST SKIPPED PARTIALLY - This test currently suffers from intermittent failures. Use -Dtest.intermittent=true to run it in full.");
+ }
+ }
+
+ // configure truststore with password
+ KcAdmExec exe = execute("config truststore --trustpass secret '" + truststore.getAbsolutePath() + "'");
+ assertExitCodeAndStreamSizes(exe, 0, 0, 0);
+
+ // perform authentication against server - asks for password, then for truststore password
+ exe = execute("config credentials --server " + serverUrl + " --realm test --user user1 --password userpass");
+ assertExitCodeAndStreamSizes(exe, 0, 0, 1);
+
+ exe = execute("config truststore --delete");
+ assertExitCodeAndStreamSizes(exe, 0, 0, 0);
+
+ exe = execute("config truststore --delete '" + truststore.getAbsolutePath() + "'");
+ assertExitCodeAndStreamSizes(exe, 1, 0, 2);
+ Assert.assertEquals("incompatible", "Option --delete is mutually exclusive with specifying a TRUSTSTORE", exe.stderrLines().get(0));
+ Assert.assertEquals("try help", "Try '" + CMD + " help config truststore' for more information", exe.stderrLines().get(1));
+
+ exe = execute("config truststore --delete --trustpass secret");
+ assertExitCodeAndStreamSizes(exe, 1, 0, 2);
+ Assert.assertEquals("no truststore error", "Options --trustpass and --delete are mutually exclusive", exe.stderrLines().get(0));
+ Assert.assertEquals("try help", "Try '" + CMD + " help config truststore' for more information", exe.stderrLines().get(1));
+
+ FileConfigHandler cfghandler = new FileConfigHandler();
+ cfghandler.setConfigFile(DEFAULT_CONFIG_FILE_PATH);
+ ConfigData config = cfghandler.loadConfig();
+ Assert.assertNull("truststore null", config.getTruststore());
+ Assert.assertNull("trustpass null", config.getTrustpass());
+
+
+ // perform no-config CRUD test against ssl protected endpoint
+ testCRUDWithOnTheFlyAuth(serverUrl,
+ "--user user1 --password userpass", " --truststore '" + truststore.getAbsolutePath() + "' --trustpass secret",
+ "Logging into " + serverUrl + " as user user1 of realm test");
+ }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/admin/KcAdmUpdateTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/admin/KcAdmUpdateTest.java
new file mode 100644
index 0000000..6e75f59
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/admin/KcAdmUpdateTest.java
@@ -0,0 +1,130 @@
+package org.keycloak.testsuite.cli.admin;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.keycloak.client.admin.cli.config.FileConfigHandler;
+import org.keycloak.representations.idm.ClientRepresentation;
+import org.keycloak.testsuite.cli.KcAdmExec;
+import org.keycloak.testsuite.util.TempFileResource;
+import org.keycloak.util.JsonSerialization;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.util.Arrays;
+
+import static org.keycloak.testsuite.cli.KcAdmExec.CMD;
+import static org.keycloak.testsuite.cli.KcAdmExec.execute;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public class KcAdmUpdateTest extends AbstractAdmCliTest {
+
+ @Test
+ public void testUpdateThoroughly() throws IOException {
+
+ FileConfigHandler handler = initCustomConfigFile();
+
+ try (TempFileResource configFile = new TempFileResource(handler.getConfigFile())) {
+
+ final String realm = "test";
+
+ loginAsUser(configFile.getFile(), serverUrl, realm, "user1", "userpass");
+
+
+ // create an object so we can update it
+ KcAdmExec exe = execute("create clients --config '" + configFile.getName() + "' -o -s clientId=my_client");
+
+ assertExitCodeAndStdErrSize(exe, 0, 0);
+
+ ClientRepresentation client = JsonSerialization.readValue(exe.stdout(), ClientRepresentation.class);
+
+ Assert.assertEquals("enabled", true, client.isEnabled());
+ Assert.assertEquals("publicClient", false, client.isPublicClient());
+ Assert.assertEquals("bearerOnly", false, client.isBearerOnly());
+ Assert.assertTrue("redirectUris is empty", client.getRedirectUris().isEmpty());
+
+
+ // Merge update
+ exe = execute("update clients/" + client.getId() + " --config '" + configFile.getName() + "' -o " +
+ " -s enabled=false -s 'redirectUris=[\"http://localhost:8980/myapp/*\"]'");
+
+ assertExitCodeAndStdErrSize(exe, 0, 0);
+
+ client = JsonSerialization.readValue(exe.stdout(), ClientRepresentation.class);
+ Assert.assertEquals("enabled", false, client.isEnabled());
+ Assert.assertEquals("redirectUris", Arrays.asList("http://localhost:8980/myapp/*"), client.getRedirectUris());
+
+
+
+ // Another merge update - test deleting an attribute, deleting a list item and adding a list item
+ exe = execute("update clients/" + client.getId() + " --config '" + configFile.getName() + "' -o -d redirectUris[0] -s webOrigins+=http://localhost:8980/myapp -s webOrigins+=http://localhost:8981/myapp -d webOrigins[0]");
+
+ assertExitCodeAndStdErrSize(exe, 0, 0);
+
+ client = JsonSerialization.readValue(exe.stdout(), ClientRepresentation.class);
+
+ Assert.assertTrue("redirectUris is empty", client.getRedirectUris().isEmpty());
+ Assert.assertEquals("webOrigins", Arrays.asList("http://localhost:8981/myapp"), client.getWebOrigins());
+
+
+
+ // Another merge update - test nested attributes and setting an attribute using json format
+ // TODO KEYCLOAK-3705 Updating protocolMapper config via client registration endpoint has no effect
+ /*
+ exe = execute("update my_client --config '" + configFile.getName() + "' -o -s 'protocolMappers[0].config.\"id.token.claim\"=false' " +
+ "-s 'protocolMappers[4].config={\"single\": \"true\", \"attribute.nameformat\": \"Basic\", \"attribute.name\": \"Role\"}'");
+
+ assertExitCodeAndStdErrSize(exe, 0, 0);
+
+ client = JsonSerialization.readValue(exe.stdout(), ClientRepresentation.class);
+ Assert.assertEquals("protocolMapper[0].config.\"id.token.claim\"", "false", client.getProtocolMappers().get(0).getConfig().get("id.token.claim"));
+ Assert.assertEquals("protocolMappers[4].config.single", "true", client.getProtocolMappers().get(4).getConfig().get("single"));
+ Assert.assertEquals("protocolMappers[4].config.\"attribute.nameformat\"", "Basic", client.getProtocolMappers().get(4).getConfig().get("attribute.nameformat"));
+ Assert.assertEquals("protocolMappers[4].config.\"attribute.name\"", "Role", client.getProtocolMappers().get(4).getConfig().get("attribute.name"));
+ */
+
+ // update using oidc format
+
+
+ // check that using an invalid attribute key is not ignored
+ exe = execute("update clients/" + client.getId() + " --nonexisting --config '" + configFile.getName() + "'");
+
+ assertExitCodeAndStreamSizes(exe, 1, 0, 2);
+ Assert.assertEquals("error message", "Invalid option: --nonexisting", exe.stderrLines().get(0));
+ Assert.assertEquals("try help", "Try '" + CMD + " help update' for more information", exe.stderrLines().get(1));
+
+
+ // test overwrite from file
+ exe = KcAdmExec.newBuilder()
+ .argsLine("update clients/" + client.getId() + " --config '" + configFile.getName() +
+ "' -o -s clientId=my_client -s 'redirectUris=[\"http://localhost:8980/myapp/*\"]' -f -")
+ .stdin(new ByteArrayInputStream("{ \"enabled\": false }".getBytes()))
+ .execute();
+
+ assertExitCodeAndStdErrSize(exe, 0, 0);
+
+ client = JsonSerialization.readValue(exe.stdout(), ClientRepresentation.class);
+ // web origin is not sent to the server, thus it retains the current value
+ Assert.assertEquals("webOrigins", Arrays.asList("http://localhost:8981/myapp"), client.getWebOrigins());
+ Assert.assertFalse("enabled is false", client.isEnabled());
+ Assert.assertEquals("redirectUris", Arrays.asList("http://localhost:8980/myapp/*"), client.getRedirectUris());
+
+
+ // test using merge with file
+ exe = KcAdmExec.newBuilder()
+ .argsLine("update clients/" + client.getId() + " --config '" + configFile.getName() +
+ "' -o -s enabled=true -m -f -")
+ .stdin(new ByteArrayInputStream("{ \"webOrigins\": [\"http://localhost:8980/myapp\"] }".getBytes()))
+ .execute();
+
+ assertExitCodeAndStdErrSize(exe, 0, 0);
+
+
+ client = JsonSerialization.readValue(exe.stdout(), ClientRepresentation.class);
+ Assert.assertEquals("webOrigins", Arrays.asList("http://localhost:8980/myapp"), client.getWebOrigins());
+ Assert.assertTrue("enabled is true", client.isEnabled());
+ Assert.assertEquals("redirectUris", Arrays.asList("http://localhost:8980/myapp/*"), client.getRedirectUris());
+ }
+ }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/registration/KcRegConfigTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/registration/KcRegConfigTest.java
index b91296c..1bf7b38 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/registration/KcRegConfigTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/registration/KcRegConfigTest.java
@@ -15,7 +15,7 @@ import static org.keycloak.testsuite.cli.KcRegExec.execute;
/**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
-public class KcRegConfigTest extends AbstractCliTest {
+public class KcRegConfigTest extends AbstractRegCliTest {
@Test
public void testRegistrationToken() throws IOException {
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/registration/KcRegCreateTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/registration/KcRegCreateTest.java
index 5db5d86..d916a9b 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/registration/KcRegCreateTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/registration/KcRegCreateTest.java
@@ -19,7 +19,7 @@ import static org.keycloak.testsuite.cli.KcRegExec.execute;
/**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
-public class KcRegCreateTest extends AbstractCliTest {
+public class KcRegCreateTest extends AbstractRegCliTest {
@Test
public void testCreateWithRealmOverride() throws IOException {
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/registration/KcRegTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/registration/KcRegTest.java
index 5647ec8..872ed32 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/registration/KcRegTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/registration/KcRegTest.java
@@ -23,7 +23,7 @@ import static org.keycloak.testsuite.cli.KcRegExec.execute;
/**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
-public class KcRegTest extends AbstractCliTest {
+public class KcRegTest extends AbstractRegCliTest {
@Test
public void testNoArgs() {
@@ -68,7 +68,7 @@ public class KcRegTest extends AbstractCliTest {
exe = execute("config truststore");
assertExitCodeAndStdErrSize(exe, 1, 0);
Assert.assertTrue("help message returned", exe.stdoutLines().size() > 10);
- Assert.assertEquals("help message", "Usage: " + CMD + " config truststore [TRUSTSTORE | --delete] [--trustpass PASSWOD] [ARGUMENTS]", exe.stdoutLines().get(0));
+ Assert.assertEquals("help message", "Usage: " + CMD + " config truststore [TRUSTSTORE | --delete] [--trustpass PASSWORD] [ARGUMENTS]", exe.stdoutLines().get(0));
exe = execute("create");
assertExitCodeAndStdErrSize(exe, 1, 0);
@@ -172,7 +172,7 @@ public class KcRegTest extends AbstractCliTest {
exe = execute("config truststore --help");
assertExitCodeAndStdErrSize(exe, 0, 0);
Assert.assertEquals("stdout first line",
- "Usage: " + CMD + " config truststore [TRUSTSTORE | --delete] [--trustpass PASSWOD] [ARGUMENTS]",
+ "Usage: " + CMD + " config truststore [TRUSTSTORE | --delete] [--trustpass PASSWORD] [ARGUMENTS]",
exe.stdoutLines().get(0));
}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/registration/KcRegTruststoreTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/registration/KcRegTruststoreTest.java
index e3f7729..34c9584 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/registration/KcRegTruststoreTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/registration/KcRegTruststoreTest.java
@@ -18,7 +18,7 @@ import static org.keycloak.testsuite.cli.KcRegExec.execute;
/**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
-public class KcRegTruststoreTest extends AbstractCliTest {
+public class KcRegTruststoreTest extends AbstractRegCliTest {
@Test
public void testTruststore() throws IOException {
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/registration/KcRegUpdateTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/registration/KcRegUpdateTest.java
index 79e6707..419d754 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/registration/KcRegUpdateTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/registration/KcRegUpdateTest.java
@@ -18,7 +18,7 @@ import static org.keycloak.testsuite.cli.KcRegExec.execute;
/**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
-public class KcRegUpdateTest extends AbstractCliTest {
+public class KcRegUpdateTest extends AbstractRegCliTest {
@Test
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/registration/KcRegUpdateTokenTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/registration/KcRegUpdateTokenTest.java
index b2aebf7..a4b6c2c 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/registration/KcRegUpdateTokenTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/registration/KcRegUpdateTokenTest.java
@@ -18,7 +18,7 @@ import static org.keycloak.testsuite.cli.KcRegExec.execute;
/**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
-public class KcRegUpdateTokenTest extends AbstractCliTest {
+public class KcRegUpdateTokenTest extends AbstractRegCliTest {
@Test
public void testUpdateToken() throws IOException {
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/exportimport/ExportImportTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/exportimport/ExportImportTest.java
index 2abf044..5d5b343 100755
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/exportimport/ExportImportTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/exportimport/ExportImportTest.java
@@ -99,9 +99,9 @@ public class ExportImportTest extends AbstractExportImportTest {
testRealmExportImport();
- // There should be 3 files in target directory (1 realm, 3 user)
+ // There should be 3 files in target directory (1 realm, 4 user)
File[] files = new File(targetDirPath).listFiles();
- assertEquals(4, files.length);
+ assertEquals(5, files.length);
}
@Test
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/exportimport/ExportImportUtil.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/exportimport/ExportImportUtil.java
index 3e16c4b..3c296a1 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/exportimport/ExportImportUtil.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/exportimport/ExportImportUtil.java
@@ -379,7 +379,7 @@ public class ExportImportUtil {
Assert.assertNotNull(linked);
Assert.assertEquals("my-service-user", linked.getUsername());
- if (Profile.isPreviewEnabled()) {
+ if (Profile.isFeatureEnabled(Profile.Feature.AUTHORIZATION)) {
assertAuthorizationSettings(realmRsc);
}
}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/RegisterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/RegisterTest.java
index 707b765..1a2149d 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/RegisterTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/RegisterTest.java
@@ -62,12 +62,12 @@ public class RegisterTest extends TestRealmKeycloakTest {
}
@Test
- public void registerExistingUser() {
+ public void registerExistingUsernameForbidden() {
loginPage.open();
loginPage.clickRegister();
registerPage.assertCurrent();
- registerPage.register("firstName", "lastName", "registerExistingUser@email", "test-user@localhost", "password", "password");
+ registerPage.register("firstName", "lastName", "registerExistingUser@email", "roleRichUser", "password", "password");
registerPage.assertCurrent();
assertEquals("Username already exists.", registerPage.getError());
@@ -80,10 +80,57 @@ public class RegisterTest extends TestRealmKeycloakTest {
assertEquals("", registerPage.getPassword());
assertEquals("", registerPage.getPasswordConfirm());
- events.expectRegister("test-user@localhost", "registerExistingUser@email")
+ events.expectRegister("roleRichUser", "registerExistingUser@email")
.removeDetail(Details.EMAIL)
.user((String) null).error("username_in_use").assertEvent();
}
+
+ @Test
+ public void registerExistingEmailForbidden() {
+ loginPage.open();
+ loginPage.clickRegister();
+ registerPage.assertCurrent();
+
+ registerPage.register("firstName", "lastName", "test-user@localhost", "registerExistingUser", "password", "password");
+
+ registerPage.assertCurrent();
+ assertEquals("Email already exists.", registerPage.getError());
+
+ // assert form keeps form fields on error
+ assertEquals("firstName", registerPage.getFirstName());
+ assertEquals("lastName", registerPage.getLastName());
+ assertEquals("", registerPage.getEmail());
+ assertEquals("registerExistingUser", registerPage.getUsername());
+ assertEquals("", registerPage.getPassword());
+ assertEquals("", registerPage.getPasswordConfirm());
+
+ events.expectRegister("registerExistingUser", "registerExistingUser@email")
+ .removeDetail(Details.EMAIL)
+ .user((String) null).error("email_in_use").assertEvent();
+ }
+
+ @Test
+ public void registerExistingEmailAllowed() {
+ setDuplicateEmailsAllowed(true);
+
+ loginPage.open();
+ loginPage.clickRegister();
+ registerPage.assertCurrent();
+
+ registerPage.register("firstName", "lastName", "test-user@localhost", "registerExistingEmailUser", "password", "password");
+
+ assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
+
+ String userId = events.expectRegister("registerExistingEmailUser", "test-user@localhost").assertEvent().getUserId();
+ events.expectLogin().detail("username", "registerexistingemailuser").user(userId).assertEvent();
+
+ UserRepresentation user = getUser(userId);
+ Assert.assertNotNull(user);
+ assertEquals("registerexistingemailuser", user.getUsername());
+ assertEquals("test-user@localhost", user.getEmail());
+ assertEquals("firstName", user.getFirstName());
+ assertEquals("lastName", user.getLastName());
+ }
@Test
public void registerUserInvalidPasswordConfirm() {
@@ -397,5 +444,11 @@ public class RegisterTest extends TestRealmKeycloakTest {
realm.setRegistrationEmailAsUsername(value);
testRealm().update(realm);
}
+
+ private void setDuplicateEmailsAllowed(boolean allowed) {
+ RealmRepresentation testRealm = testRealm().toRepresentation();
+ testRealm.setDuplicateEmailsAllowed(allowed);
+ testRealm().update(testRealm);
+ }
}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/keys/GeneratedHmacKeyProviderTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/keys/GeneratedHmacKeyProviderTest.java
new file mode 100644
index 0000000..fb3a4f7
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/keys/GeneratedHmacKeyProviderTest.java
@@ -0,0 +1,182 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.testsuite.keys;
+
+import org.jboss.arquillian.graphene.page.Page;
+import org.junit.Rule;
+import org.junit.Test;
+import org.keycloak.common.util.Base64Url;
+import org.keycloak.common.util.MultivaluedHashMap;
+import org.keycloak.common.util.PemUtils;
+import org.keycloak.jose.jws.AlgorithmType;
+import org.keycloak.jose.jws.crypto.HMACProvider;
+import org.keycloak.keys.GeneratedHmacKeyProviderFactory;
+import org.keycloak.keys.GeneratedRsaKeyProviderFactory;
+import org.keycloak.keys.KeyProvider;
+import org.keycloak.representations.idm.ComponentRepresentation;
+import org.keycloak.representations.idm.ErrorRepresentation;
+import org.keycloak.representations.idm.KeysMetadataRepresentation;
+import org.keycloak.representations.idm.RealmRepresentation;
+import org.keycloak.testsuite.AbstractKeycloakTest;
+import org.keycloak.testsuite.AssertEvents;
+import org.keycloak.testsuite.admin.ApiUtil;
+import org.keycloak.testsuite.pages.AppPage;
+import org.keycloak.testsuite.pages.LoginPage;
+
+import javax.ws.rs.core.Response;
+import java.security.interfaces.RSAPublicKey;
+import java.util.List;
+
+import static org.junit.Assert.*;
+import static org.keycloak.testsuite.admin.AbstractAdminTest.loadJson;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class GeneratedHmacKeyProviderTest extends AbstractKeycloakTest {
+
+ @Rule
+ public AssertEvents events = new AssertEvents(this);
+
+ @Page
+ protected AppPage appPage;
+
+ @Page
+ protected LoginPage loginPage;
+
+ @Override
+ public void addTestRealms(List<RealmRepresentation> testRealms) {
+ RealmRepresentation realm = loadJson(getClass().getResourceAsStream("/testrealm.json"), RealmRepresentation.class);
+ testRealms.add(realm);
+ }
+
+ @Test
+ public void defaultKeysize() throws Exception {
+ long priority = System.currentTimeMillis();
+
+ ComponentRepresentation rep = createRep("valid", GeneratedHmacKeyProviderFactory.ID);
+ rep.setConfig(new MultivaluedHashMap<>());
+ rep.getConfig().putSingle("priority", Long.toString(priority));
+
+ Response response = adminClient.realm("test").components().add(rep);
+ String id = ApiUtil.getCreatedId(response);
+
+ ComponentRepresentation createdRep = adminClient.realm("test").components().component(id).toRepresentation();
+ assertEquals(1, createdRep.getConfig().size());
+ assertEquals(Long.toString(priority), createdRep.getConfig().getFirst("priority"));
+
+ KeysMetadataRepresentation keys = adminClient.realm("test").keys().getKeyMetadata();
+
+ KeysMetadataRepresentation.KeyMetadataRepresentation key = null;
+ for (KeysMetadataRepresentation.KeyMetadataRepresentation k : keys.getKeys()) {
+ if (k.getType().equals(AlgorithmType.HMAC.name())) {
+ key = k;
+ break;
+ }
+ }
+
+ assertEquals(id, key.getProviderId());
+ assertEquals(AlgorithmType.HMAC.name(), key.getType());
+ assertEquals(priority, key.getProviderPriority());
+
+ assertEquals(32, Base64Url.decode(testingClient.testing("test").getComponentConfig(id).getFirst("secret")).length);
+ }
+
+ @Test
+ public void largeKeysize() throws Exception {
+ long priority = System.currentTimeMillis();
+
+ ComponentRepresentation rep = createRep("valid", GeneratedHmacKeyProviderFactory.ID);
+ rep.setConfig(new MultivaluedHashMap<>());
+ rep.getConfig().putSingle("priority", Long.toString(priority));
+ rep.getConfig().putSingle("secretSize", "512");
+
+ Response response = adminClient.realm("test").components().add(rep);
+ String id = ApiUtil.getCreatedId(response);
+
+ ComponentRepresentation createdRep = adminClient.realm("test").components().component(id).toRepresentation();
+ assertEquals(2, createdRep.getConfig().size());
+ assertEquals("512", createdRep.getConfig().getFirst("secretSize"));
+
+ KeysMetadataRepresentation keys = adminClient.realm("test").keys().getKeyMetadata();
+
+ KeysMetadataRepresentation.KeyMetadataRepresentation key = null;
+ for (KeysMetadataRepresentation.KeyMetadataRepresentation k : keys.getKeys()) {
+ if (k.getType().equals(AlgorithmType.HMAC.name())) {
+ key = k;
+ break;
+ }
+ }
+
+ assertEquals(id, key.getProviderId());
+ assertEquals(AlgorithmType.HMAC.name(), key.getType());
+ assertEquals(priority, key.getProviderPriority());
+
+ assertEquals(512, Base64Url.decode(testingClient.testing("test").getComponentConfig(id).getFirst("secret")).length);
+ }
+
+ @Test
+ public void updateKeysize() throws Exception {
+ long priority = System.currentTimeMillis();
+
+ ComponentRepresentation rep = createRep("valid", GeneratedHmacKeyProviderFactory.ID);
+ rep.setConfig(new MultivaluedHashMap<>());
+ rep.getConfig().putSingle("priority", Long.toString(priority));
+
+ Response response = adminClient.realm("test").components().add(rep);
+ String id = ApiUtil.getCreatedId(response);
+
+ assertEquals(32, Base64Url.decode(testingClient.testing("test").getComponentConfig(id).getFirst("secret")).length);
+
+ ComponentRepresentation createdRep = adminClient.realm("test").components().component(id).toRepresentation();
+ createdRep.getConfig().putSingle("secretSize", "512");
+ adminClient.realm("test").components().component(id).update(createdRep);
+
+ assertEquals(512, Base64Url.decode(testingClient.testing("test").getComponentConfig(id).getFirst("secret")).length);
+ }
+
+ @Test
+ public void invalidKeysize() throws Exception {
+ ComponentRepresentation rep = createRep("invalid", GeneratedHmacKeyProviderFactory.ID);
+ rep.getConfig().putSingle("secretSize", "1234");
+
+ Response response = adminClient.realm("test").components().add(rep);
+ assertErrror(response, "'Secret size' should be 32, 64, 128, 256 or 512");
+ }
+
+ protected void assertErrror(Response response, String error) {
+ if (!response.hasEntity()) {
+ fail("No error message set");
+ }
+
+ ErrorRepresentation errorRepresentation = response.readEntity(ErrorRepresentation.class);
+ assertEquals(error, errorRepresentation.getErrorMessage());
+ }
+
+ protected ComponentRepresentation createRep(String name, String providerId) {
+ ComponentRepresentation rep = new ComponentRepresentation();
+ rep.setName(name);
+ rep.setParentId("test");
+ rep.setProviderId(providerId);
+ rep.setProviderType(KeyProvider.class.getName());
+ rep.setConfig(new MultivaluedHashMap<>());
+ return rep;
+ }
+
+}
+
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/keys/JavaKeystoreKeyProviderTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/keys/JavaKeystoreKeyProviderTest.java
index 642a872..b6fec3e 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/keys/JavaKeystoreKeyProviderTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/keys/JavaKeystoreKeyProviderTest.java
@@ -23,6 +23,7 @@ import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.keycloak.common.util.MultivaluedHashMap;
+import org.keycloak.jose.jws.AlgorithmType;
import org.keycloak.keys.JavaKeystoreKeyProviderFactory;
import org.keycloak.keys.KeyMetadata;
import org.keycloak.keys.KeyProvider;
@@ -103,7 +104,7 @@ public class JavaKeystoreKeyProviderTest extends AbstractKeycloakTest {
KeysMetadataRepresentation.KeyMetadataRepresentation key = keys.getKeys().get(0);
assertEquals(id, key.getProviderId());
- assertEquals(KeyMetadata.Type.RSA.name(), key.getType());
+ assertEquals(AlgorithmType.RSA.name(), key.getType());
assertEquals(priority, key.getProviderPriority());
assertEquals(PUBLIC_KEY, key.getPublicKey());
assertEquals(CERTIFICATE, key.getCertificate());
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/keys/KeyRotationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/keys/KeyRotationTest.java
index 964832c..6c54d2b 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/keys/KeyRotationTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/keys/KeyRotationTest.java
@@ -28,14 +28,13 @@ import org.keycloak.common.util.KeyUtils;
import org.keycloak.common.util.MultivaluedHashMap;
import org.keycloak.common.util.PemUtils;
import org.keycloak.keys.Attributes;
+import org.keycloak.keys.GeneratedHmacKeyProviderFactory;
import org.keycloak.keys.KeyProvider;
-import org.keycloak.keys.RsaKeyProviderFactory;
-import org.keycloak.representations.UserInfo;
+import org.keycloak.keys.ImportedRsaKeyProviderFactory;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.ComponentRepresentation;
import org.keycloak.representations.idm.KeysMetadataRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
-import org.keycloak.storage.UserStorageProvider;
import org.keycloak.testsuite.AbstractKeycloakTest;
import org.keycloak.testsuite.AssertEvents;
import org.keycloak.testsuite.pages.AppPage;
@@ -237,7 +236,7 @@ public class KeyRotationTest extends AbstractKeycloakTest {
ComponentRepresentation rep = new ComponentRepresentation();
rep.setName("mycomponent");
rep.setParentId("test");
- rep.setProviderId(RsaKeyProviderFactory.ID);
+ rep.setProviderId(ImportedRsaKeyProviderFactory.ID);
rep.setProviderType(KeyProvider.class.getName());
org.keycloak.common.util.MultivaluedHashMap config = new org.keycloak.common.util.MultivaluedHashMap();
@@ -247,6 +246,18 @@ public class KeyRotationTest extends AbstractKeycloakTest {
adminClient.realm("test").components().add(rep);
+ rep = new ComponentRepresentation();
+ rep.setName("mycomponent2");
+ rep.setParentId("test");
+ rep.setProviderId(GeneratedHmacKeyProviderFactory.ID);
+ rep.setProviderType(KeyProvider.class.getName());
+
+ config = new org.keycloak.common.util.MultivaluedHashMap();
+ config.addFirst("priority", priority);
+ rep.setConfig(config);
+
+ adminClient.realm("test").components().add(rep);
+
return publicKey;
}
@@ -259,13 +270,16 @@ public class KeyRotationTest extends AbstractKeycloakTest {
}
private void dropKeys(String priority) {
+ int r = 0;
for (ComponentRepresentation c : adminClient.realm("test").components().query("test", KeyProvider.class.getName())) {
if (c.getConfig().getFirst("priority").equals(priority)) {
adminClient.realm("test").components().component(c.getId()).remove();
- return;
+ r++;
}
}
- throw new RuntimeException("Failed to find keys1");
+ if (r != 2) {
+ throw new RuntimeException("Failed to find keys1");
+ }
}
private void assertUserInfo(String token, int expectedStatus) {
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/AccessTokenDuplicateEmailsNotCleanedUpTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/AccessTokenDuplicateEmailsNotCleanedUpTest.java
new file mode 100644
index 0000000..d49c4b8
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/AccessTokenDuplicateEmailsNotCleanedUpTest.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.testsuite.oauth;
+
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.keycloak.OAuth2Constants;
+import org.keycloak.representations.AccessToken;
+import org.keycloak.representations.idm.RealmRepresentation;
+import org.keycloak.testsuite.AbstractKeycloakTest;
+import org.keycloak.testsuite.AssertEvents;
+import static org.keycloak.testsuite.admin.AbstractAdminTest.loadJson;
+import static org.keycloak.testsuite.admin.ApiUtil.findUserByUsername;
+import org.keycloak.testsuite.util.OAuthClient;
+import org.openqa.selenium.By;
+
+/**
+ * @author <a href="mailto:slawomir@dabek.name">Slawomir Dabek</a>
+ */
+public class AccessTokenDuplicateEmailsNotCleanedUpTest extends AbstractKeycloakTest {
+
+ @Rule
+ public AssertEvents events = new AssertEvents(this);
+
+ @Override
+ public void beforeAbstractKeycloakTest() throws Exception {
+ super.beforeAbstractKeycloakTest();
+ }
+
+ @Before
+ public void clientConfiguration() {
+ oauth.clientId("test-app");
+ oauth.realm("test-duplicate-emails");
+
+ RealmRepresentation realmRep = new RealmRepresentation();
+ // change realm settings to allow login with email after having imported users with duplicate email addresses
+ realmRep.setLoginWithEmailAllowed(true);
+ adminClient.realm("test-duplicate-emails").update(realmRep);
+ }
+
+ @Override
+ public void addTestRealms(List<RealmRepresentation> testRealms) {
+ RealmRepresentation realm = loadJson(getClass().getResourceAsStream("/testrealm-duplicate-emails.json"), RealmRepresentation.class);
+ testRealms.add(realm);
+ }
+
+ @Test
+ public void loginWithNonDuplicateEmail() throws Exception {
+ oauth.doLogin("non-duplicate-email-user@localhost", "password");
+
+ String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
+ OAuthClient.AccessTokenResponse response = oauth.doAccessTokenRequest(code, "password");
+
+ assertEquals(200, response.getStatusCode());
+
+ AccessToken token = oauth.verifyToken(response.getAccessToken());
+
+ assertEquals(findUserByUsername(adminClient.realm("test-duplicate-emails"), "non-duplicate-email-user").getId(), token.getSubject());
+ }
+
+ @Test
+ public void loginWithDuplicateEmail() throws Exception {
+ oauth.doLogin("duplicate-email-user@localhost", "password");
+
+ assertEquals("Username already exists.", driver.findElement(By.xpath("//span[@class='kc-feedback-text']")).getText());
+ }
+
+ @Test
+ public void loginWithUserHavingDuplicateEmailByUsername() throws Exception {
+ oauth.doLogin("duplicate-email-user1", "password");
+
+ String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
+ OAuthClient.AccessTokenResponse response = oauth.doAccessTokenRequest(code, "password");
+
+ assertEquals(200, response.getStatusCode());
+
+ AccessToken token = oauth.verifyToken(response.getAccessToken());
+
+ assertEquals(findUserByUsername(adminClient.realm("test-duplicate-emails"), "duplicate-email-user1").getId(), token.getSubject());
+ assertEquals("duplicate-email-user@localhost", token.getEmail());
+ }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/AccessTokenDuplicateEmailsTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/AccessTokenDuplicateEmailsTest.java
new file mode 100644
index 0000000..0f56bcd
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/AccessTokenDuplicateEmailsTest.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.testsuite.oauth;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.keycloak.OAuth2Constants;
+import org.keycloak.representations.AccessToken;
+import org.keycloak.representations.idm.RealmRepresentation;
+import org.keycloak.testsuite.AbstractKeycloakTest;
+import org.keycloak.testsuite.AssertEvents;
+import org.keycloak.testsuite.util.OAuthClient;
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
+import static org.keycloak.testsuite.admin.AbstractAdminTest.loadJson;
+import static org.keycloak.testsuite.admin.ApiUtil.findUserByUsername;
+import org.keycloak.testsuite.arquillian.AuthServerTestEnricher;
+import org.openqa.selenium.By;
+
+/**
+ * @author <a href="mailto:slawomir@dabek.name">Slawomir Dabek</a>
+ */
+public class AccessTokenDuplicateEmailsTest extends AbstractKeycloakTest {
+
+ @Rule
+ public AssertEvents events = new AssertEvents(this);
+
+
+ @Override
+ public void beforeAbstractKeycloakTest() throws Exception {
+ super.beforeAbstractKeycloakTest();
+ }
+
+ @Before
+ public void clientConfiguration() {
+ oauth.clientId("test-app");
+ oauth.realm("test-duplicate-emails");
+ }
+
+ @Override
+ public void addTestRealms(List<RealmRepresentation> testRealms) {
+ RealmRepresentation realm = loadJson(getClass().getResourceAsStream("/testrealm-duplicate-emails.json"), RealmRepresentation.class);
+ testRealms.add(realm);
+ }
+
+ @Test
+ public void loginFormUsernameLabel() throws Exception {
+ oauth.openLoginForm();
+ oauth.redirectUri(AuthServerTestEnricher.getAuthServerContextRoot() + "/does/not/matter/");
+
+ assertEquals("Username", driver.findElement(By.xpath("//label[@for='username']")).getText());
+ }
+
+ @Test
+ public void loginWithNonDuplicateEmailUser() throws Exception {
+ oauth.doLogin("non-duplicate-email-user", "password");
+
+ String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
+ OAuthClient.AccessTokenResponse response = oauth.doAccessTokenRequest(code, "password");
+
+ assertEquals(200, response.getStatusCode());
+
+ AccessToken token = oauth.verifyToken(response.getAccessToken());
+
+ assertEquals(findUserByUsername(adminClient.realm("test-duplicate-emails"), "non-duplicate-email-user").getId(), token.getSubject());
+ assertEquals("non-duplicate-email-user@localhost", token.getEmail());
+ }
+
+ @Test
+ public void loginWithFirstDuplicateEmailUser() throws Exception {
+ oauth.doLogin("duplicate-email-user1", "password");
+
+ String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
+ OAuthClient.AccessTokenResponse response = oauth.doAccessTokenRequest(code, "password");
+
+ assertEquals(200, response.getStatusCode());
+
+ AccessToken token = oauth.verifyToken(response.getAccessToken());
+
+ assertEquals(findUserByUsername(adminClient.realm("test-duplicate-emails"), "duplicate-email-user1").getId(), token.getSubject());
+ assertEquals("duplicate-email-user@localhost", token.getEmail());
+ }
+
+ @Test
+ public void loginWithSecondDuplicateEmailUser() throws Exception {
+ oauth.doLogin("duplicate-email-user2", "password");
+
+ String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
+ OAuthClient.AccessTokenResponse response = oauth.doAccessTokenRequest(code, "password");
+
+ assertEquals(200, response.getStatusCode());
+
+ AccessToken token = oauth.verifyToken(response.getAccessToken());
+
+ assertEquals(findUserByUsername(adminClient.realm("test-duplicate-emails"), "duplicate-email-user2").getId(), token.getSubject());
+ assertEquals("duplicate-email-user@localhost", token.getEmail());
+ }
+
+ @Test
+ public void loginWithNonDuplicateEmail() throws Exception {
+ oauth.doLogin("non-duplicate-email-user@localhost", "password");
+
+ assertEquals("Invalid username or password.", driver.findElement(By.xpath("//span[@class='kc-feedback-text']")).getText());
+ }
+
+ @Test
+ public void loginWithDuplicateEmail() throws Exception {
+ oauth.doLogin("duplicate-email-user@localhost", "password");
+
+ assertEquals("Invalid username or password.", driver.findElement(By.xpath("//span[@class='kc-feedback-text']")).getText());
+ }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/AccessTokenNoEmailLoginTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/AccessTokenNoEmailLoginTest.java
new file mode 100644
index 0000000..316c09d
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/AccessTokenNoEmailLoginTest.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.testsuite.oauth;
+
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
+import org.junit.Before;
+import org.junit.Test;
+import org.keycloak.OAuth2Constants;
+import org.keycloak.representations.AccessToken;
+import org.keycloak.representations.idm.RealmRepresentation;
+import org.keycloak.testsuite.AbstractKeycloakTest;
+import static org.keycloak.testsuite.admin.AbstractAdminTest.loadJson;
+import static org.keycloak.testsuite.admin.ApiUtil.findUserByUsername;
+import org.keycloak.testsuite.util.OAuthClient;
+import org.openqa.selenium.By;
+
+/**
+ * @author <a href="mailto:slawomir@dabek.name">Slawomir Dabek</a>
+ */
+public class AccessTokenNoEmailLoginTest extends AbstractKeycloakTest {
+
+ @Override
+ public void beforeAbstractKeycloakTest() throws Exception {
+ super.beforeAbstractKeycloakTest();
+ }
+
+ @Before
+ public void clientConfiguration() {
+ oauth.clientId("test-app");
+ }
+
+ @Override
+ public void addTestRealms(List<RealmRepresentation> testRealms) {
+ RealmRepresentation realm = loadJson(getClass().getResourceAsStream("/testrealm.json"), RealmRepresentation.class);
+ realm.setLoginWithEmailAllowed(false);
+ testRealms.add(realm);
+ }
+
+ @Test
+ public void loginFormUsernameLabel() throws Exception {
+ oauth.openLoginForm();
+
+ assertEquals("Username", driver.findElement(By.xpath("//label[@for='username']")).getText());
+ }
+
+ @Test
+ public void loginWithUsername() throws Exception {
+ oauth.doLogin("non-duplicate-email-user", "password");
+
+ String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
+ OAuthClient.AccessTokenResponse response = oauth.doAccessTokenRequest(code, "password");
+
+ assertEquals(200, response.getStatusCode());
+
+ AccessToken token = oauth.verifyToken(response.getAccessToken());
+
+ assertEquals(findUserByUsername(adminClient.realm("test"), "non-duplicate-email-user").getId(), token.getSubject());
+ assertEquals("non-duplicate-email-user@localhost", token.getEmail());
+ }
+
+ @Test
+ public void loginWithEmail() throws Exception {
+ oauth.doLoginGrant("non-duplicate-email-user@localhost", "password");
+
+ assertEquals("Invalid username or password.", driver.findElement(By.xpath("//span[@class='kc-feedback-text']")).getText());
+ }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/AccessTokenTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/AccessTokenTest.java
index 7115f74..92e68cb 100755
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/AccessTokenTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/AccessTokenTest.java
@@ -93,6 +93,7 @@ import static org.keycloak.testsuite.admin.ApiUtil.findUserByUsername;
import static org.keycloak.testsuite.admin.ApiUtil.findUserByUsernameId;
import static org.keycloak.testsuite.util.OAuthClient.AUTH_SERVER_ROOT;
import static org.keycloak.testsuite.util.ProtocolMapperUtil.createRoleNameMapper;
+import org.openqa.selenium.By;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@@ -135,6 +136,13 @@ public class AccessTokenTest extends AbstractKeycloakTest {
testRealms.add(realm);
}
+
+ @Test
+ public void loginFormUsernameOrEmailLabel() throws Exception {
+ oauth.openLoginForm();
+
+ assertEquals("Username or email", driver.findElement(By.xpath("//label[@for='username']")).getText());
+ }
@Test
public void accessTokenRequest() throws Exception {
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OIDCProtocolMappersTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OIDCProtocolMappersTest.java
index b896588..ec2d9f5 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OIDCProtocolMappersTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OIDCProtocolMappersTest.java
@@ -115,17 +115,18 @@ public class OIDCProtocolMappersTest extends AbstractKeycloakTest {
mapper.getConfig().put(AddressMapper.getModelPropertyName(AddressClaimSet.REGION), "region_some");
mapper.getConfig().put(AddressMapper.getModelPropertyName(AddressClaimSet.COUNTRY), "country_some");
mapper.getConfig().remove(AddressMapper.getModelPropertyName(AddressClaimSet.POSTAL_CODE)); // Even if we remove protocolMapper config property, it should still default to postal_code
- app.getProtocolMappers().createMapper(mapper);
+ app.getProtocolMappers().createMapper(mapper).close();
ProtocolMapperRepresentation hard = createHardcodedClaim("hard", "hard", "coded", "String", false, null, true, true);
- app.getProtocolMappers().createMapper(hard);
- app.getProtocolMappers().createMapper(createHardcodedClaim("hard-nested", "nested.hard", "coded-nested", "String", false, null, true, true));
- app.getProtocolMappers().createMapper(createClaimMapper("custom phone", "phone", "home_phone", "String", true, "", true, true, false));
- app.getProtocolMappers().createMapper(createClaimMapper("nested phone", "phone", "home.phone", "String", true, "", true, true, false));
- app.getProtocolMappers().createMapper(createClaimMapper("departments", "departments", "department", "String", true, "", true, true, true));
- app.getProtocolMappers().createMapper(createHardcodedRole("hard-realm", "hardcoded"));
- app.getProtocolMappers().createMapper(createHardcodedRole("hard-app", "app.hardcoded"));
- app.getProtocolMappers().createMapper(createRoleNameMapper("rename-app-role", "test-app.customer-user", "realm-user"));
+ app.getProtocolMappers().createMapper(hard).close();
+ app.getProtocolMappers().createMapper(createHardcodedClaim("hard-nested", "nested.hard", "coded-nested", "String", false, null, true, true)).close();
+ app.getProtocolMappers().createMapper(createClaimMapper("custom phone", "phone", "home_phone", "String", true, "", true, true, true)).close();
+ app.getProtocolMappers().createMapper(createClaimMapper("nested phone", "phone", "home.phone", "String", true, "", true, true, true)).close();
+ app.getProtocolMappers().createMapper(createClaimMapper("departments", "departments", "department", "String", true, "", true, true, true)).close();
+ app.getProtocolMappers().createMapper(createClaimMapper("firstDepartment", "departments", "firstDepartment", "String", true, "", true, true, false)).close();
+ app.getProtocolMappers().createMapper(createHardcodedRole("hard-realm", "hardcoded")).close();
+ app.getProtocolMappers().createMapper(createHardcodedRole("hard-app", "app.hardcoded")).close();
+ app.getProtocolMappers().createMapper(createRoleNameMapper("rename-app-role", "test-app.customer-user", "realm-user")).close();
}
{
@@ -147,9 +148,13 @@ public class OIDCProtocolMappersTest extends AbstractKeycloakTest {
assertEquals("coded-nested", nested.get("hard"));
nested = (Map) idToken.getOtherClaims().get("home");
assertThat((List<String>) nested.get("phone"), hasItems("617-777-6666"));
+
List<String> departments = (List<String>) idToken.getOtherClaims().get("department");
- assertEquals(2, departments.size());
- assertTrue(departments.contains("finance") && departments.contains("development"));
+ assertThat(departments, containsInAnyOrder("finance", "development"));
+
+ Object firstDepartment = idToken.getOtherClaims().get("firstDepartment");
+ assertThat(firstDepartment, instanceOf(String.class));
+ assertThat(firstDepartment, is("finance")); // Has to be the first item
AccessToken accessToken = oauth.verifyToken(response.getAccessToken());
assertEquals(accessToken.getName(), "Tom Brady");
@@ -186,6 +191,7 @@ public class OIDCProtocolMappersTest extends AbstractKeycloakTest {
|| model.getName().equals("hard-nested")
|| model.getName().equals("custom phone")
|| model.getName().equals("departments")
+ || model.getName().equals("firstDepartment")
|| model.getName().equals("nested phone")
|| model.getName().equals("rename-app-role")
|| model.getName().equals("hard-realm")
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/jboss-deployment-structure.xml b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/jboss-deployment-structure.xml
index 9a09a89..9356226 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/jboss-deployment-structure.xml
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/jboss-deployment-structure.xml
@@ -18,18 +18,15 @@
<jboss-deployment-structure>
<deployment>
<dependencies>
+
<!-- the Demo code uses classes in these modules. These are optional to import if you are not using
Apache Http Client or the HttpClientBuilder that comes with the adapter core -->
<module name="org.apache.httpcomponents"/>
- <!--These are needed when keycloak adapter libs are bundled in war.-->
- <module name="org.codehaus.jackson.jackson-xc" />
- <module name="org.codehaus.jackson.jackson-mapper-asl" />
- <module name="org.bouncycastle" />
- <module name="org.jboss.xnio" />
+ <!--required by SAML test servlets-->
<module name="org.keycloak.keycloak-adapter-spi" />
<module name="org.keycloak.keycloak-saml-core" />
-
+
</dependencies>
</deployment>
</jboss-deployment-structure>
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/cli/kcadm/admin-cli-keystore.jks b/testsuite/integration-arquillian/tests/base/src/test/resources/cli/kcadm/admin-cli-keystore.jks
new file mode 100644
index 0000000..5c789f3
Binary files /dev/null and b/testsuite/integration-arquillian/tests/base/src/test/resources/cli/kcadm/admin-cli-keystore.jks differ
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/org/keycloak/testsuite/admin/client/KEYCLOAK-4040-sharefile-metadata.xml b/testsuite/integration-arquillian/tests/base/src/test/resources/org/keycloak/testsuite/admin/client/KEYCLOAK-4040-sharefile-metadata.xml
new file mode 100644
index 0000000..7fe7824
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/org/keycloak/testsuite/admin/client/KEYCLOAK-4040-sharefile-metadata.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<md:EntityDescriptor entityID="https://pradeepkumar74.sharefile.com/saml/info" ID="_a0263555950f54cb56ead771a6f3516e" xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata">
+ <md:SPSSODescriptor AuthnRequestsSigned="False" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
+ <md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress</md:NameIDFormat>
+ <md:AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://pradeepkumar74.sharefile.com/saml/acs" index="1" isDefault="true" />
+ <md:AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://pradeepkumar74.sf-api.com/sf/v3/Sessions/Acs" index="2" isDefault="false" />
+ </md:SPSSODescriptor>
+ <Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
+ <SignedInfo>
+ <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" />
+ <SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1" />
+ <Reference URI="#https://pradeepkumar74.sharefile.com/saml/info">
+ <Transforms>
+ <Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature" />
+ <Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" />
+ </Transforms>
+ <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1" />
+ <DigestValue>Ldu0001hripzSq7zbIMTEKnQCOU=</DigestValue>
+ </Reference>
+ </SignedInfo>
+ <SignatureValue>J6IRDZ9RmdtpuwTlEK9HjqtbyeSA2Vz9mXF8yYRLIM0qMxRIvPLiIk02UuzCzEJ1I1xT4pcFuUfrdgDG6r9yf2iS+lV7jd0+DdXTHQ4VbQAZRC3Xd8wJ2RnnbZ3gwbIBBYurnWpKI0OCm0MnGvqV75n5Q6iF5jKA8Y4cFp60HHHnCH4QzpVTV5LjSg91eJA1X+99Xga+sK8Z+ln9wBzsrevz6ZfMt24rOMtb64wfAitz+HiD542Ta2TrzKQTnx+EPcr8xBwC62Gl+lIeE3DwKxtNk8pM8mq42D2b5UVKzjfL+PsYZ8XXBwwnwxFs40uxiI/ivq6KuQ/INt4Z5wmjGw==</SignatureValue>
+ <KeyInfo>
+ <X509Data>
+ <X509Certificate>MIIFUjCCBDqgAwIBAgIDBbH0MA0GCSqGSIb3DQEBBQUAMGExCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1HZW9UcnVzdCBJbmMuMR0wGwYDVQQLExREb21haW4gVmFsaWRhdGVkIFNTTDEbMBkGA1UEAxMSR2VvVHJ1c3QgRFYgU1NMIENBMB4XDTEyMTIxODAyMjcxOVoXDTE2MTIyMDAwNTYwMFowgckxKTAnBgNVBAUTIHlMbC1HNjFUWWtiaHBaL1JMTnNIYU8vTmJyWEVQOXc1MRMwEQYDVQQLEwpHVDQ4MjA0OTU4MTEwLwYDVQQLEyhTZWUgd3d3Lmdlb3RydXN0LmNvbS9yZXNvdXJjZXMvY3BzIChjKTEyMTcwNQYDVQQLEy5Eb21haW4gQ29udHJvbCBWYWxpZGF0ZWQgLSBRdWlja1NTTChSKSBQcmVtaXVtMRswGQYDVQQDExJzYW1sLnNoYXJlZmlsZS5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDPUZyhTw3RmP7Y7v06aHgTNuv/Fm0PbGWbGlZEwqr8TGabocPbnb8iTBWAL2ECXMbx+VrpaHiSOVxqC2Y/vDXOs+1r0CzRKeMC6oQPsXZbieW6HxOAv3UVShxc9nfWI6+immo/o3BYI5WKcOaeZieVlDq7a7ctfSUJXHEBhpaSJNhghb+cUZtp1/EXs8/LyVQ31coo1q726WjCvFVB8OUU2u6BQLcbJF5aG3qh5CkNyivwM3NtNAyHhSXRmwyE+Yv5YNo5QAtUagCGYmS2saEJj8FxhXsNRtfW5B6vVhgmNreTcHCcWTpFGhjvferPjsjaIQAs3P2zx/pW/GSCXHy1AgMBAAGjggGoMIIBpDAfBgNVHSMEGDAWgBSM9NmTCke8AKBKzkt1bqC2sLJ+/DAOBgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMB0GA1UdEQQWMBSCEnNhbWwuc2hhcmVmaWxlLmNvbTBBBgNVHR8EOjA4MDagNKAyhjBodHRwOi8vZ3Rzc2xkdi1jcmwuZ2VvdHJ1c3QuY29tL2NybHMvZ3Rzc2xkdi5jcmwwHQYDVR0OBBYEFIDTam2PfOzLpQoclHMwfsUtSi3kMAwGA1UdEwEB/wQCMAAwdQYIKwYBBQUHAQEEaTBnMCwGCCsGAQUFBzABhiBodHRwOi8vZ3Rzc2xkdi1vY3NwLmdlb3RydXN0LmNvbTA3BggrBgEFBQcwAoYraHR0cDovL2d0c3NsZHYtYWlhLmdlb3RydXN0LmNvbS9ndHNzbGR2LmNydDBMBgNVHSAERTBDMEEGCmCGSAGG+EUBBzYwMzAxBggrBgEFBQcCARYlaHR0cDovL3d3dy5nZW90cnVzdC5jb20vcmVzb3VyY2VzL2NwczANBgkqhkiG9w0BAQUFAAOCAQEAU0I6sMe1ZgJ27pdu9qhQLMIgt0w7CuEbLfsSZZdo5TXEj15SGQwU2A0F6o5ivdAvMWTCISJsjHdqCkvB6ZOdMHIfSqA9ARLqX7wLKYfM8X/4RM3koHfqHOvxXBLqCLj2mn34oZrMU5CVI6rqbMoU4D61io7DVswR7Dss0rCh1b1o52ZEBjy5w9oJhRTEFwL7ekf6tR9UioyxQ37pGfD8qOpX1hj5gqcZ5+qUSVNjOjeh+9e9OO5Y/ns3jjHK5ieZPdYeLLOp+D6qzAnOERgvKvkPyRIHZA9tAjxj5KIEzQUopmbP7oH4Ovo6YXT+iIuMVvX3dDu00ExOSZjEeDzo/w==</X509Certificate>
+ </X509Data>
+ </KeyInfo>
+ </Signature>
+ <md:Organization>
+ <md:OrganizationName xml:lang="en">ShareFile.com</md:OrganizationName>
+ <md:OrganizationDisplayName xml:lang="en">ShareFile.com, a division of Citrix Systems, Inc</md:OrganizationDisplayName>
+ <md:OrganizationUrl xml:lang="en">https://www.sharefile.com</md:OrganizationUrl>
+ </md:Organization>
+</md:EntityDescriptor>
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/testrealm.json b/testsuite/integration-arquillian/tests/base/src/test/resources/testrealm.json
index b0e8767..969d9b5 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/resources/testrealm.json
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/testrealm.json
@@ -100,6 +100,22 @@
"clientRoles": {
"test-app-scope": [ "test-app-allowed-by-scope", "test-app-disallowed-by-scope" ]
}
+ },
+ {
+ "username" : "non-duplicate-email-user",
+ "enabled": true,
+ "email" : "non-duplicate-email-user@localhost",
+ "firstName": "Brian",
+ "lastName": "Cohen",
+ "credentials" : [
+ { "type" : "password",
+ "value" : "password" }
+ ],
+ "realmRoles": ["user", "offline_access"],
+ "clientRoles": {
+ "test-app": [ "customer-user" ],
+ "account": [ "view-profile", "manage-account" ]
+ }
}
],
"scopeMappings": [
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/testrealm-duplicate-emails.json b/testsuite/integration-arquillian/tests/base/src/test/resources/testrealm-duplicate-emails.json
new file mode 100644
index 0000000..560c1d2
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/testrealm-duplicate-emails.json
@@ -0,0 +1,142 @@
+{
+ "id": "test-duplicate-emails",
+ "realm": "test-duplicate-emails",
+ "enabled": true,
+ "sslRequired": "external",
+ "registrationAllowed": true,
+ "resetPasswordAllowed": true,
+ "editUsernameAllowed" : true,
+ "loginWithEmailAllowed": false,
+ "duplicateEmailsAllowed": true,
+ "privateKey": "MIICXAIBAAKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQABAoGAfmO8gVhyBxdqlxmIuglbz8bcjQbhXJLR2EoS8ngTXmN1bo2L90M0mUKSdc7qF10LgETBzqL8jYlQIbt+e6TH8fcEpKCjUlyq0Mf/vVbfZSNaVycY13nTzo27iPyWQHK5NLuJzn1xvxxrUeXI6A2WFpGEBLbHjwpx5WQG9A+2scECQQDvdn9NE75HPTVPxBqsEd2z10TKkl9CZxu10Qby3iQQmWLEJ9LNmy3acvKrE3gMiYNWb6xHPKiIqOR1as7L24aTAkEAtyvQOlCvr5kAjVqrEKXalj0Tzewjweuxc0pskvArTI2Oo070h65GpoIKLc9jf+UA69cRtquwP93aZKtW06U8dQJAF2Y44ks/mK5+eyDqik3koCI08qaC8HYq2wVl7G2QkJ6sbAaILtcvD92ToOvyGyeE0flvmDZxMYlvaZnaQ0lcSQJBAKZU6umJi3/xeEbkJqMfeLclD27XGEFoPeNrmdx0q10Azp4NfJAY+Z8KRyQCR2BEG+oNitBOZ+YXF9KCpH3cdmECQHEigJhYg+ykOvr1aiZUMFT72HU0jnmQe2FVekuG+LJUt2Tm7GtMjTFoGpf0JwrVuZN39fOYAlo+nTixgeW7X8Y=",
+ "publicKey": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB",
+ "requiredCredentials": [ "password" ],
+ "defaultRoles": [ "user" ],
+ "smtpServer": {
+ "from": "auto@keycloak.org",
+ "host": "localhost",
+ "port":"3025"
+ },
+ "users" : [
+ {
+ "username" : "non-duplicate-email-user",
+ "enabled": true,
+ "email" : "non-duplicate-email-user@localhost",
+ "firstName": "Brian",
+ "lastName": "Cohen",
+ "credentials" : [
+ { "type" : "password",
+ "value" : "password" }
+ ],
+ "realmRoles": ["user", "offline_access"],
+ "clientRoles": {
+ "test-app": [ "customer-user" ],
+ "account": [ "view-profile", "manage-account" ]
+ }
+ },
+ {
+ "username" : "duplicate-email-user1",
+ "enabled": true,
+ "email" : "duplicate-email-user@localhost",
+ "firstName": "Agent",
+ "lastName": "Smith",
+ "credentials" : [
+ { "type" : "password",
+ "value" : "password" }
+ ],
+ "realmRoles": ["user", "offline_access"],
+ "clientRoles": {
+ "test-app": [ "customer-user" ],
+ "account": [ "view-profile", "manage-account" ]
+ }
+ },
+ {
+ "username" : "duplicate-email-user2",
+ "enabled": true,
+ "email" : "duplicate-email-user@localhost",
+ "firstName": "Agent",
+ "lastName": "Smith",
+ "credentials" : [
+ { "type" : "password",
+ "value" : "password" }
+ ],
+ "realmRoles": ["user", "offline_access"],
+ "clientRoles": {
+ "test-app": [ "customer-user" ],
+ "account": [ "view-profile", "manage-account" ]
+ }
+ }
+ ],
+ "scopeMappings": [
+ {
+ "client": "test-app",
+ "roles": ["user"]
+ }
+ ],
+ "clients": [
+ {
+ "clientId": "test-app",
+ "enabled": true,
+ "baseUrl": "http://localhost:8180/auth/realms/master/app/auth",
+ "redirectUris": [
+ "http://localhost:8180/auth/realms/master/app/auth/*"
+ ],
+ "adminUrl": "http://localhost:8180/auth/realms/master/app/admin",
+ "secret": "password"
+ }
+ ],
+ "roles" : {
+ "realm" : [
+ {
+ "name": "user",
+ "description": "Have User privileges"
+ },
+ {
+ "name": "admin",
+ "description": "Have Administrator privileges"
+ },
+ {
+ "name": "customer-user-premium",
+ "description": "Have User Premium privileges"
+ },
+ {
+ "name": "sample-realm-role",
+ "description": "Sample realm role"
+ }
+ ],
+ "client" : {
+ "test-app" : [
+ {
+ "name": "customer-user",
+ "description": "Have Customer User privileges"
+ },
+ {
+ "name": "customer-admin",
+ "description": "Have Customer Admin privileges"
+ },
+ {
+ "name": "sample-client-role",
+ "description": "Sample client role"
+ },
+ {
+ "name": "customer-admin-composite-role",
+ "description": "Have Customer Admin privileges via composite role",
+ "composite" : true,
+ "composites" : {
+ "realm" : [ "customer-user-premium" ],
+ "client" : {
+ "test-app" : [ "customer-admin" ]
+ }
+ }
+ }
+ ]
+ }
+
+ },
+ "groups" : [],
+ "clientScopeMappings": {},
+ "internationalizationEnabled": true,
+ "supportedLocales": ["en", "de"],
+ "defaultLocale": "en",
+ "eventsListeners": ["jboss-logging", "event-queue"]
+}
diff --git a/testsuite/integration-arquillian/tests/other/adapters/jboss/remote/pom.xml b/testsuite/integration-arquillian/tests/other/adapters/jboss/remote/pom.xml
index 2726651..6b26801 100644
--- a/testsuite/integration-arquillian/tests/other/adapters/jboss/remote/pom.xml
+++ b/testsuite/integration-arquillian/tests/other/adapters/jboss/remote/pom.xml
@@ -127,6 +127,14 @@
<!--dummy profile for enforcer-->
</profile>
<profile>
+ <id>app-server-remote-as7-eap6</id>
+ <!--this profile is required if remote app server is AS7 or EAP6-->
+ <properties>
+ <app.server.management.protocol>remote</app.server.management.protocol>
+ <app.server.management.port>${app.server.management.port.jmx}</app.server.management.port>
+ </properties>
+ </profile>
+ <profile>
<id>no-offset</id>
<properties>
<app.server.port.offset>0</app.server.port.offset>
diff --git a/testsuite/integration-arquillian/tests/other/adapters/jboss/remote/src/test/resources/xslt/arquillian.xsl b/testsuite/integration-arquillian/tests/other/adapters/jboss/remote/src/test/resources/xslt/arquillian.xsl
index 9da9341..c18ecc1 100644
--- a/testsuite/integration-arquillian/tests/other/adapters/jboss/remote/src/test/resources/xslt/arquillian.xsl
+++ b/testsuite/integration-arquillian/tests/other/adapters/jboss/remote/src/test/resources/xslt/arquillian.xsl
@@ -32,6 +32,7 @@
<configuration>
<property name="adapterImplClass">org.jboss.as.arquillian.container.remote.RemoteDeployableContainer</property>
+ <property name="managementProtocol">${app.server.management.protocol}</property>
<property name="managementAddress">${app.server.host}</property>
<property name="managementPort">${app.server.management.port}</property>
<property name="username">admin</property>
diff --git a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/federation/CreateKerberosUserProvider.java b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/federation/CreateKerberosUserProvider.java
index 6347392..8070a82 100644
--- a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/federation/CreateKerberosUserProvider.java
+++ b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/federation/CreateKerberosUserProvider.java
@@ -13,7 +13,7 @@ public class CreateKerberosUserProvider extends AdminConsoleCreate {
private KerberosUserProviderForm form;
public CreateKerberosUserProvider() {
- setEntity("user-federation");
+ setEntity("user-storage");
}
@Override
diff --git a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/federation/CreateLdapUserProvider.java b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/federation/CreateLdapUserProvider.java
index 13ba716..64272ec 100644
--- a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/federation/CreateLdapUserProvider.java
+++ b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/federation/CreateLdapUserProvider.java
@@ -13,7 +13,7 @@ public class CreateLdapUserProvider extends AdminConsoleCreate {
private LdapUserProviderForm form;
public CreateLdapUserProvider() {
- setEntity("user-federation");
+ setEntity("user-storage");
}
@Override
diff --git a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/federation/KerberosUserProviderForm.java b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/federation/KerberosUserProviderForm.java
index 1b815f0..d0db464 100644
--- a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/federation/KerberosUserProviderForm.java
+++ b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/federation/KerberosUserProviderForm.java
@@ -54,7 +54,6 @@ public class KerberosUserProviderForm extends Form {
}
public void setKerberosRealmInput(String kerberosRealm) {
- waitUntilElement(By.id("kerberosRealm")).is().present();
setInputValue(kerberosRealmInput, kerberosRealm);
}
@@ -75,7 +74,6 @@ public class KerberosUserProviderForm extends Form {
}
public void selectEditMode(String mode) {
- waitUntilElement(By.id("editMode")).is().present();
editModeSelect.selectByVisibleText(mode);
}
diff --git a/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/AbstractConsoleTest.java b/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/AbstractConsoleTest.java
index 8f29a3f..ecf69e5 100644
--- a/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/AbstractConsoleTest.java
+++ b/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/AbstractConsoleTest.java
@@ -107,12 +107,12 @@ public abstract class AbstractConsoleTest extends AbstractAuthTest {
}
public void assertAlertSuccess() {
- assertTrue(alert.isSuccess());
+ assertTrue("Alert is not success", alert.isSuccess());
alert.close();
}
public void assertAlertDanger() {
- assertTrue(alert.isDanger());
+ assertTrue("Alert is not danger", alert.isDanger());
alert.close();
}
diff --git a/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/federation/KerberosUserFederationTest.java b/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/federation/KerberosUserFederationTest.java
index 79ea899..55e98ff 100644
--- a/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/federation/KerberosUserFederationTest.java
+++ b/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/federation/KerberosUserFederationTest.java
@@ -2,6 +2,7 @@ package org.keycloak.testsuite.console.federation;
import org.jboss.arquillian.graphene.page.Page;
import org.junit.Test;
+import org.keycloak.representations.idm.ComponentRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.UserFederationProviderRepresentation;
import org.keycloak.testsuite.console.AbstractConsoleTest;
@@ -34,8 +35,9 @@ public class KerberosUserFederationTest extends AbstractConsoleTest {
createKerberosUserProvider.form().setUpdateProfileFirstLogin(true);
createKerberosUserProvider.form().save();
assertAlertSuccess();
- RealmRepresentation realm = testRealmResource().toRepresentation();
- UserFederationProviderRepresentation ufpr = realm.getUserFederationProviders().get(0);
+
+ ComponentRepresentation ufpr = testRealmResource().components()
+ .query(null, "org.keycloak.storage.UserStorageProvider").get(0);
assertKerberosSetings(ufpr, "KEYCLOAK.ORG", "HTTP/localhost@KEYCLOAK.ORG", "http.keytab", "true", "true", "true");
}
@@ -64,12 +66,12 @@ public class KerberosUserFederationTest extends AbstractConsoleTest {
assertAlertSuccess();
}
- private void assertKerberosSetings(UserFederationProviderRepresentation ufpr, String kerberosRealm, String serverPrincipal, String keyTab, String debug, String useKerberosForPasswordAuthentication, String updateProfileFirstLogin) {
- assertEquals(kerberosRealm, ufpr.getConfig().get("kerberosRealm"));
- assertEquals(serverPrincipal, ufpr.getConfig().get("serverPrincipal"));
- assertEquals(keyTab, ufpr.getConfig().get("keyTab"));
- assertEquals(debug, ufpr.getConfig().get("debug"));
- assertEquals(useKerberosForPasswordAuthentication, ufpr.getConfig().get("allowKerberosAuthentication"));
- assertEquals(updateProfileFirstLogin, ufpr.getConfig().get("updateProfileFirstLogin"));
+ private void assertKerberosSetings(ComponentRepresentation ufpr, String kerberosRealm, String serverPrincipal, String keyTab, String debug, String useKerberosForPasswordAuthentication, String updateProfileFirstLogin) {
+ assertEquals(kerberosRealm, ufpr.getConfig().get("kerberosRealm").get(0));
+ assertEquals(serverPrincipal, ufpr.getConfig().get("serverPrincipal").get(0));
+ assertEquals(keyTab, ufpr.getConfig().get("keyTab").get(0));
+ assertEquals(debug, ufpr.getConfig().get("debug").get(0));
+ assertEquals(useKerberosForPasswordAuthentication, ufpr.getConfig().get("allowPasswordAuthentication").get(0));
+ assertEquals(updateProfileFirstLogin, ufpr.getConfig().get("updateProfileFirstLogin").get(0));
}
}
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/federation/LdapUserFederationTest.java b/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/federation/LdapUserFederationTest.java
index 2d405ea..2f4fb40 100644
--- a/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/federation/LdapUserFederationTest.java
+++ b/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/federation/LdapUserFederationTest.java
@@ -3,8 +3,7 @@ package org.keycloak.testsuite.console.federation;
import org.apache.commons.configuration.ConfigurationException;
import org.jboss.arquillian.graphene.page.Page;
import org.junit.Test;
-import org.keycloak.representations.idm.RealmRepresentation;
-import org.keycloak.representations.idm.UserFederationProviderRepresentation;
+import org.keycloak.representations.idm.ComponentRepresentation;
import org.keycloak.testsuite.console.AbstractConsoleTest;
import org.keycloak.testsuite.console.page.federation.CreateLdapUserProvider;
import org.keycloak.util.ldap.LDAPEmbeddedServer;
@@ -53,12 +52,13 @@ public class LdapUserFederationTest extends AbstractConsoleTest {
createLdapUserProvider.form().save();
assertAlertSuccess();
- RealmRepresentation realm = testRealmResource().toRepresentation();
- UserFederationProviderRepresentation ufpr = realm.getUserFederationProviders().get(0);
- assertLdapProviderSetting(ufpr, "ldap", 0, WRITABLE, "false", "ad", "1", "true", "true", "false");
+ ComponentRepresentation ufpr = testRealmResource().components()
+ .query(null, "org.keycloak.storage.UserStorageProvider").get(0);
+
+ assertLdapProviderSetting(ufpr, "ldap", "0", WRITABLE, "false", "ad", "1", "true", "true", "false");
assertLdapBasicMapping(ufpr, "cn", "cn", "objectGUID", "person, organizationalPerson, user",
"ou=People,dc=keycloak,dc=org");
- assertLdapSyncSetings(ufpr, "1000", 0, 0);
+ assertLdapSyncSetings(ufpr, "1000", "-1", "-1");
assertLdapKerberosSetings(ufpr, "KEYCLOAK.ORG", "HTTP/localhost@KEYCLOAK.ORG", "http.keytab", "true", "false");
}
@@ -75,12 +75,13 @@ public class LdapUserFederationTest extends AbstractConsoleTest {
createLdapUserProvider.form().save();
assertAlertSuccess();
- RealmRepresentation realm = testRealmResource().toRepresentation();
- UserFederationProviderRepresentation ufpr = realm.getUserFederationProviders().get(0);
- assertLdapProviderSetting(ufpr, "ldap", 0, READ_ONLY, "false", "rhds", "1", "true", "true", "true");
+ ComponentRepresentation ufpr = testRealmResource().components()
+ .query(null, "org.keycloak.storage.UserStorageProvider").get(0);
+
+ assertLdapProviderSetting(ufpr, "ldap", "0", READ_ONLY, "false", "rhds", "1", "true", "true", "true");
assertLdapBasicMapping(ufpr, "uid", "uid", "nsuniqueid", "inetOrgPerson, organizationalPerson",
"ou=People,dc=keycloak,dc=org");
- assertLdapSyncSetings(ufpr, "1000", 0, 0);
+ assertLdapSyncSetings(ufpr, "1000", "-1", "-1");
}
@Test
@@ -175,44 +176,44 @@ public class LdapUserFederationTest extends AbstractConsoleTest {
assertTrue("Vendors list doesn't match", vendorsExpected.containsAll(vendorsActual));
}
- private void assertLdapProviderSetting(UserFederationProviderRepresentation ufpr, String name, int priority,
+ private void assertLdapProviderSetting(ComponentRepresentation ufpr, String name, String priority,
String editMode, String syncRegistrations, String vendor, String searchScope, String connectionPooling,
String pagination, String enableAccountAfterPasswordUpdate) {
- assertEquals(name, ufpr.getDisplayName());
- assertEquals(priority, ufpr.getPriority());
- assertEquals(editMode, ufpr.getConfig().get("editMode"));
- assertEquals(syncRegistrations, ufpr.getConfig().get("syncRegistrations"));
- assertEquals(vendor, ufpr.getConfig().get("vendor"));
- assertEquals(searchScope, ufpr.getConfig().get("searchScope"));
- assertEquals(connectionPooling, ufpr.getConfig().get("connectionPooling"));
- assertEquals(pagination, ufpr.getConfig().get("pagination"));
+ assertEquals(name, ufpr.getName());
+ assertEquals(priority, ufpr.getConfig().get("priority").get(0));
+ assertEquals(editMode, ufpr.getConfig().get("editMode").get(0));
+ assertEquals(syncRegistrations, ufpr.getConfig().get("syncRegistrations").get(0));
+ assertEquals(vendor, ufpr.getConfig().get("vendor").get(0));
+ assertEquals(searchScope, ufpr.getConfig().get("searchScope").get(0));
+ assertEquals(connectionPooling, ufpr.getConfig().get("connectionPooling").get(0));
+ assertEquals(pagination, ufpr.getConfig().get("pagination").get(0));
// assertEquals(enableAccountAfterPasswordUpdate, ufpr.getConfig().get("userAccountControlsAfterPasswordUpdate"));
}
- private void assertLdapBasicMapping(UserFederationProviderRepresentation ufpr, String usernameLdapAttribute,
+ private void assertLdapBasicMapping(ComponentRepresentation ufpr, String usernameLdapAttribute,
String rdnLdapAttr, String uuidLdapAttr, String userObjectClasses, String userDN) {
- assertEquals(usernameLdapAttribute, ufpr.getConfig().get("usernameLDAPAttribute"));
- assertEquals(rdnLdapAttr, ufpr.getConfig().get("rdnLDAPAttribute"));
- assertEquals(uuidLdapAttr, ufpr.getConfig().get("uuidLDAPAttribute"));
- assertEquals(userObjectClasses, ufpr.getConfig().get("userObjectClasses"));
- assertEquals(userDN, ufpr.getConfig().get("usersDn"));
+ assertEquals(usernameLdapAttribute, ufpr.getConfig().get("usernameLDAPAttribute").get(0));
+ assertEquals(rdnLdapAttr, ufpr.getConfig().get("rdnLDAPAttribute").get(0));
+ assertEquals(uuidLdapAttr, ufpr.getConfig().get("uuidLDAPAttribute").get(0));
+ assertEquals(userObjectClasses, ufpr.getConfig().get("userObjectClasses").get(0));
+ assertEquals(userDN, ufpr.getConfig().get("usersDn").get(0));
}
- private void assertLdapKerberosSetings(UserFederationProviderRepresentation ufpr, String kerberosRealm,
+ private void assertLdapKerberosSetings(ComponentRepresentation ufpr, String kerberosRealm,
String serverPrincipal, String keyTab, String debug, String useKerberosForPasswordAuthentication) {
- assertEquals(kerberosRealm, ufpr.getConfig().get("kerberosRealm"));
- assertEquals(serverPrincipal, ufpr.getConfig().get("serverPrincipal"));
- assertEquals(keyTab, ufpr.getConfig().get("keyTab"));
- assertEquals(debug, ufpr.getConfig().get("debug"));
+ assertEquals(kerberosRealm, ufpr.getConfig().get("kerberosRealm").get(0));
+ assertEquals(serverPrincipal, ufpr.getConfig().get("serverPrincipal").get(0));
+ assertEquals(keyTab, ufpr.getConfig().get("keyTab").get(0));
+ assertEquals(debug, ufpr.getConfig().get("debug").get(0));
assertEquals(useKerberosForPasswordAuthentication,
- ufpr.getConfig().get("useKerberosForPasswordAuthentication"));
+ ufpr.getConfig().get("useKerberosForPasswordAuthentication").get(0));
}
- private void assertLdapSyncSetings(UserFederationProviderRepresentation ufpr, String batchSize,
- int periodicFullSync, int periodicChangedUsersSync) {
- assertEquals(batchSize, ufpr.getConfig().get("batchSizeForSync"));
- assertEquals(periodicFullSync, ufpr.getFullSyncPeriod());
- assertEquals(periodicChangedUsersSync, ufpr.getChangedSyncPeriod());
+ private void assertLdapSyncSetings(ComponentRepresentation ufpr, String batchSize,
+ String periodicFullSync, String periodicChangedUsersSync) {
+ assertEquals(batchSize, ufpr.getConfig().get("batchSizeForSync").get(0));
+ assertEquals(periodicFullSync, ufpr.getConfig().get("fullSyncPeriod").get(0));
+ assertEquals(periodicChangedUsersSync, ufpr.getConfig().get("changedSyncPeriod").get(0));
}
private LDAPEmbeddedServer startEmbeddedLdapServer() throws Exception {
diff --git a/testsuite/integration-arquillian/tests/other/nodejs_adapter/src/test/java/org/keycloak/testsuite/adapter/nodejs/NodejsAdapterTest.java b/testsuite/integration-arquillian/tests/other/nodejs_adapter/src/test/java/org/keycloak/testsuite/adapter/nodejs/NodejsAdapterTest.java
index 1a1fa14..0df2186 100644
--- a/testsuite/integration-arquillian/tests/other/nodejs_adapter/src/test/java/org/keycloak/testsuite/adapter/nodejs/NodejsAdapterTest.java
+++ b/testsuite/integration-arquillian/tests/other/nodejs_adapter/src/test/java/org/keycloak/testsuite/adapter/nodejs/NodejsAdapterTest.java
@@ -146,7 +146,6 @@ public class NodejsAdapterTest extends AbstractAuthTest {
// KEYCLOAK-3284
@Test
- @Ignore // to be enabled when KEYCLOAK-3284 is fixed
public void sessionTest() {
nodejsExamplePage.clickLogin();
testRealmLoginPage.form().login(USER, PASSWORD);
diff --git a/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties b/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties
index 173bcfb..0106590 100644
--- a/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties
+++ b/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties
@@ -32,6 +32,10 @@ resetPasswordAllowed=Forgot password
resetPasswordAllowed.tooltip=Show a link on login page for user to click on when they have forgotten their credentials.
rememberMe=Remember Me
rememberMe.tooltip=Show checkbox on login page to allow user to remain logged in between browser restarts until session expires.
+loginWithEmailAllowed=Login with email
+loginWithEmailAllowed.tooltip=Allow users to log in with their email address.
+duplicateEmailsAllowed=Duplicate emails
+duplicateEmailsAllowed.tooltip=Allow multiple users to have the same email address. Changing this setting will also clear the users cache. It is recommended to manually update email constraints of existing users in the database after switching off support for duplicate email addresses.
verifyEmail=Verify email
verifyEmail.tooltip=Require the user to verify their email address the first time they login.
sslRequired=Require SSL
@@ -39,6 +43,7 @@ sslRequired.option.all=all requests
sslRequired.option.external=external requests
sslRequired.option.none=none
sslRequired.tooltip=Is HTTPS required? 'None' means HTTPS is not required for any client IP address. 'External requests' means localhost and private IP addresses can access without HTTPS. 'All requests' means HTTPS is required for all IP addresses.
+publicKeys=Public keys
publicKey=Public key
privateKey=Private key
gen-new-keys=Generate new keys
@@ -272,6 +277,8 @@ logout-service-post-binding-url=Logout Service POST Binding URL
logout-service-post-binding-url.tooltip=SAML POST Binding URL for the client's single logout service. You can leave this blank if you are using a different binding
logout-service-redir-binding-url=Logout Service Redirect Binding URL
logout-service-redir-binding-url.tooltip=SAML Redirect Binding URL for the client's single logout service. You can leave this blank if you are using a different binding.
+saml-signature-keyName-transformer=SAML Signature Key Name
+saml-signature-keyName-transformer.tooltip=Signed SAML documents contain identification of signing key in KeyName element. For Keycloak / RH-SSO counterparty, use KEY_ID, for MS AD FS use CERT_SUBJECT, for others check and use NONE if no other option works.
# client import
import-client=Import Client
@@ -891,6 +898,7 @@ include-representation.tooltip=Include JSON representation for create and update
clear-admin-events.tooltip=Deletes all admin events in the database.
server-version=Server Version
server-profile=Server Profile
+server-disabled=Server Disabled Features
info=Info
providers=Providers
server-time=Server Time
@@ -1029,6 +1037,10 @@ authz-result=Result
authz-authorization-services-enabled=Authorization Enabled
authz-authorization-services-enabled.tooltip=Enable/Disable fine-grained authorization support for a client
authz-required=Required
+authz-show-details=Show Details
+authz-hide-details=Hide Details
+authz-associated-permissions=Associated Permissions
+authz-no-permission-associated=No permissions associated
# Authz Settings
authz-import-config.tooltip=Import a JSON file containing authorization settings for this resource server.
@@ -1049,6 +1061,7 @@ authz-export-settings.tooltip=Export and download all authorization settings for
authz-no-resources-available=No resources available.
authz-no-scopes-assigned=No scopes assigned.
authz-no-type-defined=No type defined.
+authz-no-uri-defined=No URI defined.
authz-no-permission-assigned=No permission assigned.
authz-no-policy-assigned=No policy assigned.
authz-create-permission=Create permission
diff --git a/themes/src/main/resources/theme/base/admin/resources/js/authz/authz-controller.js b/themes/src/main/resources/theme/base/admin/resources/js/authz/authz-controller.js
index ea039c1..2a86b93 100644
--- a/themes/src/main/resources/theme/base/admin/resources/js/authz/authz-controller.js
+++ b/themes/src/main/resources/theme/base/admin/resources/js/authz/authz-controller.js
@@ -86,10 +86,13 @@ module.controller('ResourceServerResourceCtrl', function($scope, $http, $route,
$scope.query = {
realm: realm.realm,
client : client.id,
+ deep: false,
max : 20,
first : 0
};
+ $scope.listSizes = [5, 10, 20];
+
ResourceServer.get({
realm : $route.current.params.realm,
client : client.id
@@ -124,10 +127,49 @@ module.controller('ResourceServerResourceCtrl', function($scope, $http, $route,
$scope.searchQuery = function() {
$scope.searchLoaded = false;
- $scope.resources = ResourceServerResource.query($scope.query, function() {
+ ResourceServerResource.query($scope.query, function(response) {
$scope.searchLoaded = true;
$scope.lastSearch = $scope.query.search;
+ $scope.resources = response;
+ if ($scope.detailsFilter) {
+ $scope.showDetails();
+ }
+ });
+ };
+
+ $scope.loadDetails = function (resource) {
+ if (resource.details) {
+ resource.details.loaded = !resource.details.loaded;
+ return;
+ }
+
+ resource.details = {loaded: false};
+
+ ResourceServerResource.scopes({
+ realm : $route.current.params.realm,
+ client : client.id,
+ rsrid : resource._id
+ }, function(response) {
+ resource.scopes = response;
+ ResourceServerResource.permissions({
+ realm : $route.current.params.realm,
+ client : client.id,
+ rsrid : resource._id
+ }, function(response) {
+ resource.policies = response;
+ resource.details.loaded = true;
+ });
});
+ }
+
+ $scope.showDetails = function(item) {
+ if (item) {
+ $scope.loadDetails(item);
+ } else {
+ for (i = 0; i < $scope.resources.length; i++) {
+ $scope.loadDetails($scope.resources[i]);
+ }
+ }
};
});
@@ -135,9 +177,36 @@ module.controller('ResourceServerResourceDetailCtrl', function($scope, $http, $r
$scope.realm = realm;
$scope.client = client;
- ResourceServerScope.query({realm : realm.realm, client : client.id}, function (data) {
- $scope.scopes = data;
- });
+ $scope.scopesUiSelect = {
+ minimumInputLength: 1,
+ delay: 500,
+ allowClear: true,
+ query: function (query) {
+ var data = {results: []};
+ if ('' == query.term.trim()) {
+ query.callback(data);
+ return;
+ }
+ $scope.query = {
+ realm: realm.realm,
+ client : client.id,
+ name: query.term.trim(),
+ deep: false,
+ max : 20,
+ first : 0
+ };
+ ResourceServerScope.query($scope.query, function(response) {
+ data.results = response;
+ query.callback(data);
+ });
+ },
+ formatResult: function(object, container, query) {
+ return object.name;
+ },
+ formatSelection: function(object, container, query) {
+ return object.name;
+ }
+ };
var $instance = this;
@@ -165,6 +234,9 @@ module.controller('ResourceServerResourceDetailCtrl', function($scope, $http, $r
}, true);
$scope.save = function() {
+ for (i = 0; i < $scope.resource.scopes.length; i++) {
+ delete $scope.resource.scopes[i].text;
+ }
$instance.checkNameAvailability(function () {
ResourceServerResource.save({realm : realm.realm, client : $scope.client.id}, $scope.resource, function(data) {
$location.url("/realms/" + realm.realm + "/clients/" + $scope.client.id + "/authz/resource-server/resource/" + data._id);
@@ -186,17 +258,9 @@ module.controller('ResourceServerResourceDetailCtrl', function($scope, $http, $r
data.scopes = [];
}
- if (!data.policies) {
- data.policies = [];
- }
-
$scope.resource = angular.copy(data);
$scope.changed = false;
- for (i = 0; i < $scope.resource.scopes.length; i++) {
- $scope.resource.scopes[i] = $scope.resource.scopes[i].name;
- }
-
$scope.originalResource = angular.copy($scope.resource);
$scope.$watch('resource', function() {
@@ -206,6 +270,9 @@ module.controller('ResourceServerResourceDetailCtrl', function($scope, $http, $r
}, true);
$scope.save = function() {
+ for (i = 0; i < $scope.resource.scopes.length; i++) {
+ delete $scope.resource.scopes[i].text;
+ }
$instance.checkNameAvailability(function () {
ResourceServerResource.update({realm : realm.realm, client : $scope.client.id, rsrid : $scope.resource._id}, $scope.resource, function() {
$route.reload();
@@ -215,22 +282,28 @@ module.controller('ResourceServerResourceDetailCtrl', function($scope, $http, $r
}
$scope.remove = function() {
- var msg = "";
-
- if ($scope.resource.policies.length > 0) {
- msg = "<p>This resource is referenced in some policies:</p>";
- msg += "<ul>";
- for (i = 0; i < $scope.resource.policies.length; i++) {
- msg+= "<li><strong>" + $scope.resource.policies[i].name + "</strong></li>";
+ ResourceServerResource.permissions({
+ realm : $route.current.params.realm,
+ client : client.id,
+ rsrid : $scope.resource._id
+ }, function (permissions) {
+ var msg = "";
+
+ if (permissions.length > 0 && !$scope.deleteConsent) {
+ msg = "<p>This resource is referenced in some policies:</p>";
+ msg += "<ul>";
+ for (i = 0; i < permissions.length; i++) {
+ msg+= "<li><strong>" + permissions[i].name + "</strong></li>";
+ }
+ msg += "</ul>";
+ msg += "<p>If you remove this resource, the policies above will be affected and will not be associated with this resource anymore.</p>";
}
- msg += "</ul>";
- msg += "<p>If you remove this resource, the policies above will be affected and will not be associated with this resource anymore.</p>";
- }
- AuthzDialog.confirmDeleteWithMsg($scope.resource.name, "Resource", msg, function() {
- ResourceServerResource.delete({realm : realm.realm, client : $scope.client.id, rsrid : $scope.resource._id}, null, function() {
- $location.url("/realms/" + realm.realm + "/clients/" + $scope.client.id + "/authz/resource-server/resource");
- Notifications.success("The resource has been deleted.");
+ AuthzDialog.confirmDeleteWithMsg($scope.resource.name, "Resource", msg, function() {
+ ResourceServerResource.delete({realm : realm.realm, client : $scope.client.id, rsrid : $scope.resource._id}, null, function() {
+ $location.url("/realms/" + realm.realm + "/clients/" + $scope.client.id + "/authz/resource-server/resource");
+ Notifications.success("The resource has been deleted.");
+ });
});
});
}
@@ -269,10 +342,13 @@ module.controller('ResourceServerScopeCtrl', function($scope, $http, $route, $lo
$scope.query = {
realm: realm.realm,
client : client.id,
+ deep: false,
max : 20,
first : 0
};
+ $scope.listSizes = [5, 10, 20];
+
ResourceServer.get({
realm : $route.current.params.realm,
client : client.id
@@ -304,14 +380,53 @@ module.controller('ResourceServerScopeCtrl', function($scope, $http, $route, $lo
$scope.searchQuery();
}
- $scope.searchQuery = function() {
+ $scope.searchQuery = function(detailsFilter) {
$scope.searchLoaded = false;
- $scope.scopes = ResourceServerScope.query($scope.query, function() {
+ ResourceServerScope.query($scope.query, function(response) {
+ $scope.scopes = response;
$scope.searchLoaded = true;
$scope.lastSearch = $scope.query.search;
+ if ($scope.detailsFilter) {
+ $scope.showDetails();
+ }
});
};
+
+ $scope.loadDetails = function (scope) {
+ if (scope.details) {
+ scope.details.loaded = !scope.details.loaded;
+ return;
+ }
+
+ scope.details = {loaded: false};
+
+ ResourceServerScope.resources({
+ realm : $route.current.params.realm,
+ client : client.id,
+ id : scope.id
+ }, function(response) {
+ scope.resources = response;
+ ResourceServerScope.permissions({
+ realm : $route.current.params.realm,
+ client : client.id,
+ id : scope.id
+ }, function(response) {
+ scope.policies = response;
+ scope.details.loaded = true;
+ });
+ });
+ }
+
+ $scope.showDetails = function(item) {
+ if (item) {
+ $scope.loadDetails(item);
+ } else {
+ for (i = 0; i < $scope.scopes.length; i++) {
+ $scope.loadDetails($scope.scopes[i]);
+ }
+ }
+ };
});
module.controller('ResourceServerScopeDetailCtrl', function($scope, $http, $route, $location, realm, ResourceServer, client, ResourceServerScope, AuthzDialog, Notifications) {
@@ -377,22 +492,28 @@ module.controller('ResourceServerScopeDetailCtrl', function($scope, $http, $rout
}
$scope.remove = function() {
- var msg = "";
-
- if ($scope.scope.policies.length > 0) {
- msg = "<p>This resource is referenced in some policies:</p>";
- msg += "<ul>";
- for (i = 0; i < $scope.scope.policies.length; i++) {
- msg+= "<li><strong>" + $scope.scope.policies[i].name + "</strong></li>";
+ ResourceServerScope.permissions({
+ realm : $route.current.params.realm,
+ client : client.id,
+ id : $scope.scope.id
+ }, function (permissions) {
+ var msg = "";
+
+ if (permissions.length > 0 && !$scope.deleteConsent) {
+ msg = "<p>This scope is referenced in some policies:</p>";
+ msg += "<ul>";
+ for (i = 0; i < permissions.length; i++) {
+ msg+= "<li><strong>" + permissions[i].name + "</strong></li>";
+ }
+ msg += "</ul>";
+ msg += "<p>If you remove this scope, the policies above will be affected and will not be associated with this scope anymore.</p>";
}
- msg += "</ul>";
- msg += "<p>If you remove this resource, the policies above will be affected and will not be associated with this resource anymore.</p>";
- }
- AuthzDialog.confirmDeleteWithMsg($scope.scope.name, "Scope", msg, function() {
- ResourceServerScope.delete({realm : realm.realm, client : $scope.client.id, id : $scope.scope.id}, null, function() {
- $location.url("/realms/" + realm.realm + "/clients/" + client.id + "/authz/resource-server/scope");
- Notifications.success("The scope has been deleted.");
+ AuthzDialog.confirmDeleteWithMsg($scope.scope.name, "Scope", msg, function() {
+ ResourceServerScope.delete({realm : realm.realm, client : $scope.client.id, id : $scope.scope.id}, null, function() {
+ $location.url("/realms/" + realm.realm + "/clients/" + client.id + "/authz/resource-server/scope");
+ Notifications.success("The scope has been deleted.");
+ });
});
});
}
@@ -432,10 +553,12 @@ module.controller('ResourceServerPolicyCtrl', function($scope, $http, $route, $l
realm: realm.realm,
client : client.id,
permission: false,
- max : 20,
+ max: 20,
first : 0
};
+ $scope.listSizes = [5, 10, 20];
+
PolicyProvider.query({
realm : $route.current.params.realm,
client : client.id
@@ -481,17 +604,41 @@ module.controller('ResourceServerPolicyCtrl', function($scope, $http, $route, $l
$scope.searchLoaded = false;
ResourceServerPolicy.query($scope.query, function(data) {
- $scope.policies = [];
-
- for (i = 0; i < data.length; i++) {
- if (data[i].type != 'resource' && data[i].type != 'scope') {
- $scope.policies.push(data[i]);
- }
- }
-
+ $scope.policies = data;
$scope.searchLoaded = true;
$scope.lastSearch = $scope.query.search;
+ if ($scope.detailsFilter) {
+ $scope.showDetails();
+ }
+ });
+ };
+
+ $scope.loadDetails = function (policy) {
+ if (policy.details) {
+ policy.details.loaded = !policy.details.loaded;
+ return;
+ }
+
+ policy.details = {loaded: false};
+
+ ResourceServerPolicy.dependentPolicies({
+ realm : $route.current.params.realm,
+ client : client.id,
+ id : policy.id
+ }, function(response) {
+ policy.dependentPolicies = response;
+ policy.details.loaded = true;
});
+ }
+
+ $scope.showDetails = function(item) {
+ if (item) {
+ $scope.loadDetails(item);
+ } else {
+ for (i = 0; i < $scope.policies.length; i++) {
+ $scope.loadDetails($scope.policies[i]);
+ }
+ }
};
});
@@ -508,6 +655,8 @@ module.controller('ResourceServerPermissionCtrl', function($scope, $http, $route
first : 0
};
+ $scope.listSizes = [5, 10, 20];
+
PolicyProvider.query({
realm : $route.current.params.realm,
client : client.id
@@ -553,18 +702,42 @@ module.controller('ResourceServerPermissionCtrl', function($scope, $http, $route
$scope.searchLoaded = false;
ResourceServerPolicy.query($scope.query, function(data) {
- $scope.policies = [];
-
- for (i = 0; i < data.length; i++) {
- if (data[i].type == 'resource' || data[i].type == 'scope') {
- $scope.policies.push(data[i]);
- }
- }
-
+ $scope.policies = data;
$scope.searchLoaded = true;
$scope.lastSearch = $scope.query.search;
+ if ($scope.detailsFilter) {
+ $scope.showDetails();
+ }
});
};
+
+ $scope.loadDetails = function (policy) {
+ if (policy.details) {
+ policy.details.loaded = !policy.details.loaded;
+ return;
+ }
+
+ policy.details = {loaded: false};
+
+ ResourceServerPolicy.associatedPolicies({
+ realm : $route.current.params.realm,
+ client : client.id,
+ id : policy.id
+ }, function(response) {
+ policy.associatedPolicies = response;
+ policy.details.loaded = true;
+ });
+ }
+
+ $scope.showDetails = function(item) {
+ if (item) {
+ $scope.loadDetails(item);
+ } else {
+ for (i = 0; i < $scope.policies.length; i++) {
+ $scope.loadDetails($scope.policies[i]);
+ }
+ }
+ };
});
module.controller('ResourceServerPolicyDroolsDetailCtrl', function($scope, $http, $route, realm, client, PolicyController) {
@@ -623,19 +796,64 @@ module.controller('ResourceServerPolicyResourceDetailCtrl', function($scope, $ro
},
onInit : function() {
- ResourceServerResource.query({realm : realm.realm, client : client.id}, function (data) {
- $scope.resources = data;
- });
-
- ResourceServerPolicy.query({realm : realm.realm, client : client.id}, function (data) {
- $scope.policies = [];
+ $scope.resourcesUiSelect = {
+ minimumInputLength: 1,
+ delay: 500,
+ allowClear: true,
+ id: function(resource){ return resource._id; },
+ query: function (query) {
+ var data = {results: []};
+ if ('' == query.term.trim()) {
+ query.callback(data);
+ return;
+ }
+ $scope.query = {
+ realm: realm.realm,
+ client : client.id,
+ name: query.term.trim(),
+ deep: false,
+ max : 20,
+ first : 0
+ };
+ ResourceServerResource.query($scope.query, function(response) {
+ data.results = response;
+ query.callback(data);
+ });
+ },
+ formatResult: function(object, container, query) {
+ object.text = object.name;
+ return object.name;
+ }
+ };
- for (i = 0; i < data.length; i++) {
- if (data[i].type != 'resource' && data[i].type != 'scope') {
- $scope.policies.push(data[i]);
+ $scope.policiesUiSelect = {
+ minimumInputLength: 1,
+ delay: 500,
+ allowClear: true,
+ query: function (query) {
+ var data = {results: []};
+ if ('' == query.term.trim()) {
+ query.callback(data);
+ return;
}
+ $scope.query = {
+ realm: realm.realm,
+ client : client.id,
+ permission: false,
+ name: query.term.trim(),
+ max : 20,
+ first : 0
+ };
+ ResourceServerPolicy.query($scope.query, function(response) {
+ data.results = response;
+ query.callback(data);
+ });
+ },
+ formatResult: function(object, container, query) {
+ object.text = object.name;
+ return object.name;
}
- });
+ };
$scope.applyToResourceType = function() {
if ($scope.policy.config.default) {
@@ -648,30 +866,69 @@ module.controller('ResourceServerPolicyResourceDetailCtrl', function($scope, $ro
onInitUpdate : function(policy) {
policy.config.default = eval(policy.config.default);
- policy.config.resources = eval(policy.config.resources);
- policy.config.applyPolicies = eval(policy.config.applyPolicies);
+ policy.config.resources = {};
+ ResourceServerPolicy.resources({
+ realm : $route.current.params.realm,
+ client : client.id,
+ id : policy.id
+ }, function(resources) {
+ resources[0].text = resources[0].name;
+ $scope.policy.config.resources = resources[0];
+ });
+
+ policy.config.applyPolicies = [];
+ ResourceServerPolicy.associatedPolicies({
+ realm : $route.current.params.realm,
+ client : client.id,
+ id : policy.id
+ }, function(policies) {
+ for (i = 0; i < policies.length; i++) {
+ policies[i].text = policies[i].name;
+ $scope.policy.config.applyPolicies.push(policies[i]);
+ }
+ });
},
onUpdate : function() {
- $scope.policy.config.resources = JSON.stringify($scope.policy.config.resources);
- $scope.policy.config.applyPolicies = JSON.stringify($scope.policy.config.applyPolicies);
+ $scope.policy.config.resources = JSON.stringify([$scope.policy.config.resources._id]);
+ var policies = [];
+
+ for (i = 0; i < $scope.policy.config.applyPolicies.length; i++) {
+ policies.push($scope.policy.config.applyPolicies[i].id);
+ }
+
+ $scope.policy.config.applyPolicies = JSON.stringify(policies);
},
onInitCreate : function(newPolicy) {
newPolicy.decisionStrategy = 'UNANIMOUS';
newPolicy.config = {};
- newPolicy.config.resources = '';
+ newPolicy.config.resources = null;
var resourceId = $location.search()['rsrid'];
if (resourceId) {
- newPolicy.config.resources = [resourceId];
+ ResourceServerResource.get({
+ realm : $route.current.params.realm,
+ client : client.id,
+ rsrid : resourceId
+ }, function(data) {
+ data.text = data.name;
+ $scope.policy.config.resources = data;
+ });
}
},
onCreate : function() {
- $scope.policy.config.resources = JSON.stringify($scope.policy.config.resources);
- $scope.policy.config.applyPolicies = JSON.stringify($scope.policy.config.applyPolicies);
+ $scope.policy.config.resources = JSON.stringify([$scope.policy.config.resources._id]);
+
+ var policies = [];
+
+ for (i = 0; i < $scope.policy.config.applyPolicies.length; i++) {
+ policies.push($scope.policy.config.applyPolicies[i].id);
+ }
+
+ $scope.policy.config.applyPolicies = JSON.stringify(policies);
}
}, realm, client, $scope);
});
@@ -687,105 +944,241 @@ module.controller('ResourceServerPolicyScopeDetailCtrl', function($scope, $route
},
onInit : function() {
- ResourceServerScope.query({realm : realm.realm, client : client.id}, function (data) {
- $scope.scopes = data;
- });
-
- ResourceServerResource.query({realm : realm.realm, client : client.id}, function (data) {
- $scope.resources = data;
- });
-
- ResourceServerPolicy.query({realm : realm.realm, client : client.id}, function (data) {
- $scope.policies = [];
-
- for (i = 0; i < data.length; i++) {
- if (data[i].type != 'resource' && data[i].type != 'scope') {
- $scope.policies.push(data[i]);
+ $scope.scopesUiSelect = {
+ minimumInputLength: 1,
+ delay: 500,
+ allowClear: true,
+ query: function (query) {
+ var data = {results: []};
+ if ('' == query.term.trim()) {
+ query.callback(data);
+ return;
}
+ $scope.query = {
+ realm: realm.realm,
+ client : client.id,
+ name: query.term.trim(),
+ deep: false,
+ max : 20,
+ first : 0
+ };
+ ResourceServerScope.query($scope.query, function(response) {
+ data.results = response;
+ query.callback(data);
+ });
+ },
+ formatResult: function(object, container, query) {
+ object.text = object.name;
+ return object.name;
}
- });
-
- $scope.resolveScopes = function(policy, keepScopes) {
- if (!keepScopes) {
- policy.config.scopes = [];
- }
+ };
- if (!policy) {
- policy = $scope.policy;
+ $scope.resourcesUiSelect = {
+ minimumInputLength: 1,
+ delay: 500,
+ allowClear: true,
+ id: function(resource){ return resource._id; },
+ query: function (query) {
+ var data = {results: []};
+ if ('' == query.term.trim()) {
+ query.callback(data);
+ return;
+ }
+ $scope.query = {
+ realm: realm.realm,
+ client : client.id,
+ name: query.term.trim(),
+ deep: false,
+ max : 20,
+ first : 0
+ };
+ ResourceServerResource.query($scope.query, function(response) {
+ data.results = response;
+ query.callback(data);
+ });
+ },
+ formatResult: function(object, container, query) {
+ object.text = object.name;
+ return object.name;
}
+ };
- if (policy.config.resources != null) {
- ResourceServerResource.get({
- realm : $route.current.params.realm,
+ $scope.policiesUiSelect = {
+ minimumInputLength: 1,
+ delay: 500,
+ allowClear: true,
+ query: function (query) {
+ var data = {results: []};
+ if ('' == query.term.trim()) {
+ query.callback(data);
+ return;
+ }
+ $scope.query = {
+ realm: realm.realm,
client : client.id,
- rsrid : policy.config.resources
- }, function(data) {
- $scope.scopes = data.scopes;
+ permission: false,
+ name: query.term.trim(),
+ max : 20,
+ first : 0
+ };
+ ResourceServerPolicy.query($scope.query, function(response) {
+ data.results = response;
+ query.callback(data);
});
- } else {
- ResourceServerScope.query({realm : realm.realm, client : client.id}, function (data) {
- $scope.scopes = data;
+ },
+ formatResult: function(object, container, query) {
+ object.text = object.name;
+ return object.name;
+ }
+ };
+
+ $scope.selectResource = function() {
+ if ($scope.policy.config.resources) {
+ ResourceServerResource.scopes({
+ realm: $route.current.params.realm,
+ client: client.id,
+ rsrid: $scope.policy.config.resources._id
+ }, function (data) {
+ $scope.policy.config.resources.scopes = data;
});
}
}
},
onInitUpdate : function(policy) {
- if (policy.config.resources) {
- policy.config.resources = eval(policy.config.resources);
+ policy.config.resources = eval(policy.config.resources);
- if (policy.config.resources.length > 0) {
- policy.config.resources = policy.config.resources[0];
- } else {
- policy.config.resources = null;
- }
+ if (policy.config.resources == null) {
+ policy.config.resources = [];
}
- $scope.resolveScopes(policy, true);
+ if (policy.config.resources.length > 0) {
+ ResourceServerResource.query({
+ realm: $route.current.params.realm,
+ client: client.id,
+ _id: policy.config.resources[0],
+ deep: false
+ }, function (data) {
+ data[0].text = data[0].name;
+ $scope.policy.config.resources = data[0];
+ ResourceServerResource.scopes({
+ realm: $route.current.params.realm,
+ client: client.id,
+ rsrid: policy.config.resources[0]
+ }, function (data) {
+ $scope.policy.config.resources.scopes = data;
+ });
+ ResourceServerPolicy.scopes({
+ realm : $route.current.params.realm,
+ client : client.id,
+ id : policy.id
+ }, function(scopes) {
+ $scope.policy.config.scopes = [];
+ for (i = 0; i < scopes.length; i++) {
+ $scope.policy.config.scopes.push(scopes[i].id);
+ }
+ });
+ });
+ } else {
+ policy.config.resources = null;
+ ResourceServerPolicy.scopes({
+ realm : $route.current.params.realm,
+ client : client.id,
+ id : policy.id
+ }, function(scopes) {
+ $scope.policy.config.scopes = [];
+ for (i = 0; i < scopes.length; i++) {
+ scopes[i].text = scopes[i].name;
+ $scope.policy.config.scopes.push(scopes[i]);
+ }
+ });
+ }
- policy.config.applyPolicies = eval(policy.config.applyPolicies);
- policy.config.scopes = eval(policy.config.scopes);
+ policy.config.applyPolicies = [];
+ ResourceServerPolicy.associatedPolicies({
+ realm : $route.current.params.realm,
+ client : client.id,
+ id : policy.id
+ }, function(policies) {
+ for (i = 0; i < policies.length; i++) {
+ policies[i].text = policies[i].name;
+ $scope.policy.config.applyPolicies.push(policies[i]);
+ }
+ });
},
onUpdate : function() {
if ($scope.policy.config.resources != null) {
- var resources = undefined;
+ $scope.policy.config.resources = JSON.stringify([$scope.policy.config.resources._id]);
+ }
- if ($scope.policy.config.resources.length != 0) {
- resources = JSON.stringify([$scope.policy.config.resources])
+ var scopes = [];
+
+ for (i = 0; i < $scope.policy.config.scopes.length; i++) {
+ if ($scope.policy.config.resources == null) {
+ scopes.push($scope.policy.config.scopes[i].id);
+ } else {
+ scopes.push($scope.policy.config.scopes[i]);
}
+ }
- $scope.policy.config.resources = resources;
+ $scope.policy.config.scopes = JSON.stringify(scopes);
+
+ var policies = [];
+
+ for (i = 0; i < $scope.policy.config.applyPolicies.length; i++) {
+ policies.push($scope.policy.config.applyPolicies[i].id);
}
- $scope.policy.config.scopes = JSON.stringify($scope.policy.config.scopes);
- $scope.policy.config.applyPolicies = JSON.stringify($scope.policy.config.applyPolicies);
+ $scope.policy.config.applyPolicies = JSON.stringify(policies);
},
onInitCreate : function(newPolicy) {
newPolicy.decisionStrategy = 'UNANIMOUS';
newPolicy.config = {};
- newPolicy.config.resources = '';
+ newPolicy.config.resources = null;
var scopeId = $location.search()['scpid'];
if (scopeId) {
- newPolicy.config.scopes = [scopeId];
+ ResourceServerScope.get({
+ realm: $route.current.params.realm,
+ client: client.id,
+ id: scopeId,
+ }, function (data) {
+ data.text = data.name;
+ if (!$scope.policy.config.scopes) {
+ $scope.policy.config.scopes = [];
+ }
+ $scope.policy.config.scopes.push(data);
+ });
}
},
onCreate : function() {
if ($scope.policy.config.resources != null) {
- var resources = undefined;
+ $scope.policy.config.resources = JSON.stringify([$scope.policy.config.resources._id]);
+ }
+
+ var scopes = [];
- if ($scope.policy.config.resources.length != 0) {
- resources = JSON.stringify([$scope.policy.config.resources])
+ for (i = 0; i < $scope.policy.config.scopes.length; i++) {
+ if ($scope.policy.config.scopes[i].id) {
+ scopes.push($scope.policy.config.scopes[i].id);
+ } else {
+ scopes.push($scope.policy.config.scopes[i]);
}
+ }
+
+ $scope.policy.config.scopes = JSON.stringify(scopes);
- $scope.policy.config.resources = resources;
+ var policies = [];
+
+ for (i = 0; i < $scope.policy.config.applyPolicies.length; i++) {
+ policies.push($scope.policy.config.applyPolicies[i].id);
}
- $scope.policy.config.scopes = JSON.stringify($scope.policy.config.scopes);
- $scope.policy.config.applyPolicies = JSON.stringify($scope.policy.config.applyPolicies);
+
+ $scope.policy.config.applyPolicies = JSON.stringify(policies);
}
}, realm, client, $scope);
});
@@ -1250,23 +1643,58 @@ module.controller('ResourceServerPolicyAggregateDetailCtrl', function($scope, $r
},
onInit : function() {
- ResourceServerPolicy.query({realm : realm.realm, client : client.id}, function (data) {
- $scope.policies = [];
-
- for (i = 0; i < data.length; i++) {
- if (data[i].type != 'resource' && data[i].type != 'scope') {
- $scope.policies.push(data[i]);
+ $scope.policiesUiSelect = {
+ minimumInputLength: 1,
+ delay: 500,
+ allowClear: true,
+ query: function (query) {
+ var data = {results: []};
+ if ('' == query.term.trim()) {
+ query.callback(data);
+ return;
}
+ $scope.query = {
+ realm: realm.realm,
+ client : client.id,
+ permission: false,
+ name: query.term.trim(),
+ max : 20,
+ first : 0
+ };
+ ResourceServerPolicy.query($scope.query, function(response) {
+ data.results = response;
+ query.callback(data);
+ });
+ },
+ formatResult: function(object, container, query) {
+ object.text = object.name;
+ return object.name;
}
- });
+ };
},
onInitUpdate : function(policy) {
- policy.config.applyPolicies = eval(policy.config.applyPolicies);
+ policy.config.applyPolicies = [];
+ ResourceServerPolicy.associatedPolicies({
+ realm : $route.current.params.realm,
+ client : client.id,
+ id : policy.id
+ }, function(policies) {
+ for (i = 0; i < policies.length; i++) {
+ policies[i].text = policies[i].name;
+ $scope.policy.config.applyPolicies.push(policies[i]);
+ }
+ });
},
onUpdate : function() {
- $scope.policy.config.applyPolicies = JSON.stringify($scope.policy.config.applyPolicies);
+ var policies = [];
+
+ for (i = 0; i < $scope.policy.config.applyPolicies.length; i++) {
+ policies.push($scope.policy.config.applyPolicies[i].id);
+ }
+
+ $scope.policy.config.applyPolicies = JSON.stringify(policies);
},
onInitCreate : function(newPolicy) {
@@ -1275,7 +1703,13 @@ module.controller('ResourceServerPolicyAggregateDetailCtrl', function($scope, $r
},
onCreate : function() {
- $scope.policy.config.applyPolicies = JSON.stringify($scope.policy.config.applyPolicies);
+ var policies = [];
+
+ for (i = 0; i < $scope.policy.config.applyPolicies.length; i++) {
+ policies.push($scope.policy.config.applyPolicies[i].id);
+ }
+
+ $scope.policy.config.applyPolicies = JSON.stringify(policies);
}
}, realm, client, $scope);
});
@@ -1357,9 +1791,9 @@ module.service("PolicyController", function($http, $route, $location, ResourceSe
}
} else {
ResourceServerPolicy.get({
- realm : $route.current.params.realm,
+ realm: realm.realm,
client : client.id,
- id : $route.current.params.id,
+ id: $route.current.params.id
}, function(data) {
$scope.originalPolicy = data;
var policy = angular.copy(data);
@@ -1408,25 +1842,31 @@ module.service("PolicyController", function($http, $route, $location, ResourceSe
$scope.remove = function() {
var msg = "";
- if ($scope.policy.dependentPolicies.length > 0) {
- msg = "<p>This policy is being used by other policies:</p>";
- msg += "<ul>";
- for (i = 0; i < $scope.policy.dependentPolicies.length; i++) {
- msg+= "<li><strong>" + $scope.policy.dependentPolicies[i].name + "</strong></li>";
+ ResourceServerPolicy.dependentPolicies({
+ realm : $route.current.params.realm,
+ client : client.id,
+ id : $scope.policy.id
+ }, function (dependentPolicies) {
+ if (dependentPolicies.length > 0 && !$scope.deleteConsent) {
+ msg = "<p>This policy is being used by other policies:</p>";
+ msg += "<ul>";
+ for (i = 0; i < dependentPolicies.length; i++) {
+ msg+= "<li><strong>" + dependentPolicies[i].name + "</strong></li>";
+ }
+ msg += "</ul>";
+ msg += "<p>If you remove this policy, the policies above will be affected and will not be associated with this policy anymore.</p>";
}
- msg += "</ul>";
- msg += "<p>If you remove this policy, the policies above will be affected and will not be associated with this policy anymore.</p>";
- }
- AuthzDialog.confirmDeleteWithMsg($scope.policy.name, "Policy", msg, function() {
- ResourceServerPolicy.delete({realm : $scope.realm.realm, client : $scope.client.id, id : $scope.policy.id}, null, function() {
- if (delegate.isPermission()) {
- $location.url("/realms/" + realm.realm + "/clients/" + client.id + "/authz/resource-server/permission");
- Notifications.success("The permission has been deleted.");
- } else {
- $location.url("/realms/" + realm.realm + "/clients/" + client.id + "/authz/resource-server/policy");
- Notifications.success("The policy has been deleted.");
- }
+ AuthzDialog.confirmDeleteWithMsg($scope.policy.name, "Policy", msg, function() {
+ ResourceServerPolicy.delete({realm : $scope.realm.realm, client : $scope.client.id, id : $scope.policy.id}, null, function() {
+ if (delegate.isPermission()) {
+ $location.url("/realms/" + realm.realm + "/clients/" + client.id + "/authz/resource-server/permission");
+ Notifications.success("The permission has been deleted.");
+ } else {
+ $location.url("/realms/" + realm.realm + "/clients/" + client.id + "/authz/resource-server/policy");
+ Notifications.success("The policy has been deleted.");
+ }
+ });
});
});
}
@@ -1465,13 +1905,8 @@ module.controller('PolicyEvaluateCtrl', function($scope, $http, $route, $locatio
$scope.authzRequest.context = {};
$scope.authzRequest.context.attributes = {};
$scope.authzRequest.roleIds = [];
- $scope.newResource = {};
$scope.resultUrl = resourceUrl + '/partials/authz/policy/resource-server-policy-evaluate-result.html';
- ResourceServerScope.query({realm : realm.realm, client : client.id}, function (data) {
- $scope.scopes = data;
- });
-
$scope.addContextAttribute = function() {
if (!$scope.newContextAttribute.value || $scope.newContextAttribute.value == '') {
Notifications.error("You must provide a value to a context attribute.");
@@ -1573,33 +2008,39 @@ module.controller('PolicyEvaluateCtrl', function($scope, $http, $route, $locatio
}
$scope.setApplyToResourceType = function() {
- if ($scope.applyResourceType) {
- ResourceServerScope.query({realm : realm.realm, client : client.id}, function (data) {
- $scope.scopes = data;
- });
- }
-
delete $scope.newResource;
$scope.authzRequest.resources = [];
}
$scope.addResource = function() {
- var resource = {};
+ var resource = angular.copy($scope.newResource);
+
+ if (!resource) {
+ resource = {};
+ }
+
+ delete resource.text;
+
+ if (!$scope.newScopes || (resource._id != null && $scope.newScopes.length > 0 && $scope.newScopes[0].id)) {
+ $scope.newScopes = [];
+ }
- resource.id = $scope.newResource._id;
+ var scopes = [];
- for (i = 0; i < $scope.resources.length; i++) {
- if ($scope.resources[i]._id == resource.id) {
- resource.name = $scope.resources[i].name;
- break;
+ for (i = 0; i < $scope.newScopes.length; i++) {
+ if ($scope.newScopes[i].name) {
+ scopes.push($scope.newScopes[i].name);
+ } else {
+ scopes.push($scope.newScopes[i]);
}
}
- resource.scopes = $scope.newResource.scopes;
+ resource.scopes = scopes;
$scope.authzRequest.resources.push(resource);
delete $scope.newResource;
+ delete $scope.newScopes;
}
$scope.removeResource = function(index) {
@@ -1610,20 +2051,11 @@ module.controller('PolicyEvaluateCtrl', function($scope, $http, $route, $locatio
if ($scope.newResource._id) {
$scope.newResource.scopes = [];
$scope.scopes = [];
- ResourceServerResource.get({
+ ResourceServerResource.scopes({
realm: $route.current.params.realm,
- client : client.id,
+ client: client.id,
rsrid: $scope.newResource._id
}, function (data) {
- $scope.scopes = data.scopes;
- if (data.typedScopes) {
- for (i=0;i<data.typedScopes.length;i++) {
- $scope.scopes.push(data.typedScopes[i]);
- }
- }
- });
- } else {
- ResourceServerScope.query({realm : realm.realm, client : client.id}, function (data) {
$scope.scopes = data;
});
}
@@ -1647,7 +2079,17 @@ module.controller('PolicyEvaluateCtrl', function($scope, $http, $route, $locatio
if (!$scope.newResource) {
$scope.newResource = {};
}
- $scope.authzRequest.resources[0].scopes = $scope.newResource.scopes;
+ if (!$scope.newScopes || ($scope.newResource._id != null && $scope.newScopes.length > 0 && $scope.newScopes[0].id)) {
+ $scope.newScopes = [];
+ }
+
+ var scopes = angular.copy($scope.newScopes);
+
+ for (i = 0; i < scopes.length; i++) {
+ delete scopes[i].text;
+ }
+
+ $scope.authzRequest.resources[0].scopes = scopes;
}
$http.post(authUrl + '/admin/realms/'+ $route.current.params.realm + '/clients/' + client.id + '/authz/resource-server/policy/evaluate'
@@ -1697,9 +2139,64 @@ module.controller('PolicyEvaluateCtrl', function($scope, $http, $route, $locatio
}
};
- ResourceServerResource.query({realm : realm.realm, client : client.id}, function (data) {
- $scope.resources = data;
- });
+ $scope.resourcesUiSelect = {
+ minimumInputLength: 1,
+ delay: 500,
+ allowClear: true,
+ id: function(resource){ return resource._id; },
+ query: function (query) {
+ var data = {results: []};
+ if ('' == query.term.trim()) {
+ query.callback(data);
+ return;
+ }
+ $scope.query = {
+ realm: realm.realm,
+ client : client.id,
+ name: query.term.trim(),
+ deep: false,
+ max : 20,
+ first : 0
+ };
+ ResourceServerResource.query($scope.query, function(response) {
+ data.results = response;
+ query.callback(data);
+ });
+ },
+ formatResult: function(object, container, query) {
+ object.text = object.name;
+ return object.name;
+ }
+ };
+
+ $scope.scopesUiSelect = {
+ minimumInputLength: 1,
+ delay: 500,
+ allowClear: true,
+ query: function (query) {
+ var data = {results: []};
+ if ('' == query.term.trim()) {
+ query.callback(data);
+ return;
+ }
+ $scope.query = {
+ realm: realm.realm,
+ client : client.id,
+ name: query.term.trim(),
+ deep: false,
+ max : 20,
+ first : 0
+ };
+ ResourceServerScope.query($scope.query, function(response) {
+ data.results = response;
+ query.callback(data);
+ });
+ },
+ formatResult: function(object, container, query) {
+ object.text = object.name;
+ return object.name;
+ }
+ };
ResourceServer.get({
realm : $route.current.params.realm,
@@ -1717,5 +2214,4 @@ module.controller('PolicyEvaluateCtrl', function($scope, $http, $route, $locatio
$scope.authzRequest.userId = user.id;
}
-
});
\ No newline at end of file
diff --git a/themes/src/main/resources/theme/base/admin/resources/js/authz/authz-services.js b/themes/src/main/resources/theme/base/admin/resources/js/authz/authz-services.js
index 795cf1d..1c4f584 100644
--- a/themes/src/main/resources/theme/base/admin/resources/js/authz/authz-services.js
+++ b/themes/src/main/resources/theme/base/admin/resources/js/authz/authz-services.js
@@ -16,7 +16,9 @@ module.factory('ResourceServerResource', function($resource) {
rsrid : '@rsrid'
}, {
'update' : {method : 'PUT'},
- 'search' : {url: authUrl + '/admin/realms/:realm/clients/:client/authz/resource-server/resource/search', method : 'GET'}
+ 'search' : {url: authUrl + '/admin/realms/:realm/clients/:client/authz/resource-server/resource/search', method : 'GET'},
+ 'scopes' : {url: authUrl + '/admin/realms/:realm/clients/:client/authz/resource-server/resource/:rsrid/scopes', method : 'GET', isArray: true},
+ 'permissions' : {url: authUrl + '/admin/realms/:realm/clients/:client/authz/resource-server/resource/:rsrid/permissions', method : 'GET', isArray: true}
});
});
@@ -27,7 +29,9 @@ module.factory('ResourceServerScope', function($resource) {
id : '@id'
}, {
'update' : {method : 'PUT'},
- 'search' : {url: authUrl + '/admin/realms/:realm/clients/:client/authz/resource-server/scope/search', method : 'GET'}
+ 'search' : {url: authUrl + '/admin/realms/:realm/clients/:client/authz/resource-server/scope/search', method : 'GET'},
+ 'resources' : {url: authUrl + '/admin/realms/:realm/clients/:client/authz/resource-server/scope/:id/resources', method : 'GET', isArray: true},
+ 'permissions' : {url: authUrl + '/admin/realms/:realm/clients/:client/authz/resource-server/scope/:id/permissions', method : 'GET', isArray: true},
});
});
@@ -38,7 +42,11 @@ module.factory('ResourceServerPolicy', function($resource) {
id : '@id'
}, {
'update' : {method : 'PUT'},
- 'search' : {url: authUrl + '/admin/realms/:realm/clients/:client/authz/resource-server/policy/search', method : 'GET'}
+ 'search' : {url: authUrl + '/admin/realms/:realm/clients/:client/authz/resource-server/policy/search', method : 'GET'},
+ 'associatedPolicies' : {url: authUrl + '/admin/realms/:realm/clients/:client/authz/resource-server/policy/:id/associatedPolicies', method : 'GET', isArray: true},
+ 'dependentPolicies' : {url: authUrl + '/admin/realms/:realm/clients/:client/authz/resource-server/policy/:id/dependentPolicies', method : 'GET', isArray: true},
+ 'scopes' : {url: authUrl + '/admin/realms/:realm/clients/:client/authz/resource-server/policy/:id/scopes', method : 'GET', isArray: true},
+ 'resources' : {url: authUrl + '/admin/realms/:realm/clients/:client/authz/resource-server/policy/:id/resources', method : 'GET', isArray: true}
});
});
diff --git a/themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js b/themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js
index 84e2520..3974462 100755
--- a/themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js
+++ b/themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js
@@ -837,6 +837,11 @@ module.controller('ClientDetailCtrl', function($scope, realm, client, templates,
"transient",
"persistent"
];
+ $scope.xmlKeyNameTranformers = [
+ "NONE",
+ "KEY_ID",
+ "CERT_SUBJECT"
+ ];
$scope.canonicalization = [
{name: "EXCLUSIVE", value: "http://www.w3.org/2001/10/xml-exc-c14n#" },
@@ -866,6 +871,7 @@ module.controller('ClientDetailCtrl', function($scope, realm, client, templates,
$scope.samlEncrypt = false;
$scope.samlForcePostBinding = false;
$scope.samlForceNameIdFormat = false;
+ $scope.samlXmlKeyNameTranformer = $scope.xmlKeyNameTranformers[1];
$scope.disableAuthorizationTab = !client.authorizationServicesEnabled;
$scope.disableServiceAccountRolesTab = !client.serviceAccountsEnabled;
$scope.disableCredentialsTab = client.publicClient;
@@ -918,6 +924,13 @@ module.controller('ClientDetailCtrl', function($scope, realm, client, templates,
$scope.samlServerSignatureEnableKeyInfoExtension = false;
}
}
+ if ($scope.client.attributes['saml.server.signature.keyinfo.xmlSigKeyInfoKeyNameTransformer'] === 'NONE') {
+ $scope.samlXmlKeyNameTranformer = $scope.xmlKeyNameTranformers[0];
+ } else if ($scope.client.attributes['saml.server.signature.keyinfo.xmlSigKeyInfoKeyNameTransformer'] === 'KEY_ID') {
+ $scope.samlXmlKeyNameTranformer = $scope.xmlKeyNameTranformers[1];
+ } else if ($scope.client.attributes['saml.server.signature.keyinfo.xmlSigKeyInfoKeyNameTransformer'] === 'CERT_SUBJECT') {
+ $scope.samlXmlKeyNameTranformer = $scope.xmlKeyNameTranformers[2];
+ }
if ($scope.client.attributes["saml.assertion.signature"]) {
if ($scope.client.attributes["saml.assertion.signature"] == "true") {
$scope.samlAssertionSignature = true;
@@ -1037,6 +1050,10 @@ module.controller('ClientDetailCtrl', function($scope, realm, client, templates,
$scope.client.attributes['saml_name_id_format'] = $scope.nameIdFormat;
};
+ $scope.changeSamlSigKeyNameTranformer = function() {
+ $scope.client.attributes['saml.server.signature.keyinfo.xmlSigKeyInfoKeyNameTransformer'] = $scope.samlXmlKeyNameTranformer;
+ };
+
$scope.changeUserInfoSignedResponseAlg = function() {
if ($scope.userInfoSignedResponseAlg === 'unsigned') {
$scope.client.attributes['user.info.response.signature.alg'] = null;
@@ -1079,6 +1096,8 @@ module.controller('ClientDetailCtrl', function($scope, realm, client, templates,
}
$scope.client.publicClient = false;
$scope.client.serviceAccountsEnabled = true;
+ } else if ($scope.client.bearerOnly) {
+ $scope.client.serviceAccountsEnabled = false;
}
}
diff --git a/themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js b/themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js
index e451339..12d37aa 100644
--- a/themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js
+++ b/themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js
@@ -764,11 +764,17 @@ module.controller('RealmIdentityProviderCtrl', function($scope, $filter, $upload
"RSA_SHA512",
"DSA_SHA1"
];
+ $scope.xmlKeyNameTranformers = [
+ "NONE",
+ "KEY_ID",
+ "CERT_SUBJECT"
+ ];
if (instance && instance.alias) {
} else {
$scope.identityProvider.config.nameIDPolicyFormat = $scope.nameIdFormats[0].format;
$scope.identityProvider.config.signatureAlgorithm = $scope.signatureAlgorithms[1];
+ $scope.identityProvider.config.samlXmlKeyNameTranformer = $scope.xmlKeyNameTranformers[1];
}
}
@@ -1120,6 +1126,14 @@ module.controller('RealmKeysProvidersCtrl', function($scope, Realm, realm, $http
type: 'org.keycloak.keys.KeyProvider'
}, function(data) {
$scope.instances = data;
+
+ for (var i = 0; i < $scope.instances.length; i++) {
+ for (var j = 0; j < $scope.providers.length; j++) {
+ if ($scope.providers[j].id === $scope.instances[i].providerId) {
+ $scope.instances[i].provider = $scope.providers[j];
+ }
+ }
+ }
});
$scope.addProvider = function(provider) {
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/authz/permission/provider/resource-server-policy-resource-detail.html b/themes/src/main/resources/theme/base/admin/resources/partials/authz/permission/provider/resource-server-policy-resource-detail.html
index a8d4512..10e3170 100644
--- a/themes/src/main/resources/theme/base/admin/resources/partials/authz/permission/provider/resource-server-policy-resource-detail.html
+++ b/themes/src/main/resources/theme/base/admin/resources/partials/authz/permission/provider/resource-server-policy-resource-detail.html
@@ -39,9 +39,7 @@
<label class="col-md-2 control-label" for="reqActions">{{:: 'authz-resources' | translate}} <span class="required">*</span></label>
<div class="col-md-6">
- <select ui-select2="{ minimumInputLength: 1}" id="reqActions" data-ng-model="policy.config.resources" data-placeholder="{{:: 'authz-select-resource' | translate}}..." multiple data-ng-required="!policy.config.default">
- <option ng-repeat="resource in resources" value="{{resource._id}}" ng-selected="true">{{resource.name}}</option>
- </select>
+ <input type="hidden" ui-select2="resourcesUiSelect" id="reqActions" data-ng-model="policy.config.resources" data-placeholder="{{:: 'authz-select-resource' | translate}}..." data-ng-required="!policy.config.default"/>
</div>
<kc-tooltip>{{:: 'authz-permission-resource-resource.tooltip' | translate}}</kc-tooltip>
</div>
@@ -58,9 +56,7 @@
<label class="col-md-2 control-label" for="reqActions">{{:: 'authz-policy-apply-policy' | translate}} <span class="required">*</span></label>
<div class="col-md-6">
- <select ui-select2 id="reqActions" data-ng-model="policy.config.applyPolicies" data-placeholder="{{:: 'authz-select-a-policy' | translate}}..." multiple required>
- <option ng-repeat="policy in policies" value="{{policy.id}}" ng-selected="true">{{policy.name}}</option>
- </select>
+ <input type="hidden" ui-select2="policiesUiSelect" id="reqActions" data-ng-model="policy.config.applyPolicies" data-placeholder="{{:: 'authz-select-a-policy' | translate}}..." multiple required />
</div>
<kc-tooltip>{{:: 'authz-policy-apply-policy.tooltip' | translate}}</kc-tooltip>
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/authz/permission/provider/resource-server-policy-scope-detail.html b/themes/src/main/resources/theme/base/admin/resources/partials/authz/permission/provider/resource-server-policy-scope-detail.html
index 3d1660b..90a3dc6 100644
--- a/themes/src/main/resources/theme/base/admin/resources/partials/authz/permission/provider/resource-server-policy-scope-detail.html
+++ b/themes/src/main/resources/theme/base/admin/resources/partials/authz/permission/provider/resource-server-policy-scope-detail.html
@@ -32,12 +32,7 @@
<label class="col-md-2 control-label" for="reqActions">{{:: 'authz-resource' | translate}}</label>
<div class="col-md-6">
- <select class="form-control" id="reqActions"
- ng-model="policy.config.resources"
- ng-change="resolveScopes(policy)"
- data-ng-options="resource._id as resource.name for resource in resources">
- <option value="">{{:: 'authz-any-resource' | translate}}...</option>
- </select>
+ <input type="hidden" ui-select2="resourcesUiSelect" data-ng-change="selectResource()" id="reqActions" data-ng-model="policy.config.resources" data-placeholder="{{:: 'authz-any-resource' | translate}}..." />
</div>
<kc-tooltip>{{:: 'authz-permission-scope-resource.tooltip' | translate}}</kc-tooltip>
</div>
@@ -45,25 +40,19 @@
<label class="col-md-2 control-label" for="reqActions">{{:: 'authz-scopes' | translate}} <span class="required">*</span></label>
<div class="col-md-6">
- <select ui-select2 id="reqActions"
+ <select ui-select2 id="reqActions2"
data-ng-model="policy.config.scopes"
data-placeholder="{{:: 'authz-any-scope' | translate}}..." multiple
- data-ng-required="policy.config.resources != ''"
- data-ng-options="scope.id as scope.name for scope in scopes track by scope.id"/>
+ data-ng-required="policy.config.resources != null"
+ data-ng-options="scope.id as scope.name for scope in policy.config.resources.scopes track by scope.id"/>
</div>
-
<kc-tooltip>{{:: 'authz-permission-scope-scope.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group clearfix" data-ng-show="!policy.config.resources">
<label class="col-md-2 control-label" for="reqActions">{{:: 'authz-scopes' | translate}} <span class="required">*</span></label>
<div class="col-md-6">
- <select ui-select2="{ minimumInputLength: 1}" id="reqActions"
- data-ng-model="policy.config.scopes"
- data-placeholder="{{:: 'authz-any-scope' | translate}}..." multiple
- data-ng-required="policy.config.resources == ''"
- data-ng-options="scope.id as scope.name for scope in scopes track by scope.id"/>
- </select>
+ <input type="hidden" ui-select2="scopesUiSelect" id="reqActions" data-ng-model="policy.config.scopes" data-placeholder="{{:: 'authz-any-scope' | translate}}..." multiple data-ng-required="policy.config.resources == null" />
</div>
<kc-tooltip>{{:: 'authz-permission-scope-scope.tooltip' | translate}}</kc-tooltip>
</div>
@@ -71,9 +60,7 @@
<label class="col-md-2 control-label" for="reqActions">{{:: 'authz-policy-apply-policy' | translate}} <span class="required">*</span></label>
<div class="col-md-6">
- <select ui-select2 id="reqActions" data-ng-model="policy.config.applyPolicies" data-placeholder="{{:: 'authz-select-a-policy' | translate}}..." multiple required>
- <option ng-repeat="policy in policies" value="{{policy.id}}" ng-selected="true">{{policy.name}}</option>
- </select>
+ <input type="hidden" ui-select2="policiesUiSelect" id="reqActions" data-ng-model="policy.config.applyPolicies" data-placeholder="{{:: 'authz-select-a-policy' | translate}}..." multiple required />
</div>
<kc-tooltip>{{:: 'authz-policy-apply-policy.tooltip' | translate}}</kc-tooltip>
@@ -95,7 +82,6 @@
</div>
<input type="hidden" data-ng-model="policy.type"/>
</fieldset>
-
<div class="form-group" data-ng-show="access.manageAuthorization">
<div class="col-md-10 col-md-offset-2">
<button kc-save data-ng-disabled="!changed">{{:: 'save' | translate}}</button>
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/authz/permission/resource-server-permission-list.html b/themes/src/main/resources/theme/base/admin/resources/partials/authz/permission/resource-server-permission-list.html
index bfd1b22..419622f 100644
--- a/themes/src/main/resources/theme/base/admin/resources/partials/authz/permission/resource-server-permission-list.html
+++ b/themes/src/main/resources/theme/base/admin/resources/partials/authz/permission/resource-server-permission-list.html
@@ -28,6 +28,12 @@
</select>
</div>
</div>
+ <div class="input-group">
+ <select class="form-control search" data-ng-model="detailsFilter" data-ng-change="searchQuery();">
+ <option value="" selected>Hide Details</option>
+ <option value="true">Show Details</option>
+ </select>
+ </div>
<div class="pull-right">
<select class="form-control" ng-model="policyType"
ng-options="p.name for p in policyProviders track by p.type"
@@ -42,7 +48,7 @@
<th>{{:: 'name' | translate}}</th>
<th>{{:: 'description' | translate}}</th>
<th>{{:: 'type' | translate}}</th>
- <th>{{:: 'authz-associated-policies' | translate}}</th>
+ <th>{{:: 'actions' | translate}}</th>
</tr>
</thead>
<tfoot data-ng-show="policies && (policies.length >= query.max || query.first > 0)">
@@ -52,22 +58,48 @@
<button data-ng-click="firstPage()" class="first" ng-disabled="query.first == 0">{{:: 'first-page' | translate}}</button>
<button data-ng-click="previousPage()" class="prev" ng-disabled="query.first == 0">{{:: 'previous-page' | translate}}</button>
<button data-ng-click="nextPage()" class="next" ng-disabled="policies.length < query.max">{{:: 'next-page' | translate}}</button>
+ <select class="first" data-ng-model="query.max"
+ ng-options="size for size in listSizes" data-ng-change="firstPage()">
+ </select>
</div>
</td>
</tr>
</tfoot>
<tbody>
- <tr ng-repeat="policy in policies | filter: {name: search.name, type: search.type} | orderBy:'name'">
+ <tr ng-repeat-start="policy in policies | filter: {name: search.name, type: search.type} | orderBy:'name'">
<td><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/authz/resource-server/permission/{{policy.type}}/{{policy.id}}">{{policy.name}}</a></td>
<td>{{policy.description}}</td>
<td>{{policy.type}}</td>
- <td>
- <span data-ng-show="!policy.associatedPolicies.length">{{:: 'authz-no-policy-assigned' | translate}}</span>
- <span data-ng-show="policy.associatedPolicies.length > 0">
- <span ng-repeat="policy in policy.associatedPolicies">
- <a href="#/realms/{{realm.realm}}/clients/{{client.id}}/authz/resource-server/policy/{{policy.type}}/{{policy.id}}">{{policy.name}}</a>{{$last ? '' : ', '}}
- </span>
- </span>
+ <td ng-if="!policy.details.loaded" class="kc-action-cell" data-ng-click="showDetails(policy);">
+ {{:: 'authz-show-details' | translate}}
+ </td>
+ <td ng-if="policy.details.loaded" class="kc-action-cell" data-ng-click="showDetails(policy);">
+ {{:: 'authz-hide-details' | translate}}
+ </td>
+ </tr>
+ <tr ng-if="policy.details && policy.details.loaded" ng-repeat-end="">
+ <td colspan="4">
+ <div id="details">
+ <table class="table kc-authz-table-expanded table-striped">
+ <thead>
+ <tr>
+ <th>Associated Permissions</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td>
+ <span data-ng-show="policy.associatedPolicies && !policy.associatedPolicies.length">{{:: 'authz-no-permission-assigned' | translate}}</span>
+ <ul ng-repeat="dep in policy.associatedPolicies" data-ng-show="policy.associatedPolicies.length > 0">
+ <li>
+ <a href="#/realms/{{realm.realm}}/clients/{{client.id}}/authz/resource-server/policy/{{dep.type}}/{{dep.id}}">{{dep.name}}</a>
+ </li>
+ </ul>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
</td>
</tr>
<tr data-ng-show="(policies | filter:search).length == 0">
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/authz/policy/provider/resource-server-policy-aggregate-detail.html b/themes/src/main/resources/theme/base/admin/resources/partials/authz/policy/provider/resource-server-policy-aggregate-detail.html
index 7888f21..2544d3c 100644
--- a/themes/src/main/resources/theme/base/admin/resources/partials/authz/policy/provider/resource-server-policy-aggregate-detail.html
+++ b/themes/src/main/resources/theme/base/admin/resources/partials/authz/policy/provider/resource-server-policy-aggregate-detail.html
@@ -34,11 +34,8 @@
<label class="col-md-2 control-label" for="reqActions">{{:: 'authz-policy-apply-policy' | translate}} <span class="required">*</span></label>
<div class="col-md-6">
- <select ui-select2 id="reqActions" data-ng-model="policy.config.applyPolicies" data-placeholder="{{:: 'authz-select-a-policy' | translate}}..." multiple required>
- <option ng-repeat="policy in policies" value="{{policy.id}}" ng-selected="true">{{policy.name}}</option>
- </select>
+ <input type="hidden" ui-select2="policiesUiSelect" id="reqActions" data-ng-model="policy.config.applyPolicies" data-placeholder="{{:: 'authz-select-a-policy' | translate}}..." multiple required />
</div>
-
<kc-tooltip>{{:: 'authz-policy-apply-policy.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group clearfix">
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/authz/policy/resource-server-policy-evaluate.html b/themes/src/main/resources/theme/base/admin/resources/partials/authz/policy/resource-server-policy-evaluate.html
index d884150..dbaedd3 100644
--- a/themes/src/main/resources/theme/base/admin/resources/partials/authz/policy/resource-server-policy-evaluate.html
+++ b/themes/src/main/resources/theme/base/admin/resources/partials/authz/policy/resource-server-policy-evaluate.html
@@ -172,14 +172,7 @@
<label class="col-md-2 control-label" for="reqActions">{{:: 'authz-resources' | translate}} <span class="required">*</span></label>
<div class="col-md-6">
- <select ui-select2="{ minimumInputLength: 1, allowClear:true }"
- ng-model="newResource._id"
- data-placeholder="Select a resource..."
- data-ng-required="!applyResourceType && authzRequest.resources.length == 0 && !authzRequest.entitlements"
- data-ng-click="resolveScopes()"
- ng-options="resource._id as resource.name for resource in resources track by resource._id">
- <option value=""></option>
- </select>
+ <input type="hidden" ui-select2="resourcesUiSelect" id="reqActions3" data-ng-change="resolveScopes()" data-ng-model="newResource" data-placeholder="{{:: 'authz-select-resource' | translate}}..." data-ng-required="!applyResourceType && authzRequest.resources.length == 0 && !authzRequest.entitlements" />
</div>
<kc-tooltip>{{:: 'authz-permission-resource-resource.tooltip' | translate}}</kc-tooltip>
</div>
@@ -199,12 +192,7 @@
<label class="col-md-2 control-label" for="newResource.scopes">{{:: 'authz-scopes' | translate}}</label>
<div class="col-md-6">
- <select ui-select2="{ minimumInputLength: 1}"
- id="newResource.scopes"
- multiple
- data-ng-model="newResource.scopes"
- data-placeholder="{{:: 'authz-select-scope' | translate}}..."
- data-ng-options="scope.name as scope.name for scope in scopes track by scope.name"/>
+ <input type="hidden" ui-select2="scopesUiSelect" id="reqActions" data-ng-model="newScopes" data-placeholder="{{:: 'authz-any-scope' | translate}}..." multiple />
</div>
<kc-tooltip>{{:: 'authz-permission-scope-scope.tooltip' | translate}}</kc-tooltip>
@@ -215,7 +203,7 @@
<div class="col-md-6">
<select ui-select2
id="newResource.scopes"
- data-ng-model="newResource.scopes"
+ data-ng-model="newScopes"
data-placeholder="{{:: 'authz-any-scope' | translate}}..." multiple>
<option ng-repeat="scope in scopes" value="{{scope.name}}">{{scope.name}}</option>
</select>
@@ -246,11 +234,11 @@
<td>{{resource.name ? resource.name : 'authz-evaluation-any-resource-with-scopes' | translate}}</td>
<td>
<span data-ng-show="!resource.scopes.length">{{:: 'authz-any-scope' | translate}}.</span>
- <span data-ng-show="resource.scopes.length > 0">
- <span ng-repeat="scope in resource.scopes">
- {{scope}} {{$last ? '' : ', '}}
+ <span data-ng-show="resource.scopes.length > 0">
+ <span ng-repeat="scope in resource.scopes">
+ {{scope.name ? scope.name : scope}} {{$last ? '' : ', '}}
+ </span>
</span>
- </span>
</td>
<td class="kc-action-cell">
<button class="btn btn-default btn-block btn-sm"
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/authz/policy/resource-server-policy-list.html b/themes/src/main/resources/theme/base/admin/resources/partials/authz/policy/resource-server-policy-list.html
index 2574f2d..9ad378a 100644
--- a/themes/src/main/resources/theme/base/admin/resources/partials/authz/policy/resource-server-policy-list.html
+++ b/themes/src/main/resources/theme/base/admin/resources/partials/authz/policy/resource-server-policy-list.html
@@ -27,8 +27,16 @@
<option value="" selected ng-click="query.type = ''">{{:: 'authz-all-types' | translate}}</option>
</select>
</div>
+ <div class="input-group">
+ <select class="form-control search" data-ng-model="detailsFilter" data-ng-change="searchQuery();">
+ <option value="" selected>Hide Details</option>
+ <option value="true">Show Details</option>
+ </select>
+ </div>
</div>
<div class="pull-right">
+ <a id="hideDetails" data-ng-show="showDetailsFlag" class="btn btn-default" data-ng-click="showDetailsFlag = !showDetailsFlag;showDetails();" href="">{{:: 'authz-hide-details' | translate}}</a>
+ <a id="showDetails" data-ng-hide="showDetailsFlag" class="btn btn-default" data-ng-click="showDetailsFlag = !showDetailsFlag;showDetails();" href="">{{:: 'authz-show-details' | translate}}</a>
<select class="form-control" ng-model="policyType"
ng-options="p.name for p in policyProviders track by p.type"
data-ng-change="addPolicy(policyType);">
@@ -42,6 +50,7 @@
<th>{{:: 'name' | translate}}</th>
<th>{{:: 'description' | translate}}</th>
<th>{{:: 'type' | translate}}</th>
+ <th>{{:: 'actions' | translate}}</th>
</tr>
</thead>
<tfoot data-ng-show="policies && (policies.length >= query.max || query.first > 0)">
@@ -51,15 +60,49 @@
<button data-ng-click="firstPage()" class="first" ng-disabled="query.first == 0">{{:: 'first-page' | translate}}</button>
<button data-ng-click="previousPage()" class="prev" ng-disabled="query.first == 0">{{:: 'previous-page' | translate}}</button>
<button data-ng-click="nextPage()" class="next" ng-disabled="policies.length < query.max">{{:: 'next-page' | translate}}</button>
+ <select class="first" data-ng-model="query.max"
+ ng-options="size for size in listSizes" data-ng-change="firstPage()">
+ </select>
</div>
</td>
</tr>
</tfoot>
<tbody>
- <tr ng-repeat="policy in policies | filter: {name: search.name, type: search.type} | orderBy:'name'">
+ <tr ng-repeat-start="policy in policies | filter: {name: search.name, type: search.type} | orderBy:'name'">
<td><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/authz/resource-server/policy/{{policy.type}}/{{policy.id}}">{{policy.name}}</a></td>
<td>{{policy.description}}</td>
<td>{{policy.type}}</td>
+ <td ng-if="!policy.details.loaded" class="kc-action-cell" data-ng-click="showDetails(policy);">
+ {{:: 'authz-show-details' | translate}}
+ </td>
+ <td ng-if="policy.details.loaded" class="kc-action-cell" data-ng-click="showDetails(policy);">
+ {{:: 'authz-hide-details' | translate}}
+ </td>
+ </tr>
+ <tr ng-if="policy.details && policy.details.loaded" ng-repeat-end="">
+ <td colspan="4">
+ <div id="details">
+ <table class="table kc-authz-table-expanded table-striped">
+ <thead>
+ <tr>
+ <th>Dependent Permissions</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td>
+ <span data-ng-show="policy.dependentPolicies && !policy.dependentPolicies.length">{{:: 'authz-no-permission-assigned' | translate}}</span>
+ <ul ng-repeat="dep in policy.dependentPolicies" data-ng-show="policy.dependentPolicies.length > 0">
+ <li>
+ <a href="#/realms/{{realm.realm}}/clients/{{client.id}}/authz/resource-server/permission/{{dep.type}}/{{dep.id}}">{{dep.name}}</a>
+ </li>
+ </ul>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ </td>
</tr>
<tr data-ng-show="(policies | filter:search).length == 0">
<td class="text-muted" colspan="3" data-ng-show="search.name">{{:: 'no-results' | translate}}</td>
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/authz/resource-server-resource-detail.html b/themes/src/main/resources/theme/base/admin/resources/partials/authz/resource-server-resource-detail.html
index f61c6e9..ef20ae9 100644
--- a/themes/src/main/resources/theme/base/admin/resources/partials/authz/resource-server-resource-detail.html
+++ b/themes/src/main/resources/theme/base/admin/resources/partials/authz/resource-server-resource-detail.html
@@ -47,9 +47,7 @@
<label class="col-md-2 control-label" for="reqActions">{{:: 'authz-scopes' | translate}}</label>
<div class="col-md-6">
- <select ui-select2 id="reqActions" ng-model="resource.scopes" data-placeholder="{{:: 'authz-select-scope' | translate}}..." multiple>
- <option ng-repeat="scope in scopes" value="{{scope.name}}" ng-selected="true">{{scope.name}}</option>
- </select>
+ <input type="hidden" ui-select2="scopesUiSelect" id="reqActions" data-ng-model="resource.scopes" data-placeholder="{{:: 'authz-select-scope' | translate}}..." multiple/>
</div>
<kc-tooltip>{{:: 'authz-resource-scopes.tooltip' | translate}}</kc-tooltip>
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/authz/resource-server-resource-list.html b/themes/src/main/resources/theme/base/admin/resources/partials/authz/resource-server-resource-list.html
index b1c3978..1f8e1bd 100644
--- a/themes/src/main/resources/theme/base/admin/resources/partials/authz/resource-server-resource-list.html
+++ b/themes/src/main/resources/theme/base/admin/resources/partials/authz/resource-server-resource-list.html
@@ -39,8 +39,13 @@
<i class="fa fa-search" type="submit" data-ng-click="firstPage()"></i>
</div>
</div>
+ <div class="input-group">
+ <select class="form-control search" data-ng-model="detailsFilter" data-ng-change="searchQuery();">
+ <option value="" selected>Hide Details</option>
+ <option value="true">Show Details</option>
+ </select>
+ </div>
</div>
-
<div class="pull-right">
<a id="createResource" class="btn btn-default" href="#/realms/{{realm.realm}}/clients/{{client.id}}/authz/resource-server/resource/create">{{:: 'create' | translate}}</a>
</div>
@@ -52,9 +57,7 @@
<th>{{:: 'type' | translate}}</th>
<th>{{:: 'authz-uri' | translate}}</th>
<th>{{:: 'authz-owner' | translate}}</th>
- <th>{{:: 'authz-scopes' | translate}}</th>
- <th>{{:: 'authz-permissions' | translate}}</th>
- <th>{{:: 'actions' | translate}}</th>
+ <th colspan="2">{{:: 'actions' | translate}}</th>
</tr>
</thead>
<tfoot data-ng-show="resources && (resources.length >= query.max || query.first > 0)">
@@ -64,37 +67,67 @@
<button data-ng-click="firstPage()" class="first" ng-disabled="query.first == 0">{{:: 'first-page' | translate}}</button>
<button data-ng-click="previousPage()" class="prev" ng-disabled="query.first == 0">{{:: 'previous-page' | translate}}</button>
<button data-ng-click="nextPage()" class="next" ng-disabled="resources.length < query.max">{{:: 'next-page' | translate}}</button>
+ <select class="first" data-ng-model="query.max"
+ ng-options="size for size in listSizes" data-ng-change="firstPage()">
+ </select>
</div>
</td>
</tr>
</tfoot>
<tbody>
- <tr ng-repeat="resource in resources | filter:search | orderBy:'name'">
+ <tr ng-repeat-start="resource in resources | filter:search | orderBy:'name'">
<td><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/authz/resource-server/resource/{{resource._id}}">{{resource.name}}</a></td>
<td>
<span data-ng-show="resource.type">{{resource.type}}</span>
<span data-ng-show="!resource.type">{{:: 'authz-no-type-defined' | translate}}</span>
</td>
- <td>{{resource.uri}}</td>
- <td>{{resource.owner.name}}</td>
<td>
- <span data-ng-show="!resource.scopes.length">{{:: 'authz-no-scopes-assigned' | translate}}</span>
- <span data-ng-show="resource.scopes.length > 0">
- <span ng-repeat="scope in resource.scopes">
- <a href="#/realms/{{realm.realm}}/clients/{{client.id}}/authz/resource-server/scope/{{scope.id}}">{{scope.name}}</a>{{$last ? '' : ', '}}
- </span>
- </span>
+ <span data-ng-show="resource.uri">{{resource.uri}}</span>
+ <span data-ng-show="!resource.uri">{{:: 'authz-no-uri-defined' | translate}}</span>
</td>
- <td>
- <span data-ng-show="!resource.policies.length">{{:: 'authz-no-permission-assigned' | translate}}</span>
- <span data-ng-show="resource.policies.length > 0">
- <span ng-repeat="policy in resource.policies">
- <a href="#/realms/{{realm.realm}}/clients/{{client.id}}/authz/resource-server/permission/{{policy.type}}/{{policy.id}}">{{policy.name}}</a>{{$last ? '' : ', '}}
- </span>
- </span>
+ <td>{{resource.owner.name}}</td>
+ <td ng-if="!resource.details.loaded" class="kc-action-cell" data-ng-click="showDetails(resource);">
+ {{:: 'authz-show-details' | translate}}
+ </td>
+ <td ng-if="resource.details.loaded" class="kc-action-cell" data-ng-click="showDetails(resource);">
+ {{:: 'authz-hide-details' | translate}}
+ </td>
+ <td class="kc-action-cell" ng-click="createPolicy(resource);">
+ {{:: 'authz-create-permission' | translate}}
</td>
- <td class="kc-action-cell" style="vertical-align: middle">
- <button class="btn btn-default btn-block btn-sm" ng-click="createPolicy(resource);">{{:: 'authz-create-permission' | translate}}</button>
+ </tr>
+ <tr ng-if="resource.details && resource.details.loaded" ng-repeat-end="">
+ <td colspan="6">
+ <div id="details">
+ <table class="table kc-authz-table-expanded table-striped">
+ <thead>
+ <tr>
+ <th>Scopes</th>
+ <th>Associated Permissions</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td>
+ <span data-ng-show="resource.scopes && !resource.scopes.length">{{:: 'authz-no-scopes-assigned' | translate}}</span>
+ <ul ng-repeat="scope in resource.scopes" data-ng-show="resource.scopes.length > 0">
+ <li>
+ <a href="#/realms/{{realm.realm}}/clients/{{client.id}}/authz/resource-server/scope/{{scope.id}}">{{scope.name}}</a>
+ </li>
+ </ul>
+ </td>
+ <td>
+ <span data-ng-show="resource.policies && !resource.policies.length">{{:: 'authz-no-permission-assigned' | translate}}</span>
+ <ul ng-repeat="policy in resource.policies" data-ng-show="resource.policies.length > 0">
+ <li>
+ <a href="#/realms/{{realm.realm}}/clients/{{client.id}}/authz/resource-server/permission/{{policy.type}}/{{policy.id}}">{{policy.name}}</a>
+ </li>
+ </ul>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
</td>
</tr>
<tr data-ng-show="(resources | filter:search).length == 0">
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/authz/resource-server-scope-list.html b/themes/src/main/resources/theme/base/admin/resources/partials/authz/resource-server-scope-list.html
index 7886129..ff8bb14 100644
--- a/themes/src/main/resources/theme/base/admin/resources/partials/authz/resource-server-scope-list.html
+++ b/themes/src/main/resources/theme/base/admin/resources/partials/authz/resource-server-scope-list.html
@@ -14,6 +14,12 @@
<i class="fa fa-search" id="scopeSearch" type="submit" data-ng-click="firstPage()"></i>
</div>
</div>
+ <div class="input-group">
+ <select class="form-control search" data-ng-model="detailsFilter" data-ng-change="showDetails();">
+ <option value="" selected>Hide Details</option>
+ <option value="true">Show Details</option>
+ </select>
+ </div>
</div>
<div class="pull-right">
<a id="createScope" class="btn btn-default" href="#/realms/{{realm.realm}}/clients/{{client.id}}/authz/resource-server/scope/create">{{:: 'create' | translate}}</a>
@@ -23,43 +29,68 @@
</tr>
<tr data-ng-hide="scopes.length == 0">
<th>{{:: 'name' | translate}}</th>
- <th>{{:: 'authz-resources' | translate}}</th>
- <th>{{:: 'authz-permissions' | translate}}</th>
- <th>{{:: 'actions' | translate}}</th>
+ <th colspan="2">{{:: 'actions' | translate}}</th>
</tr>
</thead>
<tfoot data-ng-show="scopes && (scopes.length >= query.max || query.first > 0)">
<tr>
- <td colspan="7">
+ <td colspan="8">
<div class="table-nav">
<button data-ng-click="firstPage()" class="first" ng-disabled="query.first == 0">{{:: 'first-page' | translate}}</button>
<button data-ng-click="previousPage()" class="prev" ng-disabled="query.first == 0">{{:: 'previous-page' | translate}}</button>
<button data-ng-click="nextPage()" class="next" ng-disabled="scopes.length < query.max">{{:: 'next-page' | translate}}</button>
+ <select class="first" data-ng-model="query.max"
+ ng-options="size for size in listSizes" data-ng-change="firstPage()">
+ </select>
</div>
</td>
</tr>
</tfoot>
<tbody>
- <tr ng-repeat="scope in scopes | filter:search | orderBy:'name'">
+ <tr ng-repeat-start="scope in scopes | filter:search | orderBy:'name'">
<td><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/authz/resource-server/scope/{{scope.id}}">{{scope.name}}</a></td>
- <td>
- <span data-ng-show="!scope.resources.length">{{:: 'authz-no-resources-assigned' | translate}}</span>
- <span data-ng-show="scope.resources.length > 0">
- <span ng-repeat="resource in scope.resources">
- <a href="#/realms/{{realm.realm}}/clients/{{client.id}}/authz/resource-server/resource/{{resource._id}}">{{resource.name}}</a>{{$last ? '' : ', '}}
- </span>
- </span>
+ <td ng-if="!scope.details.loaded" class="kc-action-cell" data-ng-click="showDetails(scope);">
+ {{:: 'authz-show-details' | translate}}
+ </td>
+ <td ng-if="scope.details.loaded" class="kc-action-cell" data-ng-click="showDetails(scope);">
+ {{:: 'authz-hide-details' | translate}}
</td>
- <td>
- <span data-ng-show="!scope.policies.length">{{:: 'authz-no-permission-assigned' | translate}}</span>
- <span data-ng-show="scope.policies.length > 0">
- <span ng-repeat="policy in scope.policies">
- <a href="#/realms/{{realm.realm}}/clients/{{client.id}}/authz/resource-server/permission/{{policy.type}}/{{policy.id}}">{{policy.name}}</a>{{$last ? '' : ', '}}
- </span>
- </span>
+ <td class="kc-action-cell" ng-click="createPolicy(scope);">
+ {{:: 'authz-create-permission' | translate}}
</td>
- <td class="kc-action-cell" style="vertical-align: middle">
- <button class="btn btn-default btn-block btn-sm" ng-click="createPolicy(scope);">{{:: 'authz-create-permission' | translate}}</button>
+ </tr>
+ <tr ng-if="scope.details && scope.details.loaded" ng-repeat-end="">
+ <td colspan="3">
+ <div id="details">
+ <table class="table kc-authz-table-expanded table-striped">
+ <thead>
+ <tr>
+ <th>Resources</th>
+ <th>Associated Permissions</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td>
+ <span data-ng-show="scope.resources && !scope.resources.length">{{:: 'authz-no-resources-assigned' | translate}}</span>
+ <ul ng-repeat="resource in scope.resources" data-ng-show="scope.resources.length > 0">
+ <li>
+ <a href="#/realms/{{realm.realm}}/clients/{{client.id}}/authz/resource-server/resource/{{resource._id}}">{{resource.name}}</a>
+ </li>
+ </ul>
+ </td>
+ <td>
+ <span data-ng-show="scope.policies && !scope.policies.length">{{:: 'authz-no-permission-assigned' | translate}}</span>
+ <ul ng-repeat="policy in scope.policies" data-ng-show="scope.policies.length > 0">
+ <li>
+ <a href="#/realms/{{realm.realm}}/clients/{{client.id}}/authz/resource-server/permission/{{policy.type}}/{{policy.id}}">{{policy.name}}</a>
+ </li>
+ </ul>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
</td>
</tr>
<tr data-ng-show="(scopes | filter:search).length == 0">
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/client-detail.html b/themes/src/main/resources/theme/base/admin/resources/partials/client-detail.html
index 6b0f0a6..af99e7d 100755
--- a/themes/src/main/resources/theme/base/admin/resources/partials/client-detail.html
+++ b/themes/src/main/resources/theme/base/admin/resources/partials/client-detail.html
@@ -110,7 +110,7 @@
<input ng-model="client.serviceAccountsEnabled" name="serviceAccountsEnabled" id="serviceAccountsEnabled" onoffswitch on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}"/>
</div>
</div>
- <div class="form-group" data-ng-show="serverInfo.profileInfo.previewEnabled && protocol == 'openid-connect'">
+ <div class="form-group" data-ng-show="serverInfo.profileInfo.disabledFeatures.indexOf('AUTHORIZATION') == -1 && protocol == 'openid-connect'">
<label class="col-md-2 control-label" for="authorizationServicesEnabled">{{:: 'authz-authorization-services-enabled' | translate}}</label>
<kc-tooltip>{{:: 'authz-authorization-services-enabled.tooltip' | translate}}</kc-tooltip>
<div class="col-md-6">
@@ -158,6 +158,19 @@
</div>
<kc-tooltip>{{:: 'signature-algorithm.tooltip' | translate}}</kc-tooltip>
</div>
+ <div class="form-group clearfix block" data-ng-show="(samlAssertionSignature || samlServerSignature) && protocol == 'saml'">
+ <label class="col-md-2 control-label" for="samlSigKeyNameTranformer">{{:: 'saml-signature-keyName-transformer' | translate}}</label>
+ <div class="col-sm-6">
+ <div>
+ <select class="form-control" id="xmlKeyNameTranformer"
+ ng-change="changeSamlSigKeyNameTranformer()"
+ ng-model="samlXmlKeyNameTranformer"
+ ng-options="alg for alg in xmlKeyNameTranformers">
+ </select>
+ </div>
+ </div>
+ <kc-tooltip>{{:: 'saml-signature-keyName-transformer.tooltip' | translate}}</kc-tooltip>
+ </div>
<div class="form-group" data-ng-show="(samlAssertionSignature || samlServerSignature) && protocol == 'saml'">
<label class="col-md-2 control-label" for="canonicalization">{{:: 'canonicalization-method' | translate}}</label>
<div class="col-sm-6">
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-saml.html b/themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-saml.html
index 49f0e47..3aad92b 100755
--- a/themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-saml.html
+++ b/themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-saml.html
@@ -161,6 +161,18 @@
</div>
<kc-tooltip>{{:: 'signature-algorithm.tooltip' | translate}}</kc-tooltip>
</div>
+ <div class="form-group clearfix block" data-ng-show="identityProvider.config.wantAuthnRequestsSigned == 'true'">
+ <label class="col-md-2 control-label" for="samlSigKeyNameTranformer">{{:: 'saml-signature-keyName-transformer' | translate}}</label>
+ <div class="col-md-6">
+ <div>
+ <select class="form-control" id="samlSigKeyNameTranformer"
+ ng-model="identityProvider.config.xmlSigKeyInfoKeyNameTransformer"
+ ng-options="xmlKeyNameTranformer for xmlKeyNameTranformer in xmlKeyNameTranformers">
+ </select>
+ </div>
+ </div>
+ <kc-tooltip>{{:: 'saml-signature-keyName-transformer.tooltip' | translate}}</kc-tooltip>
+ </div>
<div class="form-group">
<label class="col-md-2 control-label" for="forceAuthn">{{:: 'force-authentication' | translate}}</label>
<div class="col-md-6">
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/realm-keys.html b/themes/src/main/resources/theme/base/admin/resources/partials/realm-keys.html
index cd9cbcd..81215d0 100755
--- a/themes/src/main/resources/theme/base/admin/resources/partials/realm-keys.html
+++ b/themes/src/main/resources/theme/base/admin/resources/partials/realm-keys.html
@@ -13,8 +13,7 @@
<th>{{:: 'type' | translate}}</th>
<th>{{:: 'kid' | translate}}</th>
<th>{{:: 'provider' | translate}}</th>
- <th>{{:: 'publicKey' | translate}}</th>
- <th>{{:: 'certificate' | translate}}</th>
+ <th colspan="2">{{:: 'publicKeys' | translate}}</th>
</tr>
</thead>
<tbody>
@@ -23,11 +22,10 @@
<td>{{key.kid}}</td>
<td><a href="#/realms/{{realm.realm}}/keys/providers/{{key.provider.providerId}}/{{key.provider.id}}">{{key.provider.name}}</a></td>
- <td data-ng-show="key.publicKey" class="kc-action-cell" data-ng-click="viewKey(key.publicKey)">{{:: 'view' | translate}}</td>
- <td data-ng-hide="key.publicKey"></td>
+ <td data-ng-show="key.type === 'RSA'" class="kc-action-cell" data-ng-click="viewKey(key.publicKey)">{{:: 'publicKey' | translate}}</td>
+ <td data-ng-show="key.type === 'RSA'" class="kc-action-cell" data-ng-click="viewKey(key.certificate)">{{:: 'certificate' | translate}}</td>
- <td data-ng-show="key.certificate" class="kc-action-cell" data-ng-click="viewKey(key.certificate)">{{:: 'view' | translate}}</td>
- <td data-ng-hide="key.certificate"></td>
+ <td data-ng-show="key.type !== 'RSA'" colspan="2"></td>
</tr>
</tbody>
</table>
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/realm-keys-list.html b/themes/src/main/resources/theme/base/admin/resources/partials/realm-keys-list.html
index ff47219..41f1684 100755
--- a/themes/src/main/resources/theme/base/admin/resources/partials/realm-keys-list.html
+++ b/themes/src/main/resources/theme/base/admin/resources/partials/realm-keys-list.html
@@ -27,28 +27,26 @@
<table class="table table-striped table-bordered">
<thead>
<tr>
- <th>{{:: 'status' | translate}}</th>
<th>{{:: 'type' | translate}}</th>
+ <th>{{:: 'status' | translate}}</th>
<th>{{:: 'kid' | translate}}</th>
<th>{{:: 'priority' | translate}}</th>
<th>{{:: 'provider' | translate}}</th>
- <th>{{:: 'publicKey' | translate}}</th>
- <th>{{:: 'certificate' | translate}}</th>
+ <th colspan="2">{{:: 'publicKeys' | translate}}</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="key in keys">
- <td>{{key.status}}</td>
<td>{{key.type}}</td>
+ <td>{{key.status}}</td>
<td>{{key.kid}}</td>
<td>{{key.providerPriority}}</td>
<td><a href="#/realms/{{realm.realm}}/keys/providers/{{key.provider.providerId}}/{{key.provider.id}}">{{key.provider.name}}</a></td>
- <td data-ng-show="key.publicKey" class="kc-action-cell" data-ng-click="viewKey(key.publicKey)">{{:: 'view' | translate}}</td>
- <td data-ng-hide="key.publicKey"></td>
+ <td data-ng-show="key.type === 'RSA'" class="kc-action-cell" data-ng-click="viewKey(key.publicKey)">{{:: 'publicKey' | translate}}</td>
+ <td data-ng-show="key.type === 'RSA'" class="kc-action-cell" data-ng-click="viewKey(key.certificate)">{{:: 'certificate' | translate}}</td>
- <td data-ng-show="key.certificate" class="kc-action-cell" data-ng-click="viewKey(key.certificate)">{{:: 'view' | translate}}</td>
- <td data-ng-hide="key.certificate"></td>
+ <td data-ng-show="key.type !== 'RSA'" colspan="2"></td>
</tr>
</tbody>
</table>
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/realm-keys-providers.html b/themes/src/main/resources/theme/base/admin/resources/partials/realm-keys-providers.html
index 810569a..fceea49 100755
--- a/themes/src/main/resources/theme/base/admin/resources/partials/realm-keys-providers.html
+++ b/themes/src/main/resources/theme/base/admin/resources/partials/realm-keys-providers.html
@@ -27,7 +27,7 @@
<table class="table table-striped table-bordered">
<thead>
<tr ng-show="providers.length > 0 && access.manageRealm">
- <th colspan="6" class="kc-table-actions">
+ <th colspan="7" class="kc-table-actions">
<div class="pull-right">
<div>
<select class="form-control" ng-model="selectedProvider"
@@ -40,6 +40,7 @@
</th>
</tr>
<tr data-ng-show="instances && instances.length > 0">
+ <th>{{:: 'type' | translate}}</th>
<th>{{:: 'name' | translate}}</th>
<th>{{:: 'id' | translate}}</th>
<th>{{:: 'provider' | translate}}</th>
@@ -49,6 +50,7 @@
</thead>
<tbody>
<tr ng-repeat="instance in instances">
+ <td>{{instance.provider.metadata.algorithmType}}</td>
<td>{{instance.name}}</td>
<td><a href="#/realms/{{realm.realm}}/keys/providers/{{instance.providerId}}/{{instance.id}}">{{instance.id}}</a></td>
<td>{{instance.providerId}}</td>
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/realm-login-settings.html b/themes/src/main/resources/theme/base/admin/resources/partials/realm-login-settings.html
index 9766c7a..c5a084a 100755
--- a/themes/src/main/resources/theme/base/admin/resources/partials/realm-login-settings.html
+++ b/themes/src/main/resources/theme/base/admin/resources/partials/realm-login-settings.html
@@ -46,6 +46,20 @@
<kc-tooltip>{{:: 'verifyEmail.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group">
+ <label for="loginWithEmailAllowed" class="col-md-2 control-label">{{:: 'loginWithEmailAllowed' | translate}}</label>
+ <div class="col-md-6">
+ <input ng-model="realm.loginWithEmailAllowed" name="loginWithEmailAllowed" id="loginWithEmailAllowed" onoffswitch on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}" />
+ </div>
+ <kc-tooltip>{{:: 'loginWithEmailAllowed.tooltip' | translate}}</kc-tooltip>
+ </div>
+ <div class="form-group" ng-show="!realm.loginWithEmailAllowed && !realm.registrationEmailAsUsername">
+ <label for="duplicateEmailsAllowed" class="col-md-2 control-label">{{:: 'duplicateEmailsAllowed' | translate}}</label>
+ <div class="col-md-6">
+ <input ng-model="realm.duplicateEmailsAllowed" name="duplicateEmailsAllowed" id="duplicateEmailsAllowed" onoffswitch on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}" />
+ </div>
+ <kc-tooltip>{{:: 'duplicateEmailsAllowed.tooltip' | translate}}</kc-tooltip>
+ </div>
+ <div class="form-group">
<label for="sslRequired" class="col-md-2 control-label">{{:: 'sslRequired' | translate}}</label>
<div class="col-md-2">
<div>
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/server-info.html b/themes/src/main/resources/theme/base/admin/resources/partials/server-info.html
index 299a93b..4a4b296 100755
--- a/themes/src/main/resources/theme/base/admin/resources/partials/server-info.html
+++ b/themes/src/main/resources/theme/base/admin/resources/partials/server-info.html
@@ -16,7 +16,11 @@
</tr>
<tr>
<td width="20%">{{:: 'server-profile' | translate}}</td>
- <td>{{serverInfo.profileInfo.name}}</td>
+ <td>{{serverInfo.profileInfo.name | capitalize}}</td>
+ </tr>
+ <tr data-ng-if="serverInfo.profileInfo.disabledFeatures.length > 0">
+ <td width="20%">{{:: 'server-disabled' | translate}}</td>
+ <td>{{serverInfo.profileInfo.disabledFeatures.join(', ').toLowerCase() | capitalize}}</td>
</tr>
<tr>
<td>{{:: 'server-time' | translate}}</td>
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/user-list.html b/themes/src/main/resources/theme/base/admin/resources/partials/user-list.html
index 36510c5..4864acf 100755
--- a/themes/src/main/resources/theme/base/admin/resources/partials/user-list.html
+++ b/themes/src/main/resources/theme/base/admin/resources/partials/user-list.html
@@ -31,7 +31,7 @@
<th class="w-15">{{:: 'email' | translate}}</th>
<th class="w-15">{{:: 'last-name' | translate}}</th>
<th class="w-15">{{:: 'first-name' | translate}}</th>
- <th colspan="{{access.impersonation == true ? '3' : '2'}}">{{:: 'actions' | translate}}</th>
+ <th colspan="{{serverInfo.profileInfo.disabledFeatures.indexOf('IMPERSONATION') == -1 && access.impersonation == true ? '3' : '2'}}">{{:: 'actions' | translate}}</th>
</tr>
</thead>
<tfoot data-ng-show="users && (users.length >= query.max || query.first > 0)">
@@ -53,7 +53,7 @@
<td class="clip">{{user.lastName}}</td>
<td class="clip">{{user.firstName}}</td>
<td class="kc-action-cell" kc-open="/realms/{{realm.realm}}/users/{{user.id}}">{{:: 'edit' | translate}}</td>
- <td data-ng-show="access.impersonation" class="kc-action-cell" data-ng-click="impersonate(user.id)">{{:: 'impersonate' | translate}}</td>
+ <td data-ng-show="serverInfo.profileInfo.disabledFeatures.indexOf('IMPERSONATION') == -1 && access.impersonation" class="kc-action-cell" data-ng-click="impersonate(user.id)">{{:: 'impersonate' | translate}}</td>
<td data-ng-show="access.manageUsers" class="kc-action-cell" data-ng-click="removeUser(user)">{{:: 'delete' | translate}}</td>
</tr>
<tr data-ng-show="!users || users.length == 0">
diff --git a/themes/src/main/resources/theme/base/login/login.ftl b/themes/src/main/resources/theme/base/login/login.ftl
index bcaa952..7c02123 100755
--- a/themes/src/main/resources/theme/base/login/login.ftl
+++ b/themes/src/main/resources/theme/base/login/login.ftl
@@ -9,7 +9,7 @@
<form id="kc-form-login" class="${properties.kcFormClass!}" action="${url.loginAction}" method="post">
<div class="${properties.kcFormGroupClass!}">
<div class="${properties.kcLabelWrapperClass!}">
- <label for="username" class="${properties.kcLabelClass!}"><#if !realm.registrationEmailAsUsername>${msg("usernameOrEmail")}<#else>${msg("email")}</#if></label>
+ <label for="username" class="${properties.kcLabelClass!}"><#if !realm.loginWithEmailAllowed>${msg("username")}<#elseif !realm.registrationEmailAsUsername>${msg("usernameOrEmail")}<#else>${msg("email")}</#if></label>
</div>
<div class="${properties.kcInputWrapperClass!}">
diff --git a/themes/src/main/resources/theme/base/login/login-reset-password.ftl b/themes/src/main/resources/theme/base/login/login-reset-password.ftl
index a561b2a..a0d118a 100755
--- a/themes/src/main/resources/theme/base/login/login-reset-password.ftl
+++ b/themes/src/main/resources/theme/base/login/login-reset-password.ftl
@@ -8,7 +8,7 @@
<form id="kc-reset-password-form" class="${properties.kcFormClass!}" action="${url.loginAction}" method="post">
<div class="${properties.kcFormGroupClass!}">
<div class="${properties.kcLabelWrapperClass!}">
- <label for="username" class="${properties.kcLabelClass!}"><#if !realm.registrationEmailAsUsername>${msg("usernameOrEmail")}<#else>${msg("email")}</#if></label>
+ <label for="username" class="${properties.kcLabelClass!}"><#if !realm.loginWithEmailAllowed>${msg("username")}<#elseif !realm.registrationEmailAsUsername>${msg("usernameOrEmail")}<#else>${msg("email")}</#if></label>
</div>
<div class="${properties.kcInputWrapperClass!}">
<input type="text" id="username" name="username" class="${properties.kcInputClass!}" autofocus/>
diff --git a/themes/src/main/resources/theme/keycloak/admin/resources/css/styles.css b/themes/src/main/resources/theme/keycloak/admin/resources/css/styles.css
index 2eb155e..6f72a78 100755
--- a/themes/src/main/resources/theme/keycloak/admin/resources/css/styles.css
+++ b/themes/src/main/resources/theme/keycloak/admin/resources/css/styles.css
@@ -390,4 +390,13 @@ h1 i {
float: left;
margin-left: 2%;
width: 25%;
+}
+
+table.kc-authz-table-expanded {
+ margin-top: 0px !important;
+}
+
+.no-gutter > [class*='col-'] {
+ padding-right:0!important;
+ padding-left:0!important;
}
\ No newline at end of file
diff --git a/util/embedded-ldap/src/main/resources/ldap/default-users.ldif b/util/embedded-ldap/src/main/resources/ldap/default-users.ldif
index ac3ffcb..22f488a 100644
--- a/util/embedded-ldap/src/main/resources/ldap/default-users.ldif
+++ b/util/embedded-ldap/src/main/resources/ldap/default-users.ldif
@@ -45,6 +45,7 @@ postalCode: 88441
postalCode: 77332
street: Elm 5
userPassword: password
+jpegPhoto:: /9j/4AAQSkZJRgABAQAAAQABAAD/2wCEAAMDAwMDAwQEBAQFBQUFBQcHBgYHBwsICQgJCAsRCwwLCwwLEQ8SDw4PEg8bFRMTFRsfGhkaHyYiIiYwLTA+PlQBAwMDAwMDBAQEBAUFBQUFBwcGBgcHCwgJCAkICxELDAsLDAsRDxIPDg8SDxsVExMVGx8aGRofJiIiJjAtMD4+VP/CABEIAFIAWAMBIgACEQEDEQH/xAAdAAACAgMBAQEAAAAAAAAAAAAGCAUHAwQJAgAB/9oACAEBAAAAAElmzK1aOaraUmpiktrD10DayAIMkKunPQdk+hrZwkUNkMrM88VDtt7r7C1KCJtprbSBP2J6K+VDqUlwErkDnOm/HF9IPDaWQ+cP85sXS7OCj1Iybjj2zVvJA0b91BqJ1JuQDYfkuSWrGdFzsMsWkaIUGHh4IuY1glqtWrK8G89YeJk+ldfT1pHz9//EABoBAAIDAQEAAAAAAAAAAAAAAAUHAwYIBAD/2gAIAQIQAAAApKICefJOj5haleI6vyhC1+FEa9UydaFaD7hDLtU9jttBsCJni6f/xAAaAQADAQEBAQAAAAAAAAAAAAAFBgcDBAII/9oACAEDEAAAAF5+Mdc4X6LUuz2kyGquiYYI/PzvUctR8d5289ghjS48fuOK/wD/xAA5EAABAwIEBAMGAwcFAAAAAAABAgMEABEFBhIhEzFBURQiMgdCYXGBwSOhsRUkM3KCkdFSU2J04f/aAAgBAQABPwGHLbkIVbmjmPhSF60BSL2VUPDhLYLjL4KuotyP0NQPZq5iWBmUiWPGFarIV/DIHS/MGpeC4lhrnh5MdxLgJBuna/wPWsFyW2/kVaH2/wB7dK5LSyPMDbb6KtWD+zqBKypKTIZQJ89q4dPNG10f+1GydizuAPSSgstwlHiawQVnVby/LrTrfBOkqF6gyWmEJQSoKvfVYUiawVbrubc6bQEr4qSkKpDj/iEMKSdLoDYSm/XkRWSco+Bg4dORxo8lCyJcd1NgqxI68tjSG2Wm/wAFKUi97DYVi2GRcZh8F+9goLSR0IpC02TakqA2rE4acSgPw9WgPJ0qPYE71mL2fQJLanYiOGmJh5DKEep125IualtvxJDrDo0OtLKFjsQd6U8tJ2686deDa/Ii1/STvWU8KwDOOVoaZ8VPioV2VLQdC0WNwQRSU2QEk3sNyetPExzq9zr8KEwpulXTrTWJx0OLQp5IOskC/Sm5iFpSpCr3A/S9IeF9zQJPI2r2g5Tw3AWFS0uPOPzpay3q9KE8zc9Tvzpxv6HrWUcCh5hnmC/iQiv6bskp18XvbcVlXJE3K0xbrOJJfadFnGi2U/UG5peocqxPEmYrCtak3I2FTcckBwp1q9PIck87Hbn3oLllYWTvzvUbFpkE7XIv1+FYFizOIM60W18inqmmeIrnXtTaxmW82p2I61CYFkL2Ui6upI5XrhKS0nWL6VadQ95J7VkzKcZzEor4lsHgkL4KwSrWO1ulJdWbeUHbcjaluqAPkX+VZjxMLkur07Nk7cjYdjyvTSzJcceWLFe4+9Mt6x5eQNOBCiR8enSsqylxcSLSdR1g7X273pmVYedJT9Kx7L+G5mjBp911sp9KkqOx+KTsazDl+Rl3EBHcktvNKuptSFbEfEdCKyEGHNi+OKhZUGFNg/1JJ3BoSU99/lTuzZUs2FutZhaSZT5SggJVcG29r9bU1p8OhQuRalyHgBa4TyriWTfqayjEck4kp1CiA0j63VyplTqPKtQV89qnfsl5jhzyzw1/7igP7XrPOF4fgmMNrg4ozIjSFH8BCwtbI7E9u1ZCwvCpjrcyRKQHW3PLG1WJ+N/8U65o3sBSWCs8R3c9B2rN2BGWy7ISQDpsRb86SuRC1IlNLQb9ep+B60ZUbYnaokOXijwaYQdJ5q90bd6wDBF4JESlJC7+Zdhvc00W3htWbZmHYVhi3J8PxUVR0rb06t+l+3zrFXMLU6VQUvJJcWVBQ0pSOiUi6jt3Jpryea52r2c+KxKap9995TEVvyjWbFajtfvWsL+VKW1iBJQoKZbV03uof4qThEWWyriNJWee++9R8uYTH06YrYIH+kU9BaCNKU2t6aw93iJ0KO6axfFIOFSW1OSEsFwn1GwJHOsdbYzhlybEgy21ugC4QoG+nfSfnU6MAtY9JR7vfferqPQmsOzTOw9bDMdTLZTfhtcklZFrnufnWLe0HM+JLMbi+FSfIpDW3zuedZXQzGyxh/IDw4Wf6/Mo1h8pqbCaktnyOi6T8L1PxlqFj0OE5sJLK7fzDkKOlbYVWM4yzl7FY63dmJKTqI90o62r2pQXnDExpg8aK4gNmytkHoR8DTMx6MVLYdW2u1uo/NNL1JSt3UFq08qCVN+pR2pS9SiR32oyPElp824iLIc/5dlUjOstvLyMKCAAEFBdJ9y/Kms2QMNyvBjofC3ww2opQb8/MAfvWPY/IxzFvFqUW9B/BsfSByFSfaGpzAm+E4WZrTyF2725/Q9qx7NT+ZHY7i20t8JrTpBvc33NSJkzgmKH3THJ1cLUdN/lQTtvUSO2664FXA0LWr6C9KJJNConrP8AIaRzXTPo/t+tO/f71J9X0H6U1T1L+1QNncQ/6n3Ff//EACQQAQEAAgICAQQDAQAAAAAAAAERACExQVGBYXGRobEQwfDR/9oACAEBAAE/EFxCJHa+QcvGUwhESkN9fiYHv0VpOm0o9OFKc/OP0nyMqyNS7O0x2FTnAH6HHF+oMkQP6cPbVCpqdHusdzBsTjjKJKrAZwN411hOmhN3Q+OdYFicQWN6QTJzrwEHC7aJwbcdwysS/AW7GNhVpxF5hwesFHQhHh77IyY3UAIHQHgwuHXP36x4apQUBQcWcXJmcwJakbelduaVYQKsBS3eAVJyB+cQnvn1w6o9ZDI6pnUIiPfdxG9iCBQRXreNy18f2PjFCeB0fH3MMVGwtGz1hMB2JKPunWGBO9cLh1omlDTJZj6GlAPSm3RMCgLAgSfWedY3okyFyEYQWneraZLkQPZNfjHeyDpVdBP7dYuNwjIibANNDHmgaDAeXRptkBMDpGVE6Hqjvltx4aCVkJq9o4gOA4y6pRz7bXiMrUKoBArI5ZdacH0tDvKBFDsjg3oRL8G/tceEiW1Y6UAXW0jxK47AXAL0gOg3HkF1MeStC2JfnHWoCStw/wDe804AlIhCOmmMAu7jZ+Fw0B2U6H2hTGAuDJHQLuYV1xK4W0jhBExMKEcCV+5m01yqkMRn8VrZVUN24oNBOeYF1eI6cEGRpvfJrB+GQLVd9ZIRJROhHod4eCYNA8PTGhag8kNPJivYOADUtVxNVsC44dbHxjhJe2AYOXuc16+cQODu1ej3TF3gwCyreivI5JUksX4fesSG1rUxOwRWcCuJY2pBRrL0AQyrY62UfZjjgyEKuhQpIYLK9dtHzfVC4V2Gib2vOsI+EqqQI4gFwY1Py185FplUDmj8LU84jKWQGhYb+GYoKQdBVa8fLgkXPHI8s+uWSNh+pzh6/EJ6CF0MduPBMS2w88OjhgEW0VUC+Ht6xbrYBeevOWtwmrpdV6RSGDx83G9WqMDRe3oFCj2uUoN4FoGLoFWobz7guMGnj2W4jCzVWD1RwXq8VTsWxxafUEV2R2WOBoHSEnGyczAFEcmv43hrndLdMA4GT4cJ8xi4mEFtpKDrTMB0WxjaWEKwJpfaeX95xkQjKxHJ5FgWWx4WjvAwAztJmx6tsUyaoEOUrgVl3jsSC8V1lgV0fwbzc/pzg9R17zkOp+jGh6Z5/wDGuf2M4nvOf+9ZxOos/wD/xAAvEQABAwMCBQMDAwUAAAAAAAACAQMEAAUREiEGBxMxQSJRcSMzYRQygRUXQpGy/9oACAECAQE/AHIDLhl0i0+VQvHvXF3MCTaZ7kW0voatv+o1FP8ABcKBfhavPM+dcbZESOCsSQki49pzj0dk+Fqy81r3FkSBkfUbkzAdQjVS6I9lQUqw8SWzi6GRRjVSHTrBdiHPaisAEuUNa5mXmBDgLFC4lBubGHmCRDFT8YQsYwtQmHpkpBFOorpepPfPmoPAIq0RPvCCGKYRKv3B71pZV8fqte6ePmuVEmaxxMLDUkWmDTL45RNenslA5lNiT/aVzPmzJt7EHzJ1GQwKKONOd65cxYz9wdUw3ANqfaOQaiC6UHbHxVxYaGzTlfX6aNL37ZqG49CurT0UnWzE8obKay/hPNWtXpdqhSFdV7qsoqmrStqq+cj4WuYPB5SVfu7twJWmAXDKhnT7COKslxnW6cMtvIg0qIW22Pav7i2hwOq7EdF3VujZJiuI+MnrzDVuO30owEmtvuq/lVrh+2G1dbZLfbkDBceDW4iEmlM99SVFVhY7e6l6e5KhLTtmjXAFafQTbwuyjsua454JC4223QLXHajNhJQnhBNO2MZ/NWLl0/Luk39Q2rcQeu2Cki6s9gJK4e5ZXNqUSzxT9KXWbcHOC2T0qlcPWz+k2dmC+YPi1kQXTjLefShJ7pTUiO2pAaIqpjG2MJQft/irl+9Ka+4XwtJ9qmu4/NF99z4D/mv/xAAsEQACAQMCBAUDBQAAAAAAAAABAgMABBEFEgYhMUETFCIycVFSYRUzNGJy/9oACAEDAQE/AE09bmQ+F8kNWhcIsrs9wAYmHp+vMVYcIx291cO+JInQqoPXJ61c8MWzW6Rx5HhoQP7HHKtZ0W5tMJMMhjkN8V5B/uFcM2EodZDAjxMNpY9qtoRyIxtHUVLNawYyBQEM6+nGcVxVFB5F96bmHsI7ZokZ61wdEP0pJcDdJzPqzSnZESprWricTAktWgtNtBfJJ7GtRtYZonWRAQQeXatdsXtr90VNoxyAbIrhDXbaJYrKO1IZurA5yfqajYk7GwAwqWxikOSVNJFHbx5BBP4q9uikEojdC/hnCEg1LNHM5Zwuc9xWnRywSq8UrJjmDitP4ha2neS6kYnwiFB+7NXvFFtawxEOrTPtJUdgetapxVBDZFrdwZPSy5/PUGtS1iO7n8xEGQuPUuT1qEhgx25yerVb9Kv/AHU/8l/8VcftrSe5fmovYK//2Q==
dn: cn=ldap-user,ou=RealmRoles,dc=keycloak,dc=org
objectclass: top