diff --git a/README.md b/README.md index c52a172d70..14addf7442 100755 --- a/README.md +++ b/README.md @@ -65,6 +65,7 @@ Contributing * [Testsuite](misc/Testsuite.md) - Details about testsuite, but also how to quickly run Keycloak during development and a few test tools (OTP generation, LDAP server, Mail server) * [Database Testing](misc/DatabaseTesting.md) - How to do testing of Keycloak on different databases * [Updating Database](misc/UpdatingDatabaseSchema.md) - How to change the Keycloak database + * [Changing the Default keycloak-subsystem Configuration](misc/UpdatingServerConfig.md) - How to update the default keycloak-subsystem config * [Developer Mailing List](https://lists.jboss.org/mailman/listinfo/keycloak-dev) - Mailing list to discuss development of Keycloak 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 cfe5ff9a97..bbbf573e34 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 @@ -32,6 +32,7 @@ import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.PathCon 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; @@ -55,7 +56,7 @@ public abstract class AbstractPolicyEnforcer { this.enforcerConfig = policyEnforcer.getEnforcerConfig(); this.authzClient = policyEnforcer.getClient(); this.pathMatcher = new PathMatcher(); - this.paths = policyEnforcer.getPaths(); + this.paths = new ArrayList<>(policyEnforcer.getPaths()); } public AuthorizationContext authorize(OIDCHttpFacade httpFacade) { 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 aa6d3d25d0..88ef9ce00e 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 @@ -33,8 +33,12 @@ import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.PathCon import org.keycloak.representations.idm.authorization.Permission; import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Set; /** @@ -95,8 +99,8 @@ public class PolicyEnforcer { return authzClient; } - List getPaths() { - return paths; + public List getPaths() { + return Collections.unmodifiableList(paths); } KeycloakDeployment getDeployment() { @@ -154,13 +158,27 @@ public class PolicyEnforcer { pathConfig.setId(registrationResponse.getId()); } else { - throw new RuntimeException("Could not find matching resource on server with uri [" + path + "] or name [" + resourceName + ". Make sure you have created a resource on the server that matches with the path configuration."); + throw new RuntimeException("Could not find matching resource on server with uri [" + path + "] or name [" + resourceName + "]. Make sure you have created a resource on the server that matches with the path configuration."); } } else { pathConfig.setId(search.iterator().next()); } - paths.add(pathConfig); + PathConfig existingPath = null; + + for (PathConfig current : paths) { + if (current.getId().equals(pathConfig.getId()) && current.getPath().equals(pathConfig.getPath())) { + existingPath = current; + break; + } + } + + if (existingPath == null) { + paths.add(pathConfig); + } else { + existingPath.getMethods().addAll(pathConfig.getMethods()); + existingPath.getScopes().addAll(pathConfig.getScopes()); + } } return paths; diff --git a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/time/TimePolicyAdminResource.java b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/time/TimePolicyAdminResource.java index 75de911765..96c1d24d25 100644 --- a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/time/TimePolicyAdminResource.java +++ b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/time/TimePolicyAdminResource.java @@ -37,12 +37,10 @@ public class TimePolicyAdminResource implements PolicyProviderAdminService { String nbf = policy.getConfig().get("nbf"); String noa = policy.getConfig().get("noa"); - if (nbf == null && noa == null) { - throw new RuntimeException("You must provide NotBefore, NotOnOrAfter or both."); + if (nbf != null && noa != null) { + validateFormat(nbf); + validateFormat(noa); } - - validateFormat(nbf); - validateFormat(noa); } @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 dc6af9f3da..3205cc5143 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 @@ -22,8 +22,11 @@ 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 static com.sun.corba.se.spi.activation.IIOP_CLEAR_TEXT.value; + /** * @author Pedro Igor */ @@ -45,9 +48,7 @@ public class TimePolicyProvider implements PolicyProvider { public void evaluate(Evaluation evaluation) { try { String notBefore = this.policy.getConfig().get("nbf"); - if (notBefore != null) { - if (this.currentDate.before(this.dateFormat.parse(format(notBefore)))) { evaluation.deny(); return; @@ -55,7 +56,6 @@ public class TimePolicyProvider implements PolicyProvider { } String notOnOrAfter = this.policy.getConfig().get("noa"); - if (notOnOrAfter != null) { if (this.currentDate.after(this.dateFormat.parse(format(notOnOrAfter)))) { evaluation.deny(); @@ -63,12 +63,48 @@ public class TimePolicyProvider implements PolicyProvider { } } + 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")) { + evaluation.deny(); + return; + } + evaluation.grant(); } catch (Exception e) { throw new RuntimeException("Could not evaluate time-based policy [" + this.policy.getName() + "].", e); } } + private boolean isInvalid(int timeConstant, String configName) { + Calendar calendar = Calendar.getInstance(); + + calendar.setTime(this.currentDate); + + int dateField = calendar.get(timeConstant); + + if (Calendar.MONTH == timeConstant) { + dateField++; + } + + String start = this.policy.getConfig().get(configName); + if (start != null) { + String end = this.policy.getConfig().get(configName + "End"); + if (end != null) { + if (dateField < Integer.parseInt(start) || dateField > Integer.parseInt(end)) { + return true; + } + } else { + if (dateField != Integer.parseInt(start)) { + return true; + } + } + } + return false; + } + static String format(String notBefore) { String trimmed = notBefore.trim(); diff --git a/core/src/main/java/org/keycloak/representations/UserInfo.java b/core/src/main/java/org/keycloak/representations/UserInfo.java index 200e8e88d1..784971889f 100755 --- a/core/src/main/java/org/keycloak/representations/UserInfo.java +++ b/core/src/main/java/org/keycloak/representations/UserInfo.java @@ -16,12 +16,31 @@ */ package org.keycloak.representations; +import java.util.HashMap; +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import org.keycloak.json.StringOrArrayDeserializer; +import org.keycloak.json.StringOrArraySerializer; /** * @author pedroigor */ public class UserInfo { + + // Should be in signed UserInfo response + @JsonProperty("iss") + protected String issuer; + @JsonProperty("aud") + @JsonSerialize(using = StringOrArraySerializer.class) + @JsonDeserialize(using = StringOrArrayDeserializer.class) + protected String[] audience; + @JsonProperty("sub") protected String sub; @@ -85,6 +104,34 @@ public class UserInfo { @JsonProperty("claims_locales") protected String claimsLocales; + protected Map otherClaims = new HashMap<>(); + + public String getIssuer() { + return issuer; + } + + public void setIssuer(String issuer) { + this.issuer = issuer; + } + + @JsonIgnore + public String[] getAudience() { + return audience; + } + + public boolean hasAudience(String audience) { + for (String a : this.audience) { + if (a.equals(audience)) { + return true; + } + } + return false; + } + + public void setAudience(String... audience) { + this.audience = audience; + } + public String getSubject() { return this.sub; } @@ -260,4 +307,19 @@ public class UserInfo { public void setClaimsLocales(String claimsLocales) { this.claimsLocales = claimsLocales; } + + /** + * This is a map of any other claims and data that might be in the UserInfo. Could be custom claims set up by the auth server + * + * @return + */ + @JsonAnyGetter + public Map getOtherClaims() { + return otherClaims; + } + + @JsonAnySetter + public void setOtherClaims(String name, Object value) { + otherClaims.put(name, value); + } } diff --git a/distribution/demo-dist/pom.xml b/distribution/demo-dist/pom.xml index 187d62dcec..71a5a1a1ed 100755 --- a/distribution/demo-dist/pom.xml +++ b/distribution/demo-dist/pom.xml @@ -202,6 +202,31 @@ + + + org.apache.maven.plugins + maven-dependency-plugin + + + unpack + compile + + unpack + + + + + org.keycloak + keycloak-wildfly-server-subsystem + ${project.version} + jar + default-config/*.xml + + + + + + diff --git a/distribution/demo-dist/src/main/xslt/standalone.xsl b/distribution/demo-dist/src/main/xslt/standalone.xsl index 4ef3f0e3ea..855efc0c56 100755 --- a/distribution/demo-dist/src/main/xslt/standalone.xsl +++ b/distribution/demo-dist/src/main/xslt/standalone.xsl @@ -1,19 +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. +--> - + jdbc:h2:${jboss.server.data.dir}/keycloak;AUTO_SERVER=TRUE h2 @@ -57,9 +57,7 @@ - - auth - + diff --git a/distribution/feature-packs/server-feature-pack/assembly.xml b/distribution/feature-packs/server-feature-pack/assembly.xml index 41ef1737eb..861c698224 100644 --- a/distribution/feature-packs/server-feature-pack/assembly.xml +++ b/distribution/feature-packs/server-feature-pack/assembly.xml @@ -59,15 +59,4 @@ - - - - src/main/resources/content/standalone/configuration/keycloak-server.json - content/domain/servers/server-one/configuration - - - src/main/resources/content/standalone/configuration/keycloak-server.json - content/domain/servers/server-two/configuration - - diff --git a/distribution/feature-packs/server-feature-pack/src/main/resources/content/standalone/configuration/keycloak-server.json b/distribution/feature-packs/server-feature-pack/src/main/resources/content/standalone/configuration/keycloak-server.json deleted file mode 100755 index b2f106792a..0000000000 --- a/distribution/feature-packs/server-feature-pack/src/main/resources/content/standalone/configuration/keycloak-server.json +++ /dev/null @@ -1,87 +0,0 @@ -{ - "providers": [ - "classpath:${jboss.home.dir}/providers/*" - ], - - "admin": { - "realm": "master" - }, - - "eventsStore": { - "provider": "jpa", - "jpa": { - "exclude-events": [ "REFRESH_TOKEN" ] - } - }, - - "realm": { - "provider": "jpa" - }, - - "user": { - "provider": "jpa" - }, - - "userCache": { - "default" : { - "enabled": true - } - }, - - "userSessionPersister": { - "provider": "jpa" - }, - - "authorizationPersister": { - "provider": "jpa" - }, - - "timer": { - "provider": "basic" - }, - - "theme": { - "staticMaxAge": 2592000, - "cacheTemplates": true, - "cacheThemes": true, - "folder": { - "dir": "${jboss.home.dir}/themes" - } - }, - - "scheduled": { - "interval": 900 - }, - - "connectionsHttpClient": { - "default": {} - }, - - "connectionsJpa": { - "default": { - "dataSource": "java:jboss/datasources/KeycloakDS", - "databaseSchema": "update" - } - }, - - "realmCache": { - "default" : { - "enabled": true - } - }, - - "connectionsInfinispan": { - "provider": "default", - "default": { - "cacheContainer" : "java:comp/env/infinispan/Keycloak" - } - }, - - "jta-lookup": { - "provider": "${keycloak.jta.lookup.provider:jboss}", - "jboss" : { - "enabled": true - } - - } -} \ No newline at end of file diff --git a/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak/org/keycloak/keycloak-services/main/module.xml b/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak/org/keycloak/keycloak-services/main/module.xml index e769a7b265..de03ed8e8b 100755 --- a/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak/org/keycloak/keycloak-services/main/module.xml +++ b/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak/org/keycloak/keycloak-services/main/module.xml @@ -55,6 +55,7 @@ + diff --git a/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak/org/keycloak/keycloak-wildfly-server-subsystem/main/module.xml b/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak/org/keycloak/keycloak-wildfly-server-subsystem/main/module.xml index 9f44e5943b..d82e1a9dde 100644 --- a/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak/org/keycloak/keycloak-wildfly-server-subsystem/main/module.xml +++ b/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak/org/keycloak/keycloak-wildfly-server-subsystem/main/module.xml @@ -28,6 +28,8 @@ + + diff --git a/distribution/server-overlay/assembly.xml b/distribution/server-overlay/assembly.xml index 325cadff19..162bd00764 100755 --- a/distribution/server-overlay/assembly.xml +++ b/distribution/server-overlay/assembly.xml @@ -84,10 +84,6 @@ - - ${project.build.directory}/unpacked/keycloak-${project.version}/standalone/configuration/keycloak-server.json - standalone/configuration - ${project.build.directory}/unpacked/keycloak-${project.version}/bin/add-user-keycloak.sh bin @@ -98,6 +94,11 @@ bin add-user-keycloak.bat + + ${project.build.directory}/cli/default-keycloak-subsys-config.cli + bin + default-keycloak-subsys-config.cli + diff --git a/distribution/server-overlay/cli/keycloak-install-ha.cli b/distribution/server-overlay/cli/keycloak-install-ha.cli index a84a34aeb5..bc7d8639d2 100644 --- a/distribution/server-overlay/cli/keycloak-install-ha.cli +++ b/distribution/server-overlay/cli/keycloak-install-ha.cli @@ -1,5 +1,5 @@ embed-server --server-config=standalone-ha.xml -/subsystem=datasources/data-source=KeycloakDS/:add(connection-url="jdbc:h2:${jboss.server.data.dir}/keycloak;AUTO_SERVER=TRUE",driver-name=h2,jndi-name=java:jboss/datasources/KeycloakDS,password=sa,user-name=sa,use-java-context=true) +/subsystem=datasources/data-source=KeycloakDS/:add(connection-url="jdbc:h2:${jboss.server.data.dir}/keycloak;AUTO_SERVER=TRUE",jta=false,driver-name=h2,jndi-name=java:jboss/datasources/KeycloakDS,password=sa,user-name=sa,use-java-context=true) /subsystem=infinispan/cache-container=keycloak:add(jndi-name="infinispan/Keycloak") /subsystem=infinispan/cache-container=keycloak/transport=TRANSPORT:add(lock-timeout=60000) /subsystem=infinispan/cache-container=keycloak/invalidation-cache=realms:add(mode="SYNC") @@ -11,4 +11,4 @@ embed-server --server-config=standalone-ha.xml /subsystem=infinispan/cache-container=keycloak/distributed-cache=authorization:add(mode="SYNC",owners="1") /subsystem=infinispan/cache-container=keycloak/replicated-cache=work:add(mode="SYNC") /extension=org.keycloak.keycloak-server-subsystem/:add(module=org.keycloak.keycloak-server-subsystem) -/subsystem=keycloak-server:add(web-context=auth) \ No newline at end of file +run-batch --file=default-keycloak-subsys-config.cli diff --git a/distribution/server-overlay/cli/keycloak-install.cli b/distribution/server-overlay/cli/keycloak-install.cli index c29cd5f1e8..dbb2c32eac 100644 --- a/distribution/server-overlay/cli/keycloak-install.cli +++ b/distribution/server-overlay/cli/keycloak-install.cli @@ -1,5 +1,5 @@ embed-server --server-config=standalone.xml -/subsystem=datasources/data-source=KeycloakDS/:add(connection-url="jdbc:h2:${jboss.server.data.dir}/keycloak;AUTO_SERVER=TRUE",driver-name=h2,jndi-name=java:jboss/datasources/KeycloakDS,password=sa,user-name=sa,use-java-context=true) +/subsystem=datasources/data-source=KeycloakDS/:add(connection-url="jdbc:h2:${jboss.server.data.dir}/keycloak;AUTO_SERVER=TRUE",jta=false,driver-name=h2,jndi-name=java:jboss/datasources/KeycloakDS,password=sa,user-name=sa,use-java-context=true) /subsystem=infinispan/cache-container=keycloak:add(jndi-name="infinispan/Keycloak") /subsystem=infinispan/cache-container=keycloak/local-cache=realms:add() /subsystem=infinispan/cache-container=keycloak/local-cache=users:add() @@ -11,4 +11,4 @@ embed-server --server-config=standalone.xml /subsystem=infinispan/cache-container=keycloak/local-cache=authorization:add() /subsystem=infinispan/cache-container=keycloak/local-cache=authorization/eviction=EVICTION:add(max-entries=100,strategy=LRU) /extension=org.keycloak.keycloak-server-subsystem/:add(module=org.keycloak.keycloak-server-subsystem) -/subsystem=keycloak-server:add(web-context=auth) \ No newline at end of file +run-batch --file=default-keycloak-subsys-config.cli \ No newline at end of file diff --git a/distribution/server-overlay/pom.xml b/distribution/server-overlay/pom.xml index 96400d14e9..900bc29657 100755 --- a/distribution/server-overlay/pom.xml +++ b/distribution/server-overlay/pom.xml @@ -45,6 +45,25 @@ org.apache.maven.plugins maven-dependency-plugin + + unpack + compile + + unpack + + + + + org.keycloak + keycloak-wildfly-server-subsystem + ${project.version} + jar + cli/*.cli + ${project.build.directory} + + + + unpack-server-dist prepare-package diff --git a/examples/authz/photoz/photoz-restful-api/pom.xml b/examples/authz/photoz/photoz-restful-api/pom.xml index ba7a88079f..7622af4f85 100755 --- a/examples/authz/photoz/photoz-restful-api/pom.xml +++ b/examples/authz/photoz/photoz-restful-api/pom.xml @@ -39,6 +39,12 @@ 1.0.0.Final provided + + javax.enterprise + cdi-api + 1.0-SP4 + provided + org.keycloak keycloak-authz-client diff --git a/examples/authz/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/admin/AdminAlbumService.java b/examples/authz/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/admin/AdminAlbumService.java index b349e026d4..428ba078bd 100644 --- a/examples/authz/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/admin/AdminAlbumService.java +++ b/examples/authz/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/admin/AdminAlbumService.java @@ -19,9 +19,8 @@ package org.keycloak.example.photoz.admin; import org.keycloak.example.photoz.entity.Album; -import javax.ejb.Stateless; +import javax.inject.Inject; import javax.persistence.EntityManager; -import javax.persistence.PersistenceContext; import javax.ws.rs.GET; import javax.ws.rs.Path; import javax.ws.rs.Produces; @@ -36,12 +35,11 @@ import java.util.List; * @author Pedro Igor */ @Path("/admin/album") -@Stateless public class AdminAlbumService { public static final String SCOPE_ADMIN_ALBUM_MANAGE = "urn:photoz.com:scopes:album:admin:manage"; - @PersistenceContext + @Inject private EntityManager entityManager; @Context diff --git a/examples/authz/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/album/AlbumService.java b/examples/authz/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/album/AlbumService.java index a5d7f161b4..d0e9c2d5c5 100644 --- a/examples/authz/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/album/AlbumService.java +++ b/examples/authz/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/album/AlbumService.java @@ -7,10 +7,12 @@ import org.keycloak.authorization.client.representation.ScopeRepresentation; import org.keycloak.authorization.client.resource.ProtectionResource; import org.keycloak.example.photoz.ErrorResponse; import org.keycloak.example.photoz.entity.Album; +import org.keycloak.example.photoz.util.Transaction; import org.keycloak.representations.adapters.config.AdapterConfig; import org.keycloak.util.JsonSerialization; import javax.ejb.Stateless; +import javax.inject.Inject; import javax.persistence.EntityManager; import javax.persistence.PersistenceContext; import javax.persistence.Query; @@ -31,14 +33,14 @@ import java.util.List; import java.util.Set; @Path("/album") -@Stateless +@Transaction public class AlbumService { public static final String SCOPE_ALBUM_VIEW = "urn:photoz.com:scopes:album:view"; public static final String SCOPE_ALBUM_CREATE = "urn:photoz.com:scopes:album:create"; public static final String SCOPE_ALBUM_DELETE = "urn:photoz.com:scopes:album:delete"; - @PersistenceContext + @Inject private EntityManager entityManager; @Context diff --git a/examples/authz/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/album/ProfileService.java b/examples/authz/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/album/ProfileService.java index be638b602e..f7b55cbc33 100644 --- a/examples/authz/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/album/ProfileService.java +++ b/examples/authz/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/album/ProfileService.java @@ -18,6 +18,7 @@ package org.keycloak.example.photoz.album; import javax.ejb.Stateless; +import javax.inject.Inject; import javax.persistence.EntityManager; import javax.persistence.PersistenceContext; import javax.servlet.http.HttpServletRequest; @@ -34,12 +35,11 @@ import java.util.List; * @author Pedro Igor */ @Path("/profile") -@Stateless public class ProfileService { private static final String PROFILE_VIEW = "urn:photoz.com:scopes:profile:view"; - @PersistenceContext + @Inject private EntityManager entityManager; @GET diff --git a/examples/authz/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/util/Resources.java b/examples/authz/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/util/Resources.java new file mode 100644 index 0000000000..c917da24db --- /dev/null +++ b/examples/authz/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/util/Resources.java @@ -0,0 +1,51 @@ +/* + * 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.photoz.util; + +import javax.annotation.PostConstruct; +import javax.annotation.PreDestroy; +import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.context.RequestScoped; +import javax.enterprise.inject.Produces; +import javax.persistence.EntityManager; +import javax.persistence.EntityManagerFactory; +import javax.persistence.Persistence; + +/** + * @author Pedro Igor + */ +@ApplicationScoped +public class Resources { + + private EntityManagerFactory entityManagerFactory; + + @PostConstruct + public void init() { + entityManagerFactory = Persistence.createEntityManagerFactory("primary"); + } + + @PreDestroy + public void dispose() { + entityManagerFactory.close(); + } + + @RequestScoped + @Produces + public EntityManager createEntityManager() { + return entityManagerFactory.createEntityManager(); + } +} diff --git a/examples/authz/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/util/Transaction.java b/examples/authz/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/util/Transaction.java new file mode 100644 index 0000000000..a3caa780a8 --- /dev/null +++ b/examples/authz/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/util/Transaction.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.example.photoz.util; + +import javax.interceptor.InterceptorBinding; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * @author Pedro Igor + */ +@InterceptorBinding +@Target({ TYPE }) +@Retention(RUNTIME) +public @interface Transaction { +} diff --git a/examples/authz/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/util/TransactionInterceptor.java b/examples/authz/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/util/TransactionInterceptor.java new file mode 100644 index 0000000000..36d35f319b --- /dev/null +++ b/examples/authz/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/util/TransactionInterceptor.java @@ -0,0 +1,56 @@ +/* + * 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.photoz.util; + +import javax.enterprise.inject.Instance; +import javax.inject.Inject; +import javax.interceptor.AroundInvoke; +import javax.interceptor.Interceptor; +import javax.interceptor.InvocationContext; +import javax.persistence.EntityManager; +import javax.persistence.EntityTransaction; + +/** + * @author Pedro Igor + */ +@Interceptor +@Transaction +public class TransactionInterceptor { + + @Inject + private Instance entityManager; + + @AroundInvoke + public Object aroundInvoke(InvocationContext context) { + EntityManager entityManager = this.entityManager.get(); + EntityTransaction transaction = entityManager.getTransaction(); + + try { + transaction.begin(); + Object proceed = context.proceed(); + transaction.commit(); + return proceed; + } catch (Exception cause) { + if (transaction != null && transaction.isActive()) { + transaction.rollback(); + } + throw new RuntimeException(cause); + } finally { + entityManager.close(); + } + } +} diff --git a/examples/authz/photoz/photoz-restful-api/src/main/resources/META-INF/beans.xml b/examples/authz/photoz/photoz-restful-api/src/main/resources/META-INF/beans.xml index 957dc8ac43..fbf2a32edf 100644 --- a/examples/authz/photoz/photoz-restful-api/src/main/resources/META-INF/beans.xml +++ b/examples/authz/photoz/photoz-restful-api/src/main/resources/META-INF/beans.xml @@ -3,5 +3,7 @@ xsi:schemaLocation=" http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/beans_1_0.xsd"> - + + org.keycloak.example.photoz.util.TransactionInterceptor + diff --git a/examples/authz/photoz/photoz-restful-api/src/main/resources/META-INF/persistence.xml b/examples/authz/photoz/photoz-restful-api/src/main/resources/META-INF/persistence.xml index 9323182405..c15d34f449 100644 --- a/examples/authz/photoz/photoz-restful-api/src/main/resources/META-INF/persistence.xml +++ b/examples/authz/photoz/photoz-restful-api/src/main/resources/META-INF/persistence.xml @@ -4,14 +4,18 @@ xsi:schemaLocation=" http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd"> - - java:jboss/datasources/PhotozDS + + org.hibernate.ejb.HibernatePersistence org.keycloak.example.photoz.entity.Album org.keycloak.example.photoz.entity.Photo - + + + + + diff --git a/examples/authz/photoz/photoz-restful-api/src/main/webapp/META-INF/jboss-deployment-structure.xml b/examples/authz/photoz/photoz-restful-api/src/main/webapp/META-INF/jboss-deployment-structure.xml index 4b23be62d7..455335898f 100644 --- a/examples/authz/photoz/photoz-restful-api/src/main/webapp/META-INF/jboss-deployment-structure.xml +++ b/examples/authz/photoz/photoz-restful-api/src/main/webapp/META-INF/jboss-deployment-structure.xml @@ -20,6 +20,7 @@ + diff --git a/examples/authz/photoz/photoz-restful-api/src/main/webapp/WEB-INF/photoz-ds.xml b/examples/authz/photoz/photoz-restful-api/src/main/webapp/WEB-INF/photoz-ds.xml deleted file mode 100644 index 247448f2ae..0000000000 --- a/examples/authz/photoz/photoz-restful-api/src/main/webapp/WEB-INF/photoz-ds.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - jdbc:h2:${jboss.server.data.dir}/kc-authz-photo;AUTO_SERVER=TRUE - h2 - - sa - sa - - - \ No newline at end of file diff --git a/examples/broker/google-authentication/README.md b/examples/broker/google-authentication/README.md index 0825aed632..48ddca70ae 100644 --- a/examples/broker/google-authentication/README.md +++ b/examples/broker/google-authentication/README.md @@ -3,7 +3,7 @@ What is it? ----------- -This example demonstrates how to use Social Ientity Providers with Keycloak to authenticate users. In this case, +This example demonstrates how to use Social Identity Providers with Keycloak to authenticate users. In this case, users are authenticated with Google using Keycloak Identity Broker capabilities using the oAuth 2 protocol. From this example, you'll learn how to: @@ -180,4 +180,4 @@ Debug the Application If you want to debug the source code or look at the Javadocs of any library in the project, run either of the following commands to pull them into your local repository. The IDE should then detect them. mvn dependency:sources - mvn dependency:resolve -Dclassifier=javadoc \ No newline at end of file + mvn dependency:resolve -Dclassifier=javadoc diff --git a/examples/broker/twitter-authentication/README.md b/examples/broker/twitter-authentication/README.md index 5e93083bbf..8f33d2b3e2 100644 --- a/examples/broker/twitter-authentication/README.md +++ b/examples/broker/twitter-authentication/README.md @@ -3,7 +3,7 @@ What is it? ----------- -This example demonstrates how to use Social Ientity Providers with Keycloak to authenticate users. In this case, +This example demonstrates how to use Social Identity Providers with Keycloak to authenticate users. In this case, users are authenticated with Twitter using Keycloak Identity Broker capabilities using the oAuth 2 protocol. From this example, you'll learn how to: @@ -179,4 +179,4 @@ Debug the Application If you want to debug the source code or look at the Javadocs of any library in the project, run either of the following commands to pull them into your local repository. The IDE should then detect them. mvn dependency:sources - mvn dependency:resolve -Dclassifier=javadoc \ No newline at end of file + mvn dependency:resolve -Dclassifier=javadoc diff --git a/misc/UpdatingServerConfig.md b/misc/UpdatingServerConfig.md new file mode 100644 index 0000000000..a3282af996 --- /dev/null +++ b/misc/UpdatingServerConfig.md @@ -0,0 +1,65 @@ +# Changing the Default *keycloak-subsystem* Configuration + +If you need to make a change to the default keycloak-subsystem +configuration that is packaged with our distributions, you will need to edit this file: +https://github.com/keycloak/keycloak/blob/master/wildfly/server-subsystem/src/main/config/default-server-subsys-config.properties + +This file contains a single multi-line property containing the subsystem +xml declaration. Maven filtering is used to read this property and +inject it everywhere it needs to go. Editing this file will also take +care of propagating it to the distributions like server-dist and demo-dist. + +Also, you need to create CLI commands for each change by editing this file: +https://github.com/keycloak/keycloak/blob/master/wildfly/server-subsystem/src/main/resources/cli/default-keycloak-subsys-config.cli + +This CLI snippet is used in the scripts required by the overlay distribution. + +## Updating an SPI +The changes you will likely make are when you need to add a new SPI, change an existing SPI, or add/change a provider within an SPI. + +All elements in an SPI declaration are optional, but a full SPI declaration + looks like this: +````xml + + mongo + + + + + + + + + + + + +```` +Here we have two providers defined for the SPI `dblock`. The +`default-provider` is listed as `mongo`. However it is up to the SPI to decide how it will +treat this setting. Some SPIs allow more than one provider and some do not. So +`default-provider` can help the SPI to choose. + +Also notice that each provider defines its own set of configuration +properties. The fact that both providers above have a property called +`lockWaitTimeout` is just a coincidence. + +## Values of type *List* +The type of each property value is interpreted by the provider. However, +there is one exception. Consider the `jpa` provider for the `eventStore` API: +````xml + + + + + + + +```` +We see that the value begins and ends with square brackets. That means that +the value will be passed to the provider as a list. In this example, +the system will pass the +provider a list with two element values `EVENT1` and `EVENT2`. To add +more values to the list, just separate each list element with a comma. Unfortunately, +you do need to escape the quotes surrounding each list element with +`"`. diff --git a/model/jpa/src/main/java/org/keycloak/connections/jpa/DefaultJpaConnectionProviderFactory.java b/model/jpa/src/main/java/org/keycloak/connections/jpa/DefaultJpaConnectionProviderFactory.java index 4272fb8869..3c74264f8d 100755 --- a/model/jpa/src/main/java/org/keycloak/connections/jpa/DefaultJpaConnectionProviderFactory.java +++ b/model/jpa/src/main/java/org/keycloak/connections/jpa/DefaultJpaConnectionProviderFactory.java @@ -17,6 +17,7 @@ package org.keycloak.connections.jpa; +import java.io.File; import java.sql.Connection; import java.sql.DatabaseMetaData; import java.sql.DriverManager; @@ -42,6 +43,7 @@ import org.keycloak.models.dblock.DBLockProvider; import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.provider.ServerInfoAwareProviderFactory; import org.keycloak.models.dblock.DBLockManager; +import org.keycloak.ServerStartupError; import org.keycloak.timer.TimerProvider; /** @@ -51,6 +53,10 @@ public class DefaultJpaConnectionProviderFactory implements JpaConnectionProvide private static final Logger logger = Logger.getLogger(DefaultJpaConnectionProviderFactory.class); + enum MigrationStrategy { + UPDATE, VALIDATE, MANUAL + } + private volatile EntityManagerFactory emf; private Config.Scope config; @@ -125,22 +131,9 @@ public class DefaultJpaConnectionProviderFactory implements JpaConnectionProvide properties.put(JpaUtils.HIBERNATE_DEFAULT_SCHEMA, schema); } - - String databaseSchema; - String databaseSchemaConf = config.get("databaseSchema"); - if (databaseSchemaConf == null) { - throw new RuntimeException("Property 'databaseSchema' needs to be specified in the configuration"); - } - - if (databaseSchemaConf.equals("development-update")) { - properties.put("hibernate.hbm2ddl.auto", "update"); - databaseSchema = null; - } else if (databaseSchemaConf.equals("development-validate")) { - properties.put("hibernate.hbm2ddl.auto", "validate"); - databaseSchema = null; - } else { - databaseSchema = databaseSchemaConf; - } + MigrationStrategy migrationStrategy = getMigrationStrategy(); + boolean initializeEmpty = config.getBoolean("initializeEmpty", true); + File databaseUpdateFile = getDatabaseUpdateFile(); properties.put("hibernate.show_sql", config.getBoolean("showSql", false)); properties.put("hibernate.format_sql", config.getBoolean("formatSql", true)); @@ -153,39 +146,8 @@ public class DefaultJpaConnectionProviderFactory implements JpaConnectionProvide if (driverDialect != null) { properties.put("hibernate.dialect", driverDialect); } - - if (databaseSchema != null) { - logger.trace("Updating database"); - - JpaUpdaterProvider updater = session.getProvider(JpaUpdaterProvider.class); - if (updater == null) { - throw new RuntimeException("Can't update database: JPA updater provider not found"); - } - // Check if having DBLock before trying to initialize hibernate - DBLockProvider dbLock = new DBLockManager(session).getDBLock(); - if (dbLock.hasLock()) { - updateOrValidateDB(databaseSchema, connection, updater, schema); - } else { - logger.trace("Don't have DBLock retrieved before upgrade. Needs to acquire lock first in separate transaction"); - - KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), new KeycloakSessionTask() { - - @Override - public void run(KeycloakSession lockSession) { - DBLockManager dbLockManager = new DBLockManager(lockSession); - DBLockProvider dbLock2 = dbLockManager.getDBLock(); - dbLock2.waitForLock(); - try { - updateOrValidateDB(databaseSchema, connection, updater, schema); - } finally { - dbLock2.releaseLock(); - } - } - - }); - } - } + migration(migrationStrategy, initializeEmpty, schema, databaseUpdateFile, connection, session); int globalStatsInterval = config.getInt("globalStatsInterval", -1); if (globalStatsInterval != -1) { @@ -199,18 +161,6 @@ public class DefaultJpaConnectionProviderFactory implements JpaConnectionProvide if (globalStatsInterval != -1) { startGlobalStats(session, globalStatsInterval); } - - } catch (Exception e) { - // Safe rollback - if (connection != null) { - try { - connection.rollback(); - } catch (SQLException e2) { - logger.warn("Can't rollback connection", e2); - } - } - - throw e; } finally { // Close after creating EntityManagerFactory to prevent in-mem databases from closing if (connection != null) { @@ -226,6 +176,11 @@ public class DefaultJpaConnectionProviderFactory implements JpaConnectionProvide } } + private File getDatabaseUpdateFile() { + String databaseUpdateFile = config.get("migrationExport", "keycloak-database-update.sql"); + return new File(databaseUpdateFile); + } + protected void prepareOperationalInfo(Connection connection) { try { operationalInfo = new LinkedHashMap<>(); @@ -282,20 +237,82 @@ public class DefaultJpaConnectionProviderFactory implements JpaConnectionProvide timer.scheduleTask(new HibernateStatsReporter(emf), globalStatsIntervalSecs * 1000, "ReportHibernateGlobalStats"); } + public void migration(MigrationStrategy strategy, boolean initializeEmpty, String schema, File databaseUpdateFile, Connection connection, KeycloakSession session) { + JpaUpdaterProvider updater = session.getProvider(JpaUpdaterProvider.class); - // Needs to be called with acquired DBLock - protected void updateOrValidateDB(String databaseSchema, Connection connection, JpaUpdaterProvider updater, String schema) { - if (databaseSchema.equals("update")) { - updater.update(connection, schema); - logger.trace("Database update completed"); - } else if (databaseSchema.equals("validate")) { - updater.validate(connection, schema); - logger.trace("Database validation completed"); + JpaUpdaterProvider.Status status = updater.validate(connection, schema); + if (status == JpaUpdaterProvider.Status.VALID) { + logger.debug("Database is up-to-date"); + } else if (status == JpaUpdaterProvider.Status.EMPTY) { + if (initializeEmpty) { + update(connection, schema, session, updater); + } else { + switch (strategy) { + case UPDATE: + update(connection, schema, session, updater); + break; + case MANUAL: + export(connection, schema, databaseUpdateFile, session, updater); + throw new ServerStartupError("Database not initialized, please initialize database with " + databaseUpdateFile.getAbsolutePath(), false); + case VALIDATE: + throw new ServerStartupError("Database not initialized, please enable database initialization", false); + } + } } else { - throw new RuntimeException("Invalid value for databaseSchema: " + databaseSchema); + switch (strategy) { + case UPDATE: + update(connection, schema, session, updater); + break; + case MANUAL: + export(connection, schema, databaseUpdateFile, session, updater); + throw new ServerStartupError("Database not up-to-date, please migrate database with " + databaseUpdateFile.getAbsolutePath(), false); + case VALIDATE: + throw new ServerStartupError("Database not up-to-date, please enable database migration", false); + } } } + protected void update(Connection connection, String schema, KeycloakSession session, JpaUpdaterProvider updater) { + DBLockProvider dbLock = new DBLockManager(session).getDBLock(); + if (dbLock.hasLock()) { + updater.update(connection, schema); + } else { + KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), new KeycloakSessionTask() { + @Override + public void run(KeycloakSession lockSession) { + DBLockManager dbLockManager = new DBLockManager(lockSession); + DBLockProvider dbLock2 = dbLockManager.getDBLock(); + dbLock2.waitForLock(); + try { + updater.update(connection, schema); + } finally { + dbLock2.releaseLock(); + } + } + }); + } + } + + protected void export(Connection connection, String schema, File databaseUpdateFile, KeycloakSession session, JpaUpdaterProvider updater) { + DBLockProvider dbLock = new DBLockManager(session).getDBLock(); + if (dbLock.hasLock()) { + updater.export(connection, schema, databaseUpdateFile); + } else { + KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), new KeycloakSessionTask() { + @Override + public void run(KeycloakSession lockSession) { + DBLockManager dbLockManager = new DBLockManager(lockSession); + DBLockProvider dbLock2 = dbLockManager.getDBLock(); + dbLock2.waitForLock(); + try { + updater.export(connection, schema, databaseUpdateFile); + } finally { + dbLock2.releaseLock(); + } + } + }); + } + } @Override public Connection getConnection() { @@ -323,4 +340,18 @@ public class DefaultJpaConnectionProviderFactory implements JpaConnectionProvide return operationalInfo; } + private MigrationStrategy getMigrationStrategy() { + String migrationStrategy = config.get("migrationStrategy"); + if (migrationStrategy == null) { + // Support 'databaseSchema' for backwards compatibility + migrationStrategy = config.get("databaseSchema"); + } + + if (migrationStrategy != null) { + return MigrationStrategy.valueOf(migrationStrategy.toUpperCase()); + } else { + return MigrationStrategy.UPDATE; + } + } + } diff --git a/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/JpaUpdaterProvider.java b/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/JpaUpdaterProvider.java index b087535625..37228e3e52 100755 --- a/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/JpaUpdaterProvider.java +++ b/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/JpaUpdaterProvider.java @@ -19,6 +19,7 @@ package org.keycloak.connections.jpa.updater; import org.keycloak.provider.Provider; +import java.io.File; import java.sql.Connection; /** @@ -26,10 +27,14 @@ import java.sql.Connection; */ public interface JpaUpdaterProvider extends Provider { - public String FIRST_VERSION = "1.0.0.Final"; + enum Status { + VALID, EMPTY, OUTDATED + } - public void update(Connection connection, String defaultSchema); + void update(Connection connection, String defaultSchema); - public void validate(Connection connection, String defaultSchema); + Status validate(Connection connection, String defaultSchema); + + void export(Connection connection, String defaultSchema, File file); } diff --git a/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/LiquibaseJpaUpdaterProvider.java b/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/LiquibaseJpaUpdaterProvider.java index 2610174ee5..b4a50a9c4d 100755 --- a/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/LiquibaseJpaUpdaterProvider.java +++ b/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/LiquibaseJpaUpdaterProvider.java @@ -30,6 +30,9 @@ import org.keycloak.connections.jpa.updater.liquibase.conn.LiquibaseConnectionPr import org.keycloak.connections.jpa.util.JpaUtils; import org.keycloak.models.KeycloakSession; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; import java.lang.reflect.Method; import java.sql.Connection; import java.util.List; @@ -53,6 +56,15 @@ public class LiquibaseJpaUpdaterProvider implements JpaUpdaterProvider { @Override public void update(Connection connection, String defaultSchema) { + update(connection, null, defaultSchema); + } + + @Override + public void export(Connection connection, String defaultSchema, File file) { + update(connection, file, defaultSchema); + } + + private void update(Connection connection, File file, String defaultSchema) { logger.debug("Starting database update"); // Need ThreadLocal as liquibase doesn't seem to have API to inject custom objects into tasks @@ -61,7 +73,7 @@ public class LiquibaseJpaUpdaterProvider implements JpaUpdaterProvider { try { // Run update with keycloak master changelog first Liquibase liquibase = getLiquibaseForKeycloakUpdate(connection, defaultSchema); - updateChangeSet(liquibase, liquibase.getChangeLogFile()); + updateChangeSet(liquibase, liquibase.getChangeLogFile(), file); // Run update for each custom JpaEntityProvider Set jpaProviders = session.getAllProviders(JpaEntityProvider.class); @@ -71,7 +83,7 @@ public class LiquibaseJpaUpdaterProvider implements JpaUpdaterProvider { String factoryId = jpaProvider.getFactoryId(); String changelogTableName = JpaUtils.getCustomChangelogTableName(factoryId); liquibase = getLiquibaseForCustomProviderUpdate(connection, defaultSchema, customChangelog, jpaProvider.getClass().getClassLoader(), changelogTableName); - updateChangeSet(liquibase, liquibase.getChangeLogFile()); + updateChangeSet(liquibase, liquibase.getChangeLogFile(), file); } } } catch (Exception e) { @@ -81,7 +93,8 @@ public class LiquibaseJpaUpdaterProvider implements JpaUpdaterProvider { } } - protected void updateChangeSet(Liquibase liquibase, String changelog) throws LiquibaseException { + + protected void updateChangeSet(Liquibase liquibase, String changelog, File exportFile) throws LiquibaseException, IOException { List changeSets = liquibase.listUnrunChangeSets((Contexts) null); if (!changeSets.isEmpty()) { List ranChangeSets = liquibase.getDatabase().getRanChangeSetList(); @@ -95,7 +108,12 @@ public class LiquibaseJpaUpdaterProvider implements JpaUpdaterProvider { } } - liquibase.update((Contexts) null); + if (exportFile != null) { + liquibase.update((Contexts) null, new FileWriter(exportFile)); + } else { + liquibase.update((Contexts) null); + } + logger.debugv("Completed database update for changelog {0}", changelog); } else { logger.debugv("Database is up to date for changelog {0}", changelog); @@ -107,13 +125,18 @@ public class LiquibaseJpaUpdaterProvider implements JpaUpdaterProvider { } @Override - public void validate(Connection connection, String defaultSchema) { + public Status validate(Connection connection, String defaultSchema) { logger.debug("Validating if database is updated"); + ThreadLocalSessionContext.setCurrentSession(session); try { // Validate with keycloak master changelog first Liquibase liquibase = getLiquibaseForKeycloakUpdate(connection, defaultSchema); - validateChangeSet(liquibase, liquibase.getChangeLogFile()); + + Status status = validateChangeSet(liquibase, liquibase.getChangeLogFile()); + if (status != Status.VALID) { + return status; + } // Validate each custom JpaEntityProvider Set jpaProviders = session.getAllProviders(JpaEntityProvider.class); @@ -123,24 +146,30 @@ public class LiquibaseJpaUpdaterProvider implements JpaUpdaterProvider { String factoryId = jpaProvider.getFactoryId(); String changelogTableName = JpaUtils.getCustomChangelogTableName(factoryId); liquibase = getLiquibaseForCustomProviderUpdate(connection, defaultSchema, customChangelog, jpaProvider.getClass().getClassLoader(), changelogTableName); - validateChangeSet(liquibase, liquibase.getChangeLogFile()); + if (validateChangeSet(liquibase, liquibase.getChangeLogFile()) != Status.VALID) { + return Status.OUTDATED; + } } } - } catch (LiquibaseException e) { throw new RuntimeException("Failed to validate database", e); } + + return Status.VALID; } - protected void validateChangeSet(Liquibase liquibase, String changelog) throws LiquibaseException { + protected Status validateChangeSet(Liquibase liquibase, String changelog) throws LiquibaseException { List changeSets = liquibase.listUnrunChangeSets((Contexts) null); if (!changeSets.isEmpty()) { - List ranChangeSets = liquibase.getDatabase().getRanChangeSetList(); - String errorMessage = String.format("Failed to validate database schema. Schema needs updating database from %s to %s. Please change databaseSchema to 'update' or use other database. Used changelog was %s", - ranChangeSets.get(ranChangeSets.size() - 1).getId(), changeSets.get(changeSets.size() - 1).getId(), changelog); - throw new RuntimeException(errorMessage); + if (changeSets.size() == liquibase.getDatabaseChangeLog().getChangeSets().size()) { + return Status.EMPTY; + } else { + logger.debugf("Validation failed. Database is not up-to-date for changelog %s", changelog); + return Status.OUTDATED; + } } else { logger.debugf("Validation passed. Database is up-to-date for changelog %s", changelog); + return Status.VALID; } } diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/ClientAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/ClientAdapter.java index 8e126e5698..e1a546e272 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/ClientAdapter.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/ClientAdapter.java @@ -498,6 +498,18 @@ public class ClientAdapter implements ClientModel, JpaModel { @Override public void updateClient() { em.flush(); + session.getKeycloakSessionFactory().publish(new RealmModel.ClientUpdatedEvent() { + + @Override + public ClientModel getUpdatedClient() { + return ClientAdapter.this; + } + + @Override + public KeycloakSession getKeycloakSession() { + return session; + } + }); } @Override diff --git a/model/mongo/src/main/java/org/keycloak/connections/mongo/DefaultMongoConnectionFactoryProvider.java b/model/mongo/src/main/java/org/keycloak/connections/mongo/DefaultMongoConnectionFactoryProvider.java index b899e0d450..ce09046862 100755 --- a/model/mongo/src/main/java/org/keycloak/connections/mongo/DefaultMongoConnectionFactoryProvider.java +++ b/model/mongo/src/main/java/org/keycloak/connections/mongo/DefaultMongoConnectionFactoryProvider.java @@ -51,6 +51,10 @@ import com.mongodb.ServerAddress; */ public class DefaultMongoConnectionFactoryProvider implements MongoConnectionProviderFactory, ServerInfoAwareProviderFactory { + enum MigrationStrategy { + UPDATE, VALIDATE + } + // TODO Make it dynamic private String[] entities = new String[]{ "org.keycloak.models.mongo.keycloak.entities.MongoRealmEntity", @@ -165,46 +169,34 @@ public class DefaultMongoConnectionFactoryProvider implements MongoConnectionPro } private void update(KeycloakSession session) { - String databaseSchema = config.get("databaseSchema"); + MigrationStrategy strategy = getMigrationStrategy(); - if (databaseSchema == null) { - throw new RuntimeException("Property 'databaseSchema' needs to be specified in the configuration of mongo connections"); + MongoUpdaterProvider mongoUpdater = session.getProvider(MongoUpdaterProvider.class); + if (mongoUpdater == null) { + throw new RuntimeException("Can't update database: Mongo updater provider not found"); + } + + DBLockProvider dbLock = new DBLockManager(session).getDBLock(); + if (dbLock.hasLock()) { + updateOrValidateDB(strategy, session, mongoUpdater); } else { - MongoUpdaterProvider mongoUpdater = session.getProvider(MongoUpdaterProvider.class); - if (mongoUpdater == null) { - throw new RuntimeException("Can't update database: Mongo updater provider not found"); - } + logger.trace("Don't have DBLock retrieved before upgrade. Needs to acquire lock first in separate transaction"); - DBLockProvider dbLock = new DBLockManager(session).getDBLock(); - if (dbLock.hasLock()) { - updateOrValidateDB(databaseSchema, session, mongoUpdater); - } else { - logger.trace("Don't have DBLock retrieved before upgrade. Needs to acquire lock first in separate transaction"); + KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), new KeycloakSessionTask() { - KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), new KeycloakSessionTask() { - - @Override - public void run(KeycloakSession lockSession) { - DBLockManager dbLockManager = new DBLockManager(lockSession); - DBLockProvider dbLock2 = dbLockManager.getDBLock(); - dbLock2.waitForLock(); - try { - updateOrValidateDB(databaseSchema, session, mongoUpdater); - } finally { - dbLock2.releaseLock(); - } + @Override + public void run(KeycloakSession lockSession) { + DBLockManager dbLockManager = new DBLockManager(lockSession); + DBLockProvider dbLock2 = dbLockManager.getDBLock(); + dbLock2.waitForLock(); + try { + updateOrValidateDB(strategy, session, mongoUpdater); + } finally { + dbLock2.releaseLock(); } + } - }); - } - - if (databaseSchema.equals("update")) { - mongoUpdater.update(session, db); - } else if (databaseSchema.equals("validate")) { - mongoUpdater.validate(session, db); - } else { - throw new RuntimeException("Invalid value for databaseSchema: " + databaseSchema); - } + }); } } @@ -217,13 +209,14 @@ public class DefaultMongoConnectionFactoryProvider implements MongoConnectionPro return entityClasses; } - protected void updateOrValidateDB(String databaseSchema, KeycloakSession session, MongoUpdaterProvider mongoUpdater) { - if (databaseSchema.equals("update")) { - mongoUpdater.update(session, db); - } else if (databaseSchema.equals("validate")) { - mongoUpdater.validate(session, db); - } else { - throw new RuntimeException("Invalid value for databaseSchema: " + databaseSchema); + protected void updateOrValidateDB(MigrationStrategy strategy, KeycloakSession session, MongoUpdaterProvider mongoUpdater) { + switch (strategy) { + case UPDATE: + mongoUpdater.update(session, db); + break; + case VALIDATE: + mongoUpdater.validate(session, db); + break; } } @@ -345,4 +338,18 @@ public class DefaultMongoConnectionFactoryProvider implements MongoConnectionPro return operationalInfo; } + private MigrationStrategy getMigrationStrategy() { + String migrationStrategy = config.get("migrationStrategy"); + if (migrationStrategy == null) { + // Support 'databaseSchema' for backwards compatibility + migrationStrategy = config.get("databaseSchema"); + } + + if (migrationStrategy != null) { + return MigrationStrategy.valueOf(migrationStrategy.toUpperCase()); + } else { + return MigrationStrategy.UPDATE; + } + } + } diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/ClientAdapter.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/ClientAdapter.java index f4983f98a3..5d6e0532d1 100755 --- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/ClientAdapter.java +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/ClientAdapter.java @@ -17,8 +17,6 @@ package org.keycloak.models.mongo.keycloak.adapters; -import com.mongodb.DBObject; -import com.mongodb.QueryBuilder; import org.keycloak.connections.mongo.api.context.MongoStoreInvocationContext; import org.keycloak.models.ClientModel; import org.keycloak.models.ClientTemplateModel; @@ -65,6 +63,19 @@ public class ClientAdapter extends AbstractMongoAdapter imple @Override public void updateClient() { updateMongoEntity(); + + session.getKeycloakSessionFactory().publish(new RealmModel.ClientUpdatedEvent() { + + @Override + public ClientModel getUpdatedClient() { + return ClientAdapter.this; + } + + @Override + public KeycloakSession getKeycloakSession() { + return session; + } + }); } diff --git a/server-spi/src/main/java/org/keycloak/ServerStartupError.java b/server-spi/src/main/java/org/keycloak/ServerStartupError.java new file mode 100644 index 0000000000..967e66f66a --- /dev/null +++ b/server-spi/src/main/java/org/keycloak/ServerStartupError.java @@ -0,0 +1,48 @@ +/* + * 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; + +/** + * Non-recoverable error thrown during server startup + * + * @author Stian Thorgersen + */ +public class ServerStartupError extends Error { + + private final boolean fillStackTrace; + + public ServerStartupError(String message) { + super(message); + fillStackTrace = true; + } + + public ServerStartupError(String message, boolean fillStackTrace) { + super(message); + this.fillStackTrace = fillStackTrace; + } + + @Override + public synchronized Throwable fillInStackTrace() { + if (fillStackTrace) { + return super.fillInStackTrace(); + } else { + return this; + } + } + +} diff --git a/server-spi/src/main/java/org/keycloak/broker/provider/AbstractIdentityProviderFactory.java b/server-spi/src/main/java/org/keycloak/broker/provider/AbstractIdentityProviderFactory.java index e8be6d6192..953797b51c 100755 --- a/server-spi/src/main/java/org/keycloak/broker/provider/AbstractIdentityProviderFactory.java +++ b/server-spi/src/main/java/org/keycloak/broker/provider/AbstractIdentityProviderFactory.java @@ -50,7 +50,7 @@ public abstract class AbstractIdentityProviderFactory parseConfig(InputStream inputStream) { + public Map parseConfig(KeycloakSession session, InputStream inputStream) { return new HashMap(); } } diff --git a/server-spi/src/main/java/org/keycloak/broker/provider/IdentityProviderFactory.java b/server-spi/src/main/java/org/keycloak/broker/provider/IdentityProviderFactory.java index fba8e6431f..93ef720fad 100755 --- a/server-spi/src/main/java/org/keycloak/broker/provider/IdentityProviderFactory.java +++ b/server-spi/src/main/java/org/keycloak/broker/provider/IdentityProviderFactory.java @@ -17,6 +17,7 @@ package org.keycloak.broker.provider; import org.keycloak.models.IdentityProviderModel; +import org.keycloak.models.KeycloakSession; import org.keycloak.provider.ProviderFactory; import java.io.InputStream; @@ -47,8 +48,9 @@ public interface IdentityProviderFactory extends Pro *

Creates an {@link IdentityProvider} based on the configuration from * inputStream.

* + * @param session * @param inputStream The input stream from where configuration will be loaded from.. * @return */ - Map parseConfig(InputStream inputStream); + Map parseConfig(KeycloakSession session, InputStream inputStream); } \ No newline at end of file diff --git a/server-spi/src/main/java/org/keycloak/events/Details.java b/server-spi/src/main/java/org/keycloak/events/Details.java index e5da713b69..772eaa794f 100755 --- a/server-spi/src/main/java/org/keycloak/events/Details.java +++ b/server-spi/src/main/java/org/keycloak/events/Details.java @@ -58,4 +58,7 @@ public interface Details { String CLIENT_AUTH_METHOD = "client_auth_method"; + String SIGNATURE_REQUIRED = "signature_required"; + String SIGNATURE_ALGORITHM = "signature_algorithm"; + } diff --git a/server-spi/src/main/java/org/keycloak/migration/migrators/MigrationUtils.java b/server-spi/src/main/java/org/keycloak/migration/migrators/MigrationUtils.java index 08da081b4e..2afbbcc1e8 100644 --- a/server-spi/src/main/java/org/keycloak/migration/migrators/MigrationUtils.java +++ b/server-spi/src/main/java/org/keycloak/migration/migrators/MigrationUtils.java @@ -49,8 +49,8 @@ public class MigrationUtils { public static void updateOTPRequiredAction(RequiredActionProviderModel otpAction) { if (otpAction == null) return; - if (!otpAction.getProviderId().equals(UserModel.RequiredAction.CONFIGURE_TOTP.name())) return; - if (!otpAction.getName().equals("Configure Totp")) return; + if (!UserModel.RequiredAction.CONFIGURE_TOTP.name().equals(otpAction.getProviderId())) return; + if (!"Configure Totp".equals(otpAction.getName())) return; otpAction.setName("Configure OTP"); } 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 04f1476716..61dc9c10f8 100755 --- a/server-spi/src/main/java/org/keycloak/models/RealmModel.java +++ b/server-spi/src/main/java/org/keycloak/models/RealmModel.java @@ -56,6 +56,12 @@ public interface RealmModel extends RoleContainerModel { ClientModel getCreatedClient(); } + // Called also during client creation after client is fully initialized (including all attributes etc) + interface ClientUpdatedEvent extends ProviderEvent { + ClientModel getUpdatedClient(); + KeycloakSession getKeycloakSession(); + } + interface ClientRemovedEvent extends ProviderEvent { ClientModel getClient(); KeycloakSession getKeycloakSession(); diff --git a/server-spi/src/main/java/org/keycloak/models/utils/RepresentationToModel.java b/server-spi/src/main/java/org/keycloak/models/utils/RepresentationToModel.java index f36f010410..d1d5b059d7 100755 --- a/server-spi/src/main/java/org/keycloak/models/utils/RepresentationToModel.java +++ b/server-spi/src/main/java/org/keycloak/models/utils/RepresentationToModel.java @@ -943,7 +943,6 @@ public class RepresentationToModel { } else { client.setNodeReRegistrationTimeout(-1); } - client.updateClient(); if (resourceRep.getNotBefore() != null) { client.setNotBefore(resourceRep.getNotBefore()); @@ -1043,6 +1042,8 @@ public class RepresentationToModel { if (resourceRep.isUseTemplateMappers() != null) client.setUseTemplateMappers(resourceRep.isUseTemplateMappers()); else client.setUseTemplateMappers(resourceRep.getClientTemplate() != null); + client.updateClient(); + return client; } @@ -1066,7 +1067,6 @@ public class RepresentationToModel { if (rep.isSurrogateAuthRequired() != null) resource.setSurrogateAuthRequired(rep.isSurrogateAuthRequired()); if (rep.getNodeReRegistrationTimeout() != null) resource.setNodeReRegistrationTimeout(rep.getNodeReRegistrationTimeout()); if (rep.getClientAuthenticatorType() != null) resource.setClientAuthenticatorType(rep.getClientAuthenticatorType()); - resource.updateClient(); if (rep.getProtocol() != null) resource.setProtocol(rep.getProtocol()); if (rep.getAttributes() != null) { @@ -1122,7 +1122,7 @@ public class RepresentationToModel { } } - + resource.updateClient(); } // CLIENT TEMPLATES diff --git a/services/pom.xml b/services/pom.xml index a8a009dfec..57d79846db 100755 --- a/services/pom.xml +++ b/services/pom.xml @@ -72,6 +72,11 @@ twitter4j-core
+ + org.wildfly.core + wildfly-controller + provided + org.jboss.logging jboss-logging diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/client/JWTClientAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/client/JWTClientAuthenticator.java index 5139e3a61d..86dd4e8c74 100644 --- a/services/src/main/java/org/keycloak/authentication/authenticators/client/JWTClientAuthenticator.java +++ b/services/src/main/java/org/keycloak/authentication/authenticators/client/JWTClientAuthenticator.java @@ -30,6 +30,7 @@ import java.util.Set; import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriBuilder; import org.keycloak.OAuth2Constants; import org.keycloak.authentication.AuthenticationFlowError; @@ -39,9 +40,11 @@ import org.keycloak.jose.jws.JWSInput; import org.keycloak.jose.jws.crypto.RSAProvider; import org.keycloak.models.AuthenticationExecutionModel; import org.keycloak.models.ClientModel; +import org.keycloak.models.ModelException; import org.keycloak.models.RealmModel; import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.protocol.oidc.OIDCLoginProtocol; +import org.keycloak.protocol.oidc.OIDCLoginProtocolService; import org.keycloak.provider.ProviderConfigProperty; import org.keycloak.representations.JsonWebToken; import org.keycloak.representations.idm.CertificateRepresentation; @@ -139,10 +142,11 @@ public class JWTClientAuthenticator extends AbstractClientAuthenticator { throw new RuntimeException("Signature on JWT token failed validation"); } - // Validate other things - String expectedAudience = Urls.realmIssuer(context.getUriInfo().getBaseUri(), realm.getName()); - if (!token.hasAudience(expectedAudience)) { - throw new RuntimeException("Token audience doesn't match domain. Realm audience is '" + expectedAudience + "' but audience from token is '" + Arrays.asList(token.getAudience()).toString() + "'"); + // Allow both "issuer" or "token-endpoint" as audience + String issuerUrl = Urls.realmIssuer(context.getUriInfo().getBaseUri(), realm.getName()); + String tokenUrl = OIDCLoginProtocolService.tokenUrl(context.getUriInfo().getBaseUriBuilder()).build(realm.getName()).toString(); + if (!token.hasAudience(issuerUrl) && !token.hasAudience(tokenUrl)) { + throw new RuntimeException("Token audience doesn't match domain. Realm issuer is '" + issuerUrl + "' but audience from token is '" + Arrays.asList(token.getAudience()).toString() + "'"); } if (!token.isActive()) { @@ -163,30 +167,13 @@ public class JWTClientAuthenticator extends AbstractClientAuthenticator { } protected PublicKey getSignatureValidationKey(ClientModel client, ClientAuthenticationFlowContext context) { - CertificateRepresentation certInfo = CertificateInfoHelper.getCertificateFromClient(client, ATTR_PREFIX); - - String encodedCertificate = certInfo.getCertificate(); - String encodedPublicKey = certInfo.getPublicKey(); - - if (encodedCertificate == null && encodedPublicKey == null) { - Response challengeResponse = ClientAuthUtil.errorResponse(Response.Status.BAD_REQUEST.getStatusCode(), "unauthorized_client", "Client '" + client.getClientId() + "' doesn't have certificate or publicKey configured"); + try { + return CertificateInfoHelper.getSignatureValidationKey(client, ATTR_PREFIX); + } catch (ModelException me) { + Response challengeResponse = ClientAuthUtil.errorResponse(Response.Status.BAD_REQUEST.getStatusCode(), "unauthorized_client", me.getMessage()); context.failure(AuthenticationFlowError.CLIENT_CREDENTIALS_SETUP_REQUIRED, challengeResponse); return null; } - - if (encodedCertificate != null && encodedPublicKey != null) { - Response challengeResponse = ClientAuthUtil.errorResponse(Response.Status.BAD_REQUEST.getStatusCode(), "unauthorized_client", "Client '" + client.getClientId() + "' has both publicKey and certificate configured"); - context.failure(AuthenticationFlowError.CLIENT_CREDENTIALS_SETUP_REQUIRED, challengeResponse); - return null; - } - - // TODO: Caching of publicKeys / certificates, so it doesn't need to be always computed from pem. For performance reasons... - if (encodedCertificate != null) { - X509Certificate clientCert = KeycloakModelUtils.getCertificate(encodedCertificate); - return clientCert.getPublicKey(); - } else { - return KeycloakModelUtils.getPublicKey(encodedPublicKey); - } } @Override diff --git a/services/src/main/java/org/keycloak/broker/oidc/KeycloakOIDCIdentityProviderFactory.java b/services/src/main/java/org/keycloak/broker/oidc/KeycloakOIDCIdentityProviderFactory.java index d3e8eba5aa..56f24a2c58 100755 --- a/services/src/main/java/org/keycloak/broker/oidc/KeycloakOIDCIdentityProviderFactory.java +++ b/services/src/main/java/org/keycloak/broker/oidc/KeycloakOIDCIdentityProviderFactory.java @@ -18,6 +18,7 @@ package org.keycloak.broker.oidc; import org.keycloak.broker.provider.AbstractIdentityProviderFactory; import org.keycloak.models.IdentityProviderModel; +import org.keycloak.models.KeycloakSession; import java.io.InputStream; import java.util.Map; @@ -45,8 +46,8 @@ public class KeycloakOIDCIdentityProviderFactory extends AbstractIdentityProvide } @Override - public Map parseConfig(InputStream inputStream) { - return OIDCIdentityProviderFactory.parseOIDCConfig(inputStream); + public Map parseConfig(KeycloakSession session, InputStream inputStream) { + return OIDCIdentityProviderFactory.parseOIDCConfig(session, inputStream); } diff --git a/services/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProviderFactory.java b/services/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProviderFactory.java index 82c2cdd966..a0e5017504 100755 --- a/services/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProviderFactory.java +++ b/services/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProviderFactory.java @@ -19,6 +19,7 @@ package org.keycloak.broker.oidc; import org.keycloak.broker.provider.AbstractIdentityProviderFactory; import org.keycloak.jose.jwk.JWK; import org.keycloak.models.IdentityProviderModel; +import org.keycloak.models.KeycloakSession; import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.jose.jwk.JSONWebKeySet; import org.keycloak.protocol.oidc.representations.OIDCConfigurationRepresentation; @@ -56,11 +57,11 @@ public class OIDCIdentityProviderFactory extends AbstractIdentityProviderFactory } @Override - public Map parseConfig(InputStream inputStream) { - return parseOIDCConfig(inputStream); + public Map parseConfig(KeycloakSession session, InputStream inputStream) { + return parseOIDCConfig(session, inputStream); } - protected static Map parseOIDCConfig(InputStream inputStream) { + protected static Map parseOIDCConfig(KeycloakSession session, InputStream inputStream) { OIDCConfigurationRepresentation rep; try { rep = JsonSerialization.readValue(inputStream, OIDCConfigurationRepresentation.class); @@ -74,14 +75,14 @@ public class OIDCIdentityProviderFactory extends AbstractIdentityProviderFactory config.setTokenUrl(rep.getTokenEndpoint()); config.setUserInfoUrl(rep.getUserinfoEndpoint()); if (rep.getJwksUri() != null) { - sendJwksRequest(rep, config); + sendJwksRequest(session, rep, config); } return config.getConfig(); } - protected static void sendJwksRequest(OIDCConfigurationRepresentation rep, OIDCIdentityProviderConfig config) { + protected static void sendJwksRequest(KeycloakSession session, OIDCConfigurationRepresentation rep, OIDCIdentityProviderConfig config) { try { - JSONWebKeySet keySet = JWKSUtils.sendJwksRequest(rep.getJwksUri()); + JSONWebKeySet keySet = JWKSUtils.sendJwksRequest(session, rep.getJwksUri()); PublicKey key = JWKSUtils.getKeyForUse(keySet, JWK.Use.SIG); if (key == null) { logger.supportedJwkNotFound(JWK.Use.SIG.asString()); diff --git a/services/src/main/java/org/keycloak/broker/saml/SAMLIdentityProviderFactory.java b/services/src/main/java/org/keycloak/broker/saml/SAMLIdentityProviderFactory.java index dac6750719..261799524e 100755 --- a/services/src/main/java/org/keycloak/broker/saml/SAMLIdentityProviderFactory.java +++ b/services/src/main/java/org/keycloak/broker/saml/SAMLIdentityProviderFactory.java @@ -24,6 +24,7 @@ import org.keycloak.dom.saml.v2.metadata.IDPSSODescriptorType; import org.keycloak.dom.saml.v2.metadata.KeyDescriptorType; import org.keycloak.dom.saml.v2.metadata.KeyTypes; import org.keycloak.models.IdentityProviderModel; +import org.keycloak.models.KeycloakSession; import org.keycloak.saml.common.constants.JBossSAMLURIConstants; import org.keycloak.saml.common.exceptions.ParsingException; import org.keycloak.saml.common.util.DocumentUtil; @@ -54,7 +55,7 @@ public class SAMLIdentityProviderFactory extends AbstractIdentityProviderFactory } @Override - public Map parseConfig(InputStream inputStream) { + public Map parseConfig(KeycloakSession session, InputStream inputStream) { try { Object parsedObject = new SAMLParser().parse(inputStream); EntityDescriptorType entityType; diff --git a/services/src/main/java/org/keycloak/protocol/oidc/OIDCAdvancedConfigWrapper.java b/services/src/main/java/org/keycloak/protocol/oidc/OIDCAdvancedConfigWrapper.java new file mode 100644 index 0000000000..3233690493 --- /dev/null +++ b/services/src/main/java/org/keycloak/protocol/oidc/OIDCAdvancedConfigWrapper.java @@ -0,0 +1,106 @@ +/* + * 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.protocol.oidc; + +import java.util.HashMap; + +import org.keycloak.jose.jws.Algorithm; +import org.keycloak.models.ClientModel; +import org.keycloak.representations.idm.ClientRepresentation; + +/** + * @author Marek Posolda + */ +public class OIDCAdvancedConfigWrapper { + + private static final String USER_INFO_RESPONSE_SIGNATURE_ALG = "user.info.response.signature.alg"; + + private static final String REQUEST_OBJECT_SIGNATURE_ALG = "request.object.signature.alg"; + + private final ClientModel clientModel; + private final ClientRepresentation clientRep; + + private OIDCAdvancedConfigWrapper(ClientModel client, ClientRepresentation clientRep) { + this.clientModel = client; + this.clientRep = clientRep; + } + + + public static OIDCAdvancedConfigWrapper fromClientModel(ClientModel client) { + return new OIDCAdvancedConfigWrapper(client, null); + } + + public static OIDCAdvancedConfigWrapper fromClientRepresentation(ClientRepresentation clientRep) { + return new OIDCAdvancedConfigWrapper(null, clientRep); + } + + + public Algorithm getUserInfoSignedResponseAlg() { + String alg = getAttribute(USER_INFO_RESPONSE_SIGNATURE_ALG); + return alg==null ? null : Enum.valueOf(Algorithm.class, alg); + } + + public void setUserInfoSignedResponseAlg(Algorithm alg) { + String algStr = alg==null ? null : alg.toString(); + setAttribute(USER_INFO_RESPONSE_SIGNATURE_ALG, algStr); + } + + public boolean isUserInfoSignatureRequired() { + return getUserInfoSignedResponseAlg() != null; + } + + public Algorithm getRequestObjectSignatureAlg() { + String alg = getAttribute(REQUEST_OBJECT_SIGNATURE_ALG); + return alg==null ? null : Enum.valueOf(Algorithm.class, alg); + } + + public void setRequestObjectSignatureAlg(Algorithm alg) { + String algStr = alg==null ? null : alg.toString(); + setAttribute(REQUEST_OBJECT_SIGNATURE_ALG, algStr); + } + + + private String getAttribute(String attrKey) { + if (clientModel != null) { + return clientModel.getAttribute(attrKey); + } else { + return clientRep.getAttributes()==null ? null : clientRep.getAttributes().get(attrKey); + } + } + + private void setAttribute(String attrKey, String attrValue) { + if (clientModel != null) { + if (attrValue != null) { + clientModel.setAttribute(attrKey, attrValue); + } else { + clientModel.removeAttribute(attrKey); + } + } else { + if (attrValue != null) { + if (clientRep.getAttributes() == null) { + clientRep.setAttributes(new HashMap<>()); + } + clientRep.getAttributes().put(attrKey, attrValue); + } else { + if (clientRep.getAttributes() != null) { + clientRep.getAttributes().put(attrKey, null); + } + } + } + } +} diff --git a/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocol.java b/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocol.java index 3c4c3aa650..c263405213 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocol.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocol.java @@ -66,6 +66,7 @@ public class OIDCLoginProtocol implements LoginProtocol { public static final String REQUEST_PARAM = "request"; public static final String REQUEST_URI_PARAM = "request_uri"; public static final String UI_LOCALES_PARAM = OAuth2Constants.UI_LOCALES_PARAM; + public static final String CLAIMS_PARAM = "claims"; public static final String LOGOUT_REDIRECT_URI = "OIDC_LOGOUT_REDIRECT_URI"; public static final String ISSUER = "iss"; diff --git a/services/src/main/java/org/keycloak/protocol/oidc/OIDCWellKnownProvider.java b/services/src/main/java/org/keycloak/protocol/oidc/OIDCWellKnownProvider.java index eb5b5b411e..800630c2c2 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/OIDCWellKnownProvider.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/OIDCWellKnownProvider.java @@ -48,6 +48,10 @@ public class OIDCWellKnownProvider implements WellKnownProvider { public static final List DEFAULT_ID_TOKEN_SIGNING_ALG_VALUES_SUPPORTED = list(Algorithm.RS256.toString()); + public static final List DEFAULT_USER_INFO_SIGNING_ALG_VALUES_SUPPORTED = list(Algorithm.RS256.toString()); + + public static final List DEFAULT_REQUEST_OBJECT_SIGNING_ALG_VALUES_SUPPORTED = list(Algorithm.none.toString(), Algorithm.RS256.toString()); + public static final List DEFAULT_GRANT_TYPES_SUPPORTED = list(OAuth2Constants.AUTHORIZATION_CODE, OAuth2Constants.IMPLICIT, OAuth2Constants.REFRESH_TOKEN, OAuth2Constants.PASSWORD, OAuth2Constants.CLIENT_CREDENTIALS); public static final List DEFAULT_RESPONSE_TYPES_SUPPORTED = list(OAuth2Constants.CODE, OIDCResponseType.NONE, OIDCResponseType.ID_TOKEN, OIDCResponseType.TOKEN, "id_token token", "code id_token", "code token", "code id_token token"); @@ -90,6 +94,8 @@ public class OIDCWellKnownProvider implements WellKnownProvider { config.setRegistrationEndpoint(RealmsResource.clientRegistrationUrl(uriInfo).path(ClientRegistrationService.class, "provider").build(realm.getName(), OIDCClientRegistrationProviderFactory.ID).toString()); config.setIdTokenSigningAlgValuesSupported(DEFAULT_ID_TOKEN_SIGNING_ALG_VALUES_SUPPORTED); + config.setUserInfoSigningAlgValuesSupported(DEFAULT_USER_INFO_SIGNING_ALG_VALUES_SUPPORTED); + config.setRequestObjectSigningAlgValuesSupported(DEFAULT_REQUEST_OBJECT_SIGNING_ALG_VALUES_SUPPORTED); config.setResponseTypesSupported(DEFAULT_RESPONSE_TYPES_SUPPORTED); config.setSubjectTypesSupported(DEFAULT_SUBJECT_TYPES_SUPPORTED); config.setResponseModesSupported(DEFAULT_RESPONSE_MODES_SUPPORTED); @@ -104,8 +110,8 @@ public class OIDCWellKnownProvider implements WellKnownProvider { config.setScopesSupported(SCOPES_SUPPORTED); - config.setRequestParameterSupported(false); - config.setRequestUriParameterSupported(false); + config.setRequestParameterSupported(true); + config.setRequestUriParameterSupported(true); return config; } diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java index 931b04cfba..1b4db2951a 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java @@ -17,11 +17,6 @@ package org.keycloak.protocol.oidc.endpoints; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; - import javax.ws.rs.GET; import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.Response; @@ -42,6 +37,8 @@ import org.keycloak.models.IdentityProviderModel; import org.keycloak.models.RealmModel; import org.keycloak.protocol.AuthorizationEndpointBase; import org.keycloak.protocol.oidc.OIDCLoginProtocol; +import org.keycloak.protocol.oidc.endpoints.request.AuthorizationEndpointRequest; +import org.keycloak.protocol.oidc.endpoints.request.AuthorizationEndpointRequestParserProcessor; import org.keycloak.protocol.oidc.utils.OIDCRedirectUriBuilder; import org.keycloak.protocol.oidc.utils.OIDCResponseMode; import org.keycloak.protocol.oidc.utils.OIDCResponseType; @@ -67,43 +64,10 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase { /** * Prefix used to store additional HTTP GET params from original client request into {@link ClientSessionModel} note to be available later in Authenticators, RequiredActions etc. Prefix is used to * prevent collisions with internally used notes. - * + * * @see ClientSessionModel#getNote(String) - * @see #KNOWN_REQ_PARAMS - * @see #additionalReqParams */ public static final String CLIENT_SESSION_NOTE_ADDITIONAL_REQ_PARAMS_PREFIX = "client_request_param_"; - /** - * Max number of additional req params copied into client session note to prevent DoS attacks - * - * @see #additionalReqParams - */ - public static final int ADDITIONAL_REQ_PARAMS_MAX_MUMBER = 5; - /** - * Max size of additional req param value copied into client session note to prevent DoS attacks - params with longer value are ignored - * - * @see #additionalReqParams - */ - public static final int ADDITIONAL_REQ_PARAMS_MAX_SIZE = 200; - - /** Set of known protocol GET params not to be stored into {@link #additionalReqParams} */ - private static final Set KNOWN_REQ_PARAMS = new HashSet<>(); - static { - KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.CLIENT_ID_PARAM); - KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.RESPONSE_TYPE_PARAM); - KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.RESPONSE_MODE_PARAM); - KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.REDIRECT_URI_PARAM); - KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.STATE_PARAM); - KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.SCOPE_PARAM); - KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.LOGIN_HINT_PARAM); - KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.PROMPT_PARAM); - KNOWN_REQ_PARAMS.add(AdapterConstants.KC_IDP_HINT); - KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.NONCE_PARAM); - KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.MAX_AGE_PARAM); - KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.UI_LOCALES_PARAM); - KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.REQUEST_PARAM); - KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.REQUEST_URI_PARAM); - } private enum Action { REGISTER, CODE, FORGOT_CREDENTIALS @@ -116,19 +80,8 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase { private OIDCResponseType parsedResponseType; private OIDCResponseMode parsedResponseMode; - private String clientId; + private AuthorizationEndpointRequest request; private String redirectUri; - private String redirectUriParam; - private String responseType; - private String responseMode; - private String state; - private String scope; - private String loginHint; - private String prompt; - private String nonce; - private String maxAge; - private String idpHint; - protected Map additionalReqParams = new HashMap<>(); public AuthorizationEndpoint(RealmModel realm, EventBuilder event) { super(realm, event); @@ -139,34 +92,25 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase { public Response build() { MultivaluedMap params = uriInfo.getQueryParameters(); - clientId = params.getFirst(OIDCLoginProtocol.CLIENT_ID_PARAM); - responseType = params.getFirst(OIDCLoginProtocol.RESPONSE_TYPE_PARAM); - responseMode = params.getFirst(OIDCLoginProtocol.RESPONSE_MODE_PARAM); - redirectUriParam = params.getFirst(OIDCLoginProtocol.REDIRECT_URI_PARAM); - state = params.getFirst(OIDCLoginProtocol.STATE_PARAM); - scope = params.getFirst(OIDCLoginProtocol.SCOPE_PARAM); - loginHint = params.getFirst(OIDCLoginProtocol.LOGIN_HINT_PARAM); - prompt = params.getFirst(OIDCLoginProtocol.PROMPT_PARAM); - idpHint = params.getFirst(AdapterConstants.KC_IDP_HINT); - nonce = params.getFirst(OIDCLoginProtocol.NONCE_PARAM); - maxAge = params.getFirst(OIDCLoginProtocol.MAX_AGE_PARAM); - - extractAdditionalReqParams(params); + String clientId = params.getFirst(OIDCLoginProtocol.CLIENT_ID_PARAM); checkSsl(); checkRealm(); - checkClient(); + checkClient(clientId); + + request = AuthorizationEndpointRequestParserProcessor.parseRequest(event, session, client, params); + checkRedirectUri(); Response errorResponse = checkResponseType(); if (errorResponse != null) { return errorResponse; } - if (!TokenUtil.isOIDCRequest(scope)) { + if (!TokenUtil.isOIDCRequest(request.getScope())) { logger.oidcScopeMissing(); } - errorResponse = checkOIDCParams(params); + errorResponse = checkOIDCParams(); if (errorResponse != null) { return errorResponse; } @@ -186,27 +130,6 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase { throw new RuntimeException("Unknown action " + action); } - protected void extractAdditionalReqParams(MultivaluedMap params) { - for (String paramName : params.keySet()) { - if (!KNOWN_REQ_PARAMS.contains(paramName)) { - String value = params.getFirst(paramName); - if (value != null && value.trim().isEmpty()) { - value = null; - } - if (value != null && value.length() <= ADDITIONAL_REQ_PARAMS_MAX_SIZE) { - if (additionalReqParams.size() >= ADDITIONAL_REQ_PARAMS_MAX_MUMBER) { - logger.debug("Maximal number of additional OIDC params (" + ADDITIONAL_REQ_PARAMS_MAX_MUMBER + ") exceeded, ignoring rest of them!"); - break; - } - additionalReqParams.put(paramName, value); - } else { - logger.debug("OIDC Additional param " + paramName + " ignored because value is empty or longer than " + ADDITIONAL_REQ_PARAMS_MAX_SIZE); - } - } - - } - } - public AuthorizationEndpoint register() { event.event(EventType.REGISTER); action = Action.REGISTER; @@ -243,7 +166,7 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase { } } - private void checkClient() { + private void checkClient(String clientId) { if (clientId == null) { event.error(Errors.INVALID_REQUEST); throw new ErrorPageException(session, Messages.MISSING_PARAMETER, OIDCLoginProtocol.CLIENT_ID_PARAM); @@ -271,6 +194,8 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase { } private Response checkResponseType() { + String responseType = request.getResponseType(); + if (responseType == null) { logger.missingParameter(OAuth2Constants.RESPONSE_TYPE); event.error(Errors.INVALID_REQUEST); @@ -292,7 +217,7 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase { OIDCResponseMode parsedResponseMode = null; try { - parsedResponseMode = OIDCResponseMode.parse(responseMode, parsedResponseType); + parsedResponseMode = OIDCResponseMode.parse(request.getResponseMode(), parsedResponseType); } catch (IllegalArgumentException iae) { logger.invalidParameter(OIDCLoginProtocol.RESPONSE_MODE_PARAM); event.error(Errors.INVALID_REQUEST); @@ -325,20 +250,8 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase { return null; } - private Response checkOIDCParams(MultivaluedMap params) { - if (params.getFirst(OIDCLoginProtocol.REQUEST_PARAM) != null) { - logger.unsupportedParameter(OIDCLoginProtocol.REQUEST_PARAM); - event.error(Errors.INVALID_REQUEST); - return redirectErrorToClient(parsedResponseMode, OAuthErrorException.REQUEST_NOT_SUPPORTED, null); - } - - if (params.getFirst(OIDCLoginProtocol.REQUEST_URI_PARAM) != null) { - logger.unsupportedParameter(OIDCLoginProtocol.REQUEST_URI_PARAM); - event.error(Errors.INVALID_REQUEST); - return redirectErrorToClient(parsedResponseMode, OAuthErrorException.REQUEST_URI_NOT_SUPPORTED, null); - } - - if (parsedResponseType.isImplicitOrHybridFlow() && nonce == null) { + private Response checkOIDCParams() { + if (parsedResponseType.isImplicitOrHybridFlow() && request.getNonce() == null) { logger.missingParameter(OIDCLoginProtocol.NONCE_PARAM); event.error(Errors.INVALID_REQUEST); return redirectErrorToClient(parsedResponseMode, OAuthErrorException.INVALID_REQUEST, "Missing parameter: nonce"); @@ -355,14 +268,16 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase { errorResponseBuilder.addParam(OAuth2Constants.ERROR_DESCRIPTION, errorDescription); } - if (state != null) { - errorResponseBuilder.addParam(OAuth2Constants.STATE, state); + if (request.getState() != null) { + errorResponseBuilder.addParam(OAuth2Constants.STATE, request.getState()); } return errorResponseBuilder.build(); } private void checkRedirectUri() { + String redirectUriParam = request.getRedirectUriParam(); + event.detail(Details.REDIRECT_URI, redirectUriParam); redirectUri = RedirectUtils.verifyRedirectUri(uriInfo, redirectUriParam, realm, client); @@ -377,27 +292,28 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase { clientSession.setAuthMethod(OIDCLoginProtocol.LOGIN_PROTOCOL); clientSession.setRedirectUri(redirectUri); clientSession.setAction(ClientSessionModel.Action.AUTHENTICATE.name()); - clientSession.setNote(OIDCLoginProtocol.RESPONSE_TYPE_PARAM, responseType); - clientSession.setNote(OIDCLoginProtocol.REDIRECT_URI_PARAM, redirectUriParam); + clientSession.setNote(OIDCLoginProtocol.RESPONSE_TYPE_PARAM, request.getResponseType()); + clientSession.setNote(OIDCLoginProtocol.REDIRECT_URI_PARAM, request.getRedirectUriParam()); clientSession.setNote(OIDCLoginProtocol.ISSUER, Urls.realmIssuer(uriInfo.getBaseUri(), realm.getName())); - if (state != null) clientSession.setNote(OIDCLoginProtocol.STATE_PARAM, state); - if (nonce != null) clientSession.setNote(OIDCLoginProtocol.NONCE_PARAM, nonce); - if (maxAge != null) clientSession.setNote(OIDCLoginProtocol.MAX_AGE_PARAM, maxAge); - if (scope != null) clientSession.setNote(OIDCLoginProtocol.SCOPE_PARAM, scope); - if (loginHint != null) clientSession.setNote(OIDCLoginProtocol.LOGIN_HINT_PARAM, loginHint); - if (prompt != null) clientSession.setNote(OIDCLoginProtocol.PROMPT_PARAM, prompt); - if (idpHint != null) clientSession.setNote(AdapterConstants.KC_IDP_HINT, idpHint); - if (responseMode != null) clientSession.setNote(OIDCLoginProtocol.RESPONSE_MODE_PARAM, responseMode); + if (request.getState() != null) clientSession.setNote(OIDCLoginProtocol.STATE_PARAM, request.getState()); + if (request.getNonce() != null) clientSession.setNote(OIDCLoginProtocol.NONCE_PARAM, request.getNonce()); + if (request.getMaxAge() != null) clientSession.setNote(OIDCLoginProtocol.MAX_AGE_PARAM, String.valueOf(request.getMaxAge())); + if (request.getScope() != null) clientSession.setNote(OIDCLoginProtocol.SCOPE_PARAM, request.getScope()); + if (request.getLoginHint() != null) clientSession.setNote(OIDCLoginProtocol.LOGIN_HINT_PARAM, request.getLoginHint()); + if (request.getPrompt() != null) clientSession.setNote(OIDCLoginProtocol.PROMPT_PARAM, request.getPrompt()); + if (request.getIdpHint() != null) clientSession.setNote(AdapterConstants.KC_IDP_HINT, request.getIdpHint()); + if (request.getResponseMode() != null) clientSession.setNote(OIDCLoginProtocol.RESPONSE_MODE_PARAM, request.getResponseMode()); - if (additionalReqParams != null) { - for (String paramName : additionalReqParams.keySet()) { - clientSession.setNote(CLIENT_SESSION_NOTE_ADDITIONAL_REQ_PARAMS_PREFIX + paramName, additionalReqParams.get(paramName)); + if (request.getAdditionalReqParams() != null) { + for (String paramName : request.getAdditionalReqParams().keySet()) { + clientSession.setNote(CLIENT_SESSION_NOTE_ADDITIONAL_REQ_PARAMS_PREFIX + paramName, request.getAdditionalReqParams().get(paramName)); } } } private Response buildAuthorizationCodeAuthorizationResponse() { + String idpHint = request.getIdpHint(); if (idpHint != null && !"".equals(idpHint)) { IdentityProviderModel identityProviderModel = realm.getIdentityProviderByAlias(idpHint); @@ -413,7 +329,7 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase { this.event.event(EventType.LOGIN); clientSession.setNote(Details.AUTH_TYPE, CODE_AUTH_TYPE); - return handleBrowserAuthenticationRequest(clientSession, new OIDCLoginProtocol(session, realm, uriInfo, headers, event), TokenUtil.hasPrompt(prompt, OIDCLoginProtocol.PROMPT_VALUE_NONE), false); + return handleBrowserAuthenticationRequest(clientSession, new OIDCLoginProtocol(session, realm, uriInfo, headers, event), TokenUtil.hasPrompt(request.getPrompt(), OIDCLoginProtocol.PROMPT_VALUE_NONE), false); } private Response buildRegister() { 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 ad23283d14..f5e4caab17 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 @@ -19,6 +19,7 @@ package org.keycloak.protocol.oidc.endpoints; import org.jboss.resteasy.annotations.cache.NoCache; import org.jboss.resteasy.spi.HttpRequest; import org.jboss.resteasy.spi.HttpResponse; +import org.keycloak.OAuth2Constants; import org.keycloak.common.ClientConnection; import org.keycloak.OAuthErrorException; import org.keycloak.RSATokenVerifier; @@ -27,12 +28,15 @@ import org.keycloak.events.Details; import org.keycloak.events.Errors; import org.keycloak.events.EventBuilder; import org.keycloak.events.EventType; +import org.keycloak.jose.jws.Algorithm; +import org.keycloak.jose.jws.JWSBuilder; import org.keycloak.models.ClientModel; import org.keycloak.models.ClientSessionModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.models.UserSessionModel; +import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper; import org.keycloak.protocol.oidc.TokenManager; import org.keycloak.representations.AccessToken; import org.keycloak.services.ErrorResponseException; @@ -40,17 +44,18 @@ import org.keycloak.services.managers.AppAuthManager; import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.services.resources.Cors; import org.keycloak.services.Urls; +import org.keycloak.utils.MediaType; import javax.ws.rs.GET; import javax.ws.rs.OPTIONS; import javax.ws.rs.POST; import javax.ws.rs.Path; -import javax.ws.rs.Produces; import javax.ws.rs.core.Context; import javax.ws.rs.core.HttpHeaders; -import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import javax.ws.rs.core.UriInfo; + +import java.security.PrivateKey; import java.util.HashMap; import java.util.Map; @@ -86,7 +91,6 @@ public class UserInfoEndpoint { @Path("/") @OPTIONS - @Produces(MediaType.APPLICATION_JSON) public Response issueUserInfoPreflight() { return Cors.add(this.request, Response.ok()).auth().preflight().build(); } @@ -94,7 +98,6 @@ public class UserInfoEndpoint { @Path("/") @GET @NoCache - @Produces(MediaType.APPLICATION_JSON) public Response issueUserInfoGet(@Context final HttpHeaders headers) { String accessToken = this.appAuthManager.extractAuthorizationHeaderToken(headers); return issueUserInfo(accessToken); @@ -103,7 +106,6 @@ public class UserInfoEndpoint { @Path("/") @POST @NoCache - @Produces(MediaType.APPLICATION_JSON) public Response issueUserInfoPost() { // Try header first HttpHeaders headers = request.getHttpHeaders(); @@ -176,12 +178,39 @@ public class UserInfoEndpoint { AccessToken userInfo = new AccessToken(); tokenManager.transformUserInfoAccessToken(session, userInfo, realm, clientModel, userModel, userSession, clientSession); - event.success(); - Map claims = new HashMap(); claims.putAll(userInfo.getOtherClaims()); claims.put("sub", userModel.getId()); - return Cors.add(request, Response.ok(claims)).auth().allowedOrigins(token).build(); + + Response.ResponseBuilder responseBuilder; + OIDCAdvancedConfigWrapper cfg = OIDCAdvancedConfigWrapper.fromClientModel(clientModel); + + if (cfg.isUserInfoSignatureRequired()) { + String issuerUrl = Urls.realmIssuer(uriInfo.getBaseUri(), realm.getName()); + String audience = clientModel.getClientId(); + claims.put("iss", issuerUrl); + claims.put("aud", audience); + + Algorithm signatureAlg = cfg.getUserInfoSignedResponseAlg(); + PrivateKey privateKey = realm.getPrivateKey(); + + String signedUserInfo = new JWSBuilder() + .jsonContent(claims) + .sign(signatureAlg, privateKey); + + responseBuilder = Response.ok(signedUserInfo).header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JWT); + + event.detail(Details.SIGNATURE_REQUIRED, "true"); + event.detail(Details.SIGNATURE_ALGORITHM, cfg.getUserInfoSignedResponseAlg().toString()); + } else { + responseBuilder = Response.ok(claims).header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON); + + event.detail(Details.SIGNATURE_REQUIRED, "false"); + } + + event.success(); + + return Cors.add(request, responseBuilder).auth().allowedOrigins(token).build(); } } diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthorizationEndpointRequest.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthorizationEndpointRequest.java new file mode 100644 index 0000000000..998a58ca45 --- /dev/null +++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthorizationEndpointRequest.java @@ -0,0 +1,88 @@ +/* + * 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.protocol.oidc.endpoints.request; + +import java.util.HashMap; +import java.util.Map; + +/** + * @author Marek Posolda + */ +public class AuthorizationEndpointRequest { + + String clientId; + String redirectUriParam; + String responseType; + String responseMode; + String state; + String scope; + String loginHint; + String prompt; + String nonce; + Integer maxAge; + String idpHint; + Map additionalReqParams = new HashMap<>(); + + public String getClientId() { + return clientId; + } + + public String getRedirectUriParam() { + return redirectUriParam; + } + + public String getResponseType() { + return responseType; + } + + public String getResponseMode() { + return responseMode; + } + + public String getState() { + return state; + } + + public String getScope() { + return scope; + } + + public String getLoginHint() { + return loginHint; + } + + public String getPrompt() { + return prompt; + } + + public String getNonce() { + return nonce; + } + + public Integer getMaxAge() { + return maxAge; + } + + public String getIdpHint() { + return idpHint; + } + + public Map getAdditionalReqParams() { + return additionalReqParams; + } +} diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthorizationEndpointRequestParserProcessor.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthorizationEndpointRequestParserProcessor.java new file mode 100644 index 0000000000..8db219c08f --- /dev/null +++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthorizationEndpointRequestParserProcessor.java @@ -0,0 +1,73 @@ +/* + * 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.protocol.oidc.endpoints.request; + +import java.io.InputStream; +import java.util.Map; + +import javax.ws.rs.core.MultivaluedMap; + +import org.keycloak.common.util.StreamUtil; +import org.keycloak.connections.httpclient.HttpClientProvider; +import org.keycloak.events.Errors; +import org.keycloak.events.EventBuilder; +import org.keycloak.models.ClientModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.protocol.oidc.OIDCLoginProtocol; +import org.keycloak.services.ErrorPageException; +import org.keycloak.services.ServicesLogger; +import org.keycloak.services.messages.Messages; + +/** + * @author Marek Posolda + */ +public class AuthorizationEndpointRequestParserProcessor { + + private static final ServicesLogger logger = ServicesLogger.ROOT_LOGGER; + + public static AuthorizationEndpointRequest parseRequest(EventBuilder event, KeycloakSession session, ClientModel client, MultivaluedMap requestParams) { + try { + AuthorizationEndpointRequest request = new AuthorizationEndpointRequest(); + + new AuthzEndpointQueryStringParser(requestParams).parseRequest(request); + + String requestParam = requestParams.getFirst(OIDCLoginProtocol.REQUEST_PARAM); + String requestUriParam = requestParams.getFirst(OIDCLoginProtocol.REQUEST_URI_PARAM); + + if (requestParam != null && requestUriParam != null) { + throw new RuntimeException("Illegal to use both 'request' and 'request_uri' parameters together"); + } + + if (requestParam != null) { + new AuthzEndpointRequestObjectParser(requestParam, client).parseRequest(request); + } else if (requestUriParam != null) { + InputStream is = session.getProvider(HttpClientProvider.class).get(requestUriParam); + String retrievedRequest = StreamUtil.readString(is); + + new AuthzEndpointRequestObjectParser(retrievedRequest, client).parseRequest(request); + } + + return request; + + } catch (Exception e) { + logger.invalidRequest(e); + event.error(Errors.INVALID_REQUEST); + throw new ErrorPageException(session, Messages.INVALID_REQUEST); + } + } +} diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthzEndpointQueryStringParser.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthzEndpointQueryStringParser.java new file mode 100644 index 0000000000..8384fdcdcb --- /dev/null +++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthzEndpointQueryStringParser.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.protocol.oidc.endpoints.request; + +import java.util.Set; + +import javax.ws.rs.core.MultivaluedMap; + +/** + * Parse the parameters from request queryString + * + * @author Marek Posolda + */ +class AuthzEndpointQueryStringParser extends AuthzEndpointRequestParser { + + private final MultivaluedMap requestParams; + + public AuthzEndpointQueryStringParser(MultivaluedMap requestParams) { + this.requestParams = requestParams; + } + + @Override + protected String getParameter(String paramName) { + return requestParams.getFirst(paramName); + } + + @Override + protected Integer getIntParameter(String paramName) { + String paramVal = requestParams.getFirst(paramName); + return paramVal==null ? null : Integer.parseInt(paramVal); + } + + @Override + protected Set keySet() { + return requestParams.keySet(); + } +} diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthzEndpointRequestObjectParser.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthzEndpointRequestObjectParser.java new file mode 100644 index 0000000000..658a2c002e --- /dev/null +++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthzEndpointRequestObjectParser.java @@ -0,0 +1,88 @@ +/* + * 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.protocol.oidc.endpoints.request; + +import java.security.PublicKey; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import org.keycloak.authentication.authenticators.client.JWTClientAuthenticator; +import org.keycloak.jose.jws.Algorithm; +import org.keycloak.jose.jws.JWSHeader; +import org.keycloak.jose.jws.JWSInput; +import org.keycloak.jose.jws.crypto.RSAProvider; +import org.keycloak.models.ClientModel; +import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper; +import org.keycloak.services.util.CertificateInfoHelper; +import org.keycloak.util.JsonSerialization; + +/** + * Parse the parameters from OIDC "request" object + * + * @author Marek Posolda + */ +class AuthzEndpointRequestObjectParser extends AuthzEndpointRequestParser { + + private final Map requestParams; + + public AuthzEndpointRequestObjectParser(String requestObject, ClientModel client) throws Exception { + JWSInput input = new JWSInput(requestObject); + JWSHeader header = input.getHeader(); + + Algorithm requestedSignatureAlgorithm = OIDCAdvancedConfigWrapper.fromClientModel(client).getRequestObjectSignatureAlg(); + + if (requestedSignatureAlgorithm != null && requestedSignatureAlgorithm != header.getAlgorithm()) { + throw new RuntimeException("Request object signed with different algorithm than client requested algorithm"); + } + + if (header.getAlgorithm() == Algorithm.none) { + this.requestParams = JsonSerialization.readValue(input.getContent(), TypedHashMap.class); + } else if (header.getAlgorithm() == Algorithm.RS256) { + PublicKey clientPublicKey = CertificateInfoHelper.getSignatureValidationKey(client, JWTClientAuthenticator.ATTR_PREFIX); + boolean verified = RSAProvider.verify(input, clientPublicKey); + if (!verified) { + throw new RuntimeException("Failed to verify signature on 'request' object"); + } + + this.requestParams = JsonSerialization.readValue(input.getContent(), TypedHashMap.class); + } else { + throw new RuntimeException("Unsupported JWA algorithm used for signed request"); + } + } + + @Override + protected String getParameter(String paramName) { + Object val = this.requestParams.get(paramName); + return val==null ? null : val.toString(); + } + + @Override + protected Integer getIntParameter(String paramName) { + Object val = this.requestParams.get(paramName); + return val==null ? null : Integer.parseInt(getParameter(paramName)); + } + + @Override + protected Set keySet() { + return requestParams.keySet(); + } + + static class TypedHashMap extends HashMap { + } +} diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthzEndpointRequestParser.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthzEndpointRequestParser.java new file mode 100644 index 0000000000..e322d4b75e --- /dev/null +++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthzEndpointRequestParser.java @@ -0,0 +1,122 @@ +/* + * 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.protocol.oidc.endpoints.request; + +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import org.keycloak.constants.AdapterConstants; +import org.keycloak.protocol.oidc.OIDCLoginProtocol; +import org.keycloak.services.ServicesLogger; + +/** + * @author Marek Posolda + */ +abstract class AuthzEndpointRequestParser { + + private static final ServicesLogger logger = ServicesLogger.ROOT_LOGGER; + + /** + * Max number of additional req params copied into client session note to prevent DoS attacks + * + */ + public static final int ADDITIONAL_REQ_PARAMS_MAX_MUMBER = 5; + + /** + * Max size of additional req param value copied into client session note to prevent DoS attacks - params with longer value are ignored + * + */ + public static final int ADDITIONAL_REQ_PARAMS_MAX_SIZE = 200; + + /** Set of known protocol GET params not to be stored into additionalReqParams} */ + private static final Set KNOWN_REQ_PARAMS = new HashSet<>(); + static { + KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.CLIENT_ID_PARAM); + KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.RESPONSE_TYPE_PARAM); + KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.RESPONSE_MODE_PARAM); + KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.REDIRECT_URI_PARAM); + KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.STATE_PARAM); + KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.SCOPE_PARAM); + KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.LOGIN_HINT_PARAM); + KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.PROMPT_PARAM); + KNOWN_REQ_PARAMS.add(AdapterConstants.KC_IDP_HINT); + KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.NONCE_PARAM); + KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.MAX_AGE_PARAM); + KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.UI_LOCALES_PARAM); + KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.REQUEST_PARAM); + KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.REQUEST_URI_PARAM); + } + + + public void parseRequest(AuthorizationEndpointRequest request) { + String clientId = getParameter(OIDCLoginProtocol.CLIENT_ID_PARAM); + + if (request.clientId != null && !request.clientId.equals(clientId)) { + throw new IllegalArgumentException("The client_id parameter doesn't match the one from OIDC 'request' or 'request_uri'"); + } + + request.clientId = clientId; + request.responseType = replaceIfNotNull(request.responseType, getParameter(OIDCLoginProtocol.RESPONSE_TYPE_PARAM)); + request.responseMode = replaceIfNotNull(request.responseMode, getParameter(OIDCLoginProtocol.RESPONSE_MODE_PARAM)); + request.redirectUriParam = replaceIfNotNull(request.redirectUriParam, getParameter(OIDCLoginProtocol.REDIRECT_URI_PARAM)); + request.state = replaceIfNotNull(request.state, getParameter(OIDCLoginProtocol.STATE_PARAM)); + request.scope = replaceIfNotNull(request.scope, getParameter(OIDCLoginProtocol.SCOPE_PARAM)); + request.loginHint = replaceIfNotNull(request.loginHint, getParameter(OIDCLoginProtocol.LOGIN_HINT_PARAM)); + request.prompt = replaceIfNotNull(request.prompt, getParameter(OIDCLoginProtocol.PROMPT_PARAM)); + request.idpHint = replaceIfNotNull(request.idpHint, getParameter(AdapterConstants.KC_IDP_HINT)); + request.nonce = replaceIfNotNull(request.nonce, getParameter(OIDCLoginProtocol.NONCE_PARAM)); + request.maxAge = replaceIfNotNull(request.maxAge, getIntParameter(OIDCLoginProtocol.MAX_AGE_PARAM)); + + extractAdditionalReqParams(request.additionalReqParams); + } + + + protected void extractAdditionalReqParams(Map additionalReqParams) { + for (String paramName : keySet()) { + if (!KNOWN_REQ_PARAMS.contains(paramName)) { + String value = getParameter(paramName); + if (value != null && value.trim().isEmpty()) { + value = null; + } + if (value != null && value.length() <= ADDITIONAL_REQ_PARAMS_MAX_SIZE) { + if (additionalReqParams.size() >= ADDITIONAL_REQ_PARAMS_MAX_MUMBER) { + logger.debug("Maximal number of additional OIDC params (" + ADDITIONAL_REQ_PARAMS_MAX_MUMBER + ") exceeded, ignoring rest of them!"); + break; + } + additionalReqParams.put(paramName, value); + } else { + logger.debug("OIDC Additional param " + paramName + " ignored because value is empty or longer than " + ADDITIONAL_REQ_PARAMS_MAX_SIZE); + } + } + + } + } + + protected T replaceIfNotNull(T previousVal, T newVal) { + return newVal==null ? previousVal : newVal; + } + + + protected abstract String getParameter(String paramName); + + protected abstract Integer getIntParameter(String paramName); + + protected abstract Set keySet(); + +} diff --git a/services/src/main/java/org/keycloak/protocol/oidc/representations/OIDCConfigurationRepresentation.java b/services/src/main/java/org/keycloak/protocol/oidc/representations/OIDCConfigurationRepresentation.java index 0421e16edd..181e0d28df 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/representations/OIDCConfigurationRepresentation.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/representations/OIDCConfigurationRepresentation.java @@ -64,6 +64,12 @@ public class OIDCConfigurationRepresentation { @JsonProperty("id_token_signing_alg_values_supported") private List idTokenSigningAlgValuesSupported; + @JsonProperty("userinfo_signing_alg_values_supported") + private List userInfoSigningAlgValuesSupported; + + @JsonProperty("request_object_signing_alg_values_supported") + private List requestObjectSigningAlgValuesSupported; + @JsonProperty("response_modes_supported") private List responseModesSupported; @@ -184,6 +190,22 @@ public class OIDCConfigurationRepresentation { this.idTokenSigningAlgValuesSupported = idTokenSigningAlgValuesSupported; } + public List getUserInfoSigningAlgValuesSupported() { + return userInfoSigningAlgValuesSupported; + } + + public void setUserInfoSigningAlgValuesSupported(List userInfoSigningAlgValuesSupported) { + this.userInfoSigningAlgValuesSupported = userInfoSigningAlgValuesSupported; + } + + public List getRequestObjectSigningAlgValuesSupported() { + return requestObjectSigningAlgValuesSupported; + } + + public void setRequestObjectSigningAlgValuesSupported(List requestObjectSigningAlgValuesSupported) { + this.requestObjectSigningAlgValuesSupported = requestObjectSigningAlgValuesSupported; + } + public List getResponseModesSupported() { return responseModesSupported; } diff --git a/services/src/main/java/org/keycloak/protocol/oidc/utils/JWKSUtils.java b/services/src/main/java/org/keycloak/protocol/oidc/utils/JWKSUtils.java index c856a81cef..d8f7fe72de 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/utils/JWKSUtils.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/utils/JWKSUtils.java @@ -18,12 +18,15 @@ package org.keycloak.protocol.oidc.utils; import java.io.IOException; +import java.io.InputStream; import java.security.PublicKey; -import org.keycloak.broker.provider.util.SimpleHttp; +import org.keycloak.common.util.StreamUtil; +import org.keycloak.connections.httpclient.HttpClientProvider; import org.keycloak.jose.jwk.JSONWebKeySet; import org.keycloak.jose.jwk.JWK; import org.keycloak.jose.jwk.JWKParser; +import org.keycloak.models.KeycloakSession; import org.keycloak.util.JsonSerialization; /** @@ -31,8 +34,9 @@ import org.keycloak.util.JsonSerialization; */ public class JWKSUtils { - public static JSONWebKeySet sendJwksRequest(String jwksURI) throws IOException { - String keySetString = SimpleHttp.doGet(jwksURI).asString(); + public static JSONWebKeySet sendJwksRequest(KeycloakSession session, String jwksURI) throws IOException { + InputStream is = session.getProvider(HttpClientProvider.class).get(jwksURI); + String keySetString = StreamUtil.readString(is); return JsonSerialization.readValue(keySetString, JSONWebKeySet.class); } diff --git a/services/src/main/java/org/keycloak/protocol/oidc/utils/RedirectUtils.java b/services/src/main/java/org/keycloak/protocol/oidc/utils/RedirectUtils.java index 441cd57531..b7d9ad4246 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/utils/RedirectUtils.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/utils/RedirectUtils.java @@ -77,7 +77,7 @@ public class RedirectUtils { } else { redirectUri = lowerCaseHostname(redirectUri); - String r = redirectUri.indexOf('?') != -1 ? redirectUri.substring(0, redirectUri.indexOf('?')) : redirectUri; + String r = redirectUri; Set resolveValidRedirects = resolveValidRedirects(uriInfo, rootUrl, validRedirects); boolean valid = matchesRedirects(resolveValidRedirects, r); @@ -134,15 +134,17 @@ public class RedirectUtils { private static boolean matchesRedirects(Set validRedirects, String redirect) { for (String validRedirect : validRedirects) { - if (validRedirect.endsWith("*")) { + if (validRedirect.endsWith("*") && !validRedirect.contains("?")) { + // strip off the query component - we don't check them when wildcards are effective + String r = redirect.contains("?") ? redirect.substring(0, redirect.indexOf("?")) : redirect; // strip off * int length = validRedirect.length() - 1; validRedirect = validRedirect.substring(0, length); - if (redirect.startsWith(validRedirect)) return true; + if (r.startsWith(validRedirect)) return true; // strip off trailing '/' if (length - 1 > 0 && validRedirect.charAt(length - 1) == '/') length--; validRedirect = validRedirect.substring(0, length); - if (validRedirect.equals(redirect)) return true; + if (validRedirect.equals(r)) return true; } else if (validRedirect.equals(redirect)) return true; } return false; diff --git a/services/src/main/java/org/keycloak/services/ServicesLogger.java b/services/src/main/java/org/keycloak/services/ServicesLogger.java index de7eef743f..af6c4c13e5 100644 --- a/services/src/main/java/org/keycloak/services/ServicesLogger.java +++ b/services/src/main/java/org/keycloak/services/ServicesLogger.java @@ -430,4 +430,8 @@ public interface ServicesLogger extends BasicLogger { @Message(id=96, value="Not found JWK of supported keyType under jwks_uri for usage: %s") void supportedJwkNotFound(String usage); + @LogMessage(level = WARN) + @Message(id=97, value="Invalid request") + void invalidRequest(@Cause Throwable t); + } diff --git a/services/src/main/java/org/keycloak/services/clientregistration/AbstractClientRegistrationProvider.java b/services/src/main/java/org/keycloak/services/clientregistration/AbstractClientRegistrationProvider.java index 88b5a340c1..d634b006b7 100755 --- a/services/src/main/java/org/keycloak/services/clientregistration/AbstractClientRegistrationProvider.java +++ b/services/src/main/java/org/keycloak/services/clientregistration/AbstractClientRegistrationProvider.java @@ -29,8 +29,12 @@ import org.keycloak.models.utils.RepresentationToModel; import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.services.ErrorResponseException; import org.keycloak.services.ForbiddenException; +import org.keycloak.services.resources.admin.AdminRoot; +import org.keycloak.services.validation.ClientValidator; +import org.keycloak.services.validation.ValidationMessages; import javax.ws.rs.core.Response; +import java.util.Properties; /** * @author Stian Thorgersen @@ -50,6 +54,16 @@ public abstract class AbstractClientRegistrationProvider implements ClientRegist auth.requireCreate(); + ValidationMessages validationMessages = new ValidationMessages(); + if (!ClientValidator.validate(client, validationMessages)) { + String errorCode = validationMessages.fieldHasError("redirectUris") ? ErrorCodes.INVALID_REDIRECT_URI : ErrorCodes.INVALID_CLIENT_METADATA; + throw new ErrorResponseException( + errorCode, + validationMessages.getStringMessages(), + Response.Status.BAD_REQUEST + ); + } + try { ClientModel clientModel = RepresentationToModel.createClient(session, session.getContext().getRealm(), client, true); @@ -104,6 +118,16 @@ public abstract class AbstractClientRegistrationProvider implements ClientRegist throw new ErrorResponseException(ErrorCodes.INVALID_CLIENT_METADATA, "Client Identifier modified", Response.Status.BAD_REQUEST); } + ValidationMessages validationMessages = new ValidationMessages(); + if (!ClientValidator.validate(rep, validationMessages)) { + String errorCode = validationMessages.fieldHasError("redirectUris") ? ErrorCodes.INVALID_REDIRECT_URI : ErrorCodes.INVALID_CLIENT_METADATA; + throw new ErrorResponseException( + errorCode, + validationMessages.getStringMessages(), + Response.Status.BAD_REQUEST + ); + } + RepresentationToModel.updateClient(rep, client); rep = ModelToRepresentation.toRepresentation(client); diff --git a/services/src/main/java/org/keycloak/services/clientregistration/oidc/DescriptionConverter.java b/services/src/main/java/org/keycloak/services/clientregistration/oidc/DescriptionConverter.java index 3ddcc59a74..e1939ef0c8 100644 --- a/services/src/main/java/org/keycloak/services/clientregistration/oidc/DescriptionConverter.java +++ b/services/src/main/java/org/keycloak/services/clientregistration/oidc/DescriptionConverter.java @@ -24,8 +24,10 @@ import org.keycloak.authentication.authenticators.client.ClientIdAndSecretAuthen import org.keycloak.authentication.authenticators.client.JWTClientAuthenticator; import org.keycloak.jose.jwk.JSONWebKeySet; import org.keycloak.jose.jwk.JWK; +import org.keycloak.jose.jws.Algorithm; import org.keycloak.models.KeycloakSession; import org.keycloak.models.utils.KeycloakModelUtils; +import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper; import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.protocol.oidc.utils.AuthorizeClientUtil; import org.keycloak.protocol.oidc.utils.JWKSUtils; @@ -87,14 +89,12 @@ public class DescriptionConverter { } client.setClientAuthenticatorType(clientAuthFactory.getId()); - // Externalize to ClientAuthenticator itself? - if (authMethod != null && authMethod.equals(OIDCLoginProtocol.PRIVATE_KEY_JWT)) { - - PublicKey publicKey = retrievePublicKey(clientOIDC); - if (publicKey == null) { - throw new ClientRegistrationException("Didn't find key of supported keyType for use " + JWK.Use.SIG.asString()); - } + PublicKey publicKey = retrievePublicKey(session, clientOIDC); + if (authMethod != null && authMethod.equals(OIDCLoginProtocol.PRIVATE_KEY_JWT) && publicKey == null) { + throw new ClientRegistrationException("Didn't find key of supported keyType for use " + JWK.Use.SIG.asString()); + } + if (publicKey != null) { String publicKeyPem = KeycloakModelUtils.getPemFromKey(publicKey); CertificateRepresentation rep = new CertificateRepresentation(); @@ -102,13 +102,24 @@ public class DescriptionConverter { CertificateInfoHelper.updateClientRepresentationCertificateInfo(client, rep, JWTClientAuthenticator.ATTR_PREFIX); } + OIDCAdvancedConfigWrapper configWrapper = OIDCAdvancedConfigWrapper.fromClientRepresentation(client); + if (clientOIDC.getUserinfoSignedResponseAlg() != null) { + Algorithm algorithm = Enum.valueOf(Algorithm.class, clientOIDC.getUserinfoSignedResponseAlg()); + configWrapper.setUserInfoSignedResponseAlg(algorithm); + } + + if (clientOIDC.getRequestObjectSigningAlg() != null) { + Algorithm algorithm = Enum.valueOf(Algorithm.class, clientOIDC.getRequestObjectSigningAlg()); + configWrapper.setRequestObjectSignatureAlg(algorithm); + } + return client; } - private static PublicKey retrievePublicKey(OIDCClientRepresentation clientOIDC) { + private static PublicKey retrievePublicKey(KeycloakSession session, OIDCClientRepresentation clientOIDC) { if (clientOIDC.getJwksUri() == null && clientOIDC.getJwks() == null) { - throw new ClientRegistrationException("Requested client authentication method '%s' but jwks_uri nor jwks were available in config"); + return null; } if (clientOIDC.getJwksUri() != null && clientOIDC.getJwks() != null) { @@ -120,7 +131,7 @@ public class DescriptionConverter { keySet = clientOIDC.getJwks(); } else { try { - keySet = JWKSUtils.sendJwksRequest(clientOIDC.getJwksUri()); + keySet = JWKSUtils.sendJwksRequest(session, clientOIDC.getJwksUri()); } catch (IOException ioe) { throw new ClientRegistrationException("Failed to send JWKS request to specified jwks_uri", ioe); } @@ -152,6 +163,15 @@ public class DescriptionConverter { response.setRegistrationClientUri(uri.toString()); response.setResponseTypes(getOIDCResponseTypes(client)); response.setGrantTypes(getOIDCGrantTypes(client)); + + OIDCAdvancedConfigWrapper config = OIDCAdvancedConfigWrapper.fromClientRepresentation(client); + if (config.isUserInfoSignatureRequired()) { + response.setUserinfoSignedResponseAlg(config.getUserInfoSignedResponseAlg().toString()); + } + if (config.getRequestObjectSignatureAlg() != null) { + response.setRequestObjectSigningAlg(config.getRequestObjectSignatureAlg().toString()); + } + return response; } diff --git a/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java b/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java index 5e1cd9f876..6cdf52eab2 100644 --- a/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java +++ b/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java @@ -56,15 +56,22 @@ import java.io.*; import java.net.URI; import java.net.URL; import java.util.*; +import org.jboss.dmr.ModelNode; /** * @author Bill Burke * @version $Revision: 1 $ */ public class KeycloakApplication extends Application { + // This param name is defined again in Keycloak Server Subsystem class + // org.keycloak.subsystem.server.extension.KeycloakServerDeploymentProcessor. We have this value in + // two places to avoid dependency between Keycloak Subsystem and Keycloak Services module. + public static final String KEYCLOAK_CONFIG_PARAM_NAME = "org.keycloak.server-subsystem.Config"; private static final ServicesLogger logger = ServicesLogger.ROOT_LOGGER; + protected boolean embedded = false; + protected Set singletons = new HashSet(); protected Set> classes = new HashSet>(); @@ -72,69 +79,79 @@ public class KeycloakApplication extends Application { protected String contextPath; public KeycloakApplication(@Context ServletContext context, @Context Dispatcher dispatcher) { - loadConfig(); - - this.contextPath = context.getContextPath(); - this.sessionFactory = createSessionFactory(); - - dispatcher.getDefaultContextObjects().put(KeycloakApplication.class, this); - ResteasyProviderFactory.pushContext(KeycloakApplication.class, this); // for injection - context.setAttribute(KeycloakSessionFactory.class.getName(), this.sessionFactory); - - singletons.add(new ServerVersionResource()); - singletons.add(new RobotsResource()); - singletons.add(new RealmsResource()); - singletons.add(new AdminRoot()); - classes.add(ThemeResource.class); - classes.add(JsResource.class); - - classes.add(KeycloakTransactionCommitter.class); - - singletons.add(new ObjectMapperResolver(Boolean.parseBoolean(System.getProperty("keycloak.jsonPrettyPrint", "false")))); - - ExportImportManager[] exportImportManager = new ExportImportManager[1]; - - KeycloakModelUtils.runJobInTransaction(sessionFactory, new KeycloakSessionTask() { - - @Override - public void run(KeycloakSession lockSession) { - DBLockManager dbLockManager = new DBLockManager(lockSession); - dbLockManager.checkForcedUnlock(); - DBLockProvider dbLock = dbLockManager.getDBLock(); - dbLock.waitForLock(); - try { - exportImportManager[0] = migrateAndBootstrap(); - } finally { - dbLock.releaseLock(); - } + try { + if ("true".equals(context.getInitParameter("keycloak.embedded"))) { + embedded = true; } - }); + loadConfig(context); + + this.contextPath = context.getContextPath(); + this.sessionFactory = createSessionFactory(); + + dispatcher.getDefaultContextObjects().put(KeycloakApplication.class, this); + ResteasyProviderFactory.pushContext(KeycloakApplication.class, this); // for injection + context.setAttribute(KeycloakSessionFactory.class.getName(), this.sessionFactory); + + singletons.add(new ServerVersionResource()); + singletons.add(new RobotsResource()); + singletons.add(new RealmsResource()); + singletons.add(new AdminRoot()); + classes.add(ThemeResource.class); + classes.add(JsResource.class); + + classes.add(KeycloakTransactionCommitter.class); + + singletons.add(new ObjectMapperResolver(Boolean.parseBoolean(System.getProperty("keycloak.jsonPrettyPrint", "false")))); + + ExportImportManager[] exportImportManager = new ExportImportManager[1]; + + KeycloakModelUtils.runJobInTransaction(sessionFactory, new KeycloakSessionTask() { + + @Override + public void run(KeycloakSession lockSession) { + DBLockManager dbLockManager = new DBLockManager(lockSession); + dbLockManager.checkForcedUnlock(); + DBLockProvider dbLock = dbLockManager.getDBLock(); + dbLock.waitForLock(); + try { + exportImportManager[0] = migrateAndBootstrap(); + } finally { + dbLock.releaseLock(); + } + } + + }); - if (exportImportManager[0].isRunExport()) { - exportImportManager[0].runExport(); + if (exportImportManager[0].isRunExport()) { + exportImportManager[0].runExport(); + } + + boolean bootstrapAdminUser = false; + KeycloakSession session = sessionFactory.create(); + try { + session.getTransactionManager().begin(); + bootstrapAdminUser = new ApplianceBootstrap(session).isNoMasterUser(); + + session.getTransactionManager().commit(); + } finally { + session.close(); + } + + sessionFactory.publish(new PostMigrationEvent()); + + singletons.add(new WelcomeResource(bootstrapAdminUser)); + + setupScheduledTasks(sessionFactory); + } catch (Throwable t) { + if (!embedded) { + exit(1); + } + throw t; } - - boolean bootstrapAdminUser = false; - KeycloakSession session = sessionFactory.create(); - try { - session.getTransactionManager().begin(); - bootstrapAdminUser = new ApplianceBootstrap(session).isNoMasterUser(); - - session.getTransactionManager().commit(); - } finally { - session.close(); - } - - sessionFactory.publish(new PostMigrationEvent()); - - singletons.add(new WelcomeResource(bootstrapAdminUser)); - - setupScheduledTasks(sessionFactory); } - // Migrate model, bootstrap master realm, import realms and create admin user. This is done with acquired dbLock protected ExportImportManager migrateAndBootstrap() { ExportImportManager exportImportManager; @@ -185,7 +202,6 @@ public class KeycloakApplication extends Application { session.getTransactionManager().commit(); } catch (Exception e) { session.getTransactionManager().rollback(); - logger.migrationFailure(e); throw e; } finally { session.close(); @@ -206,12 +222,18 @@ public class KeycloakApplication extends Application { return uriInfo.getBaseUriBuilder().replacePath(getContextPath()).build(); } - public static void loadConfig() { + public static void loadConfig(ServletContext context) { try { JsonNode node = null; + + String dmrConfig = loadDmrConfig(context); + if (dmrConfig != null) { + node = new ObjectMapper().readTree(dmrConfig); + logger.loadingFrom("standalone.xml or domain.xml"); + } String configDir = System.getProperty("jboss.server.config.dir"); - if (configDir != null) { + if (node == null && configDir != null) { File f = new File(configDir + File.separator + "keycloak-server.json"); if (f.isFile()) { logger.loadingFrom(f.getAbsolutePath()); @@ -230,14 +252,24 @@ public class KeycloakApplication extends Application { if (node != null) { Properties properties = new SystemEnvProperties(); Config.init(new JsonConfigProvider(node, properties)); - return; } else { - throw new RuntimeException("Config 'keycloak-server.json' not found"); + throw new RuntimeException("Keycloak config not found."); } } catch (IOException e) { throw new RuntimeException("Failed to load config", e); } } + + private static String loadDmrConfig(ServletContext context) { + String dmrConfig = context.getInitParameter(KEYCLOAK_CONFIG_PARAM_NAME); + if (dmrConfig == null) return null; + + ModelNode dmrConfigNode = ModelNode.fromString(dmrConfig); + if (dmrConfigNode.asPropertyList().isEmpty()) return null; + + // note that we need to resolve expressions BEFORE we convert to JSON + return dmrConfigNode.resolve().toJSONString(true); + } public static KeycloakSessionFactory createSessionFactory() { DefaultKeycloakSessionFactory factory = new DefaultKeycloakSessionFactory(); @@ -386,4 +418,13 @@ public class KeycloakApplication extends Application { } } + private void exit(int status) { + new Thread() { + @Override + public void run() { + System.exit(status); + } + }.start(); + } + } 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 f29ed8cf49..152d632941 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 @@ -66,6 +66,10 @@ import java.util.Map; */ public class ClientAttributeCertificateResource { + public static final String CERTIFICATE_PEM = "Certificate PEM"; + public static final String PUBLIC_KEY_PEM = "Public Key PEM"; + public static final String JSON_WEB_KEY_SET = "JSON Web Key Set"; + protected RealmModel realm; private RealmAuth auth; protected ClientModel client; @@ -195,12 +199,23 @@ public class ClientAttributeCertificateResource { Map> uploadForm = input.getFormDataMap(); String keystoreFormat = uploadForm.get("keystoreFormat").get(0).getBodyAsString(); List inputParts = uploadForm.get("file"); - if (keystoreFormat.equals("Certificate PEM")) { + if (keystoreFormat.equals(CERTIFICATE_PEM)) { String pem = StreamUtil.readString(inputParts.get(0).getBody(InputStream.class, null)); + + // Validate format + KeycloakModelUtils.getCertificate(pem); + info.setCertificate(pem); return info; + } else if (keystoreFormat.equals(PUBLIC_KEY_PEM)) { + String pem = StreamUtil.readString(inputParts.get(0).getBody(InputStream.class, null)); - } else if (keystoreFormat.equals("JSON Web Key Set (JWK)")) { + // Validate format + KeycloakModelUtils.getPublicKey(pem); + + info.setPublicKey(pem); + return info; + } else if (keystoreFormat.equals(JSON_WEB_KEY_SET)) { InputStream stream = inputParts.get(0).getBody(InputStream.class, null); JSONWebKeySet keySet = JsonSerialization.readValue(stream, JSONWebKeySet.class); PublicKey publicKey = JWKSUtils.getKeyForUse(keySet, JWK.Use.SIG); 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 fc5b3c9e93..dd17b07a8e 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 @@ -40,6 +40,7 @@ import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.CredentialRepresentation; import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.representations.idm.UserSessionRepresentation; +import org.keycloak.services.ErrorResponseException; import org.keycloak.services.ServicesLogger; import org.keycloak.services.clientregistration.ClientRegistrationTokenUtils; import org.keycloak.services.managers.ClientManager; @@ -48,6 +49,8 @@ import org.keycloak.services.managers.ResourceAdminManager; import org.keycloak.services.resources.KeycloakApplication; import org.keycloak.services.ErrorResponse; import org.keycloak.common.util.Time; +import org.keycloak.services.validation.ClientValidator; +import org.keycloak.services.validation.ValidationMessages; import javax.ws.rs.Consumes; import javax.ws.rs.DELETE; @@ -63,10 +66,7 @@ import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import javax.ws.rs.core.UriInfo; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; import static java.lang.Boolean.TRUE; @@ -126,6 +126,16 @@ public class ClientResource { throw new NotFoundException("Could not find client"); } + ValidationMessages validationMessages = new ValidationMessages(); + if (!ClientValidator.validate(rep, validationMessages)) { + Properties messages = AdminRoot.getMessages(session, realm, auth.getAuth().getToken().getLocale()); + throw new ErrorResponseException( + validationMessages.getStringMessages(), + validationMessages.getStringMessages(messages), + Response.Status.BAD_REQUEST + ); + } + try { updateClientFromRep(rep, client, session); adminEvent.operation(OperationType.UPDATE).resourcePath(uriInfo).representation(rep).success(); diff --git a/services/src/main/java/org/keycloak/services/resources/admin/ClientsResource.java b/services/src/main/java/org/keycloak/services/resources/admin/ClientsResource.java index 207f8d1fb5..dacea26b68 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/ClientsResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/ClientsResource.java @@ -17,7 +17,6 @@ package org.keycloak.services.resources.admin; import org.jboss.resteasy.annotations.cache.NoCache; -import org.jboss.resteasy.spi.NotFoundException; import org.jboss.resteasy.spi.ResteasyProviderFactory; import org.keycloak.events.admin.OperationType; import org.keycloak.events.admin.ResourceType; @@ -28,16 +27,13 @@ import org.keycloak.models.RealmModel; import org.keycloak.models.utils.ModelToRepresentation; import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.services.ErrorResponse; +import org.keycloak.services.ErrorResponseException; import org.keycloak.services.ServicesLogger; import org.keycloak.services.managers.ClientManager; +import org.keycloak.services.validation.ClientValidator; +import org.keycloak.services.validation.ValidationMessages; -import javax.ws.rs.Consumes; -import javax.ws.rs.GET; -import javax.ws.rs.POST; -import javax.ws.rs.Path; -import javax.ws.rs.PathParam; -import javax.ws.rs.Produces; -import javax.ws.rs.QueryParam; +import javax.ws.rs.*; import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; @@ -45,6 +41,7 @@ import javax.ws.rs.core.UriInfo; import java.util.ArrayList; import java.util.List; +import java.util.Properties; /** * Base resource class for managing a realm's clients. @@ -122,6 +119,16 @@ public class ClientsResource { public Response createClient(final @Context UriInfo uriInfo, final ClientRepresentation rep) { auth.requireManage(); + ValidationMessages validationMessages = new ValidationMessages(); + if (!ClientValidator.validate(rep, validationMessages)) { + Properties messages = AdminRoot.getMessages(session, realm, auth.getAuth().getToken().getLocale()); + throw new ErrorResponseException( + validationMessages.getStringMessages(), + validationMessages.getStringMessages(messages), + Response.Status.BAD_REQUEST + ); + } + try { ClientModel clientModel = ClientManager.createClient(session, realm, rep, true); diff --git a/services/src/main/java/org/keycloak/services/resources/admin/IdentityProvidersResource.java b/services/src/main/java/org/keycloak/services/resources/admin/IdentityProvidersResource.java index 958a8494e0..8e7c9acb69 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/IdentityProvidersResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/IdentityProvidersResource.java @@ -116,7 +116,7 @@ public class IdentityProvidersResource { InputPart file = formDataMap.get("file").get(0); InputStream inputStream = file.getBody(InputStream.class, null); IdentityProviderFactory providerFactory = getProviderFactorytById(providerId); - Map config = providerFactory.parseConfig(inputStream); + Map config = providerFactory.parseConfig(session, inputStream); return config; } @@ -143,7 +143,7 @@ public class IdentityProvidersResource { try { IdentityProviderFactory providerFactory = getProviderFactorytById(providerId); Map config; - config = providerFactory.parseConfig(inputStream); + config = providerFactory.parseConfig(session, inputStream); return config; } finally { try { diff --git a/services/src/main/java/org/keycloak/services/util/CertificateInfoHelper.java b/services/src/main/java/org/keycloak/services/util/CertificateInfoHelper.java index b309927f81..359d28d84b 100644 --- a/services/src/main/java/org/keycloak/services/util/CertificateInfoHelper.java +++ b/services/src/main/java/org/keycloak/services/util/CertificateInfoHelper.java @@ -17,9 +17,18 @@ package org.keycloak.services.util; +import java.security.PublicKey; +import java.security.cert.X509Certificate; import java.util.HashMap; +import javax.ws.rs.core.Response; + +import org.keycloak.authentication.AuthenticationFlowError; +import org.keycloak.authentication.ClientAuthenticationFlowContext; +import org.keycloak.authentication.authenticators.client.ClientAuthUtil; import org.keycloak.models.ClientModel; +import org.keycloak.models.ModelException; +import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.representations.idm.CertificateRepresentation; import org.keycloak.representations.idm.ClientRepresentation; @@ -34,6 +43,8 @@ public class CertificateInfoHelper { public static final String PUBLIC_KEY = "public.key"; + // CLIENT MODEL METHODS + public static CertificateRepresentation getCertificateFromClient(ClientModel client, String attributePrefix) { String privateKeyAttribute = attributePrefix + "." + PRIVATE_KEY; String certificateAttribute = attributePrefix + "." + X509CERTIFICATE; @@ -75,6 +86,32 @@ public class CertificateInfoHelper { } + public static PublicKey getSignatureValidationKey(ClientModel client, String attributePrefix) throws ModelException { + CertificateRepresentation certInfo = getCertificateFromClient(client, attributePrefix); + + String encodedCertificate = certInfo.getCertificate(); + String encodedPublicKey = certInfo.getPublicKey(); + + if (encodedCertificate == null && encodedPublicKey == null) { + throw new ModelException("Client doesn't have certificate or publicKey configured"); + } + + if (encodedCertificate != null && encodedPublicKey != null) { + throw new ModelException("Client has both publicKey and certificate configured"); + } + + // TODO: Caching of publicKeys / certificates, so it doesn't need to be always computed from pem. For performance reasons... + if (encodedCertificate != null) { + X509Certificate clientCert = KeycloakModelUtils.getCertificate(encodedCertificate); + return clientCert.getPublicKey(); + } else { + return KeycloakModelUtils.getPublicKey(encodedPublicKey); + } + } + + + // CLIENT REPRESENTATION METHODS + public static void updateClientRepresentationCertificateInfo(ClientRepresentation client, CertificateRepresentation rep, String attributePrefix) { String privateKeyAttribute = attributePrefix + "." + PRIVATE_KEY; String certificateAttribute = attributePrefix + "." + X509CERTIFICATE; diff --git a/services/src/main/java/org/keycloak/services/validation/ClientValidator.java b/services/src/main/java/org/keycloak/services/validation/ClientValidator.java new file mode 100644 index 0000000000..22290c5a12 --- /dev/null +++ b/services/src/main/java/org/keycloak/services/validation/ClientValidator.java @@ -0,0 +1,54 @@ +/* + * + * * 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.services.validation; + +import org.keycloak.representations.idm.ClientRepresentation; + +/** + * @author Vaclav Muzikar + */ +public class ClientValidator { + /** + * Checks if the Client's Redirect URIs doesn't contain any URI fragments (like http://example.org/auth#fragment) + * + * @see KEYCLOAK-3421 + * @param client + * @param messages + * @return true if Redirect URIs doesn't contain any URI with fragments + */ + public static boolean validate(ClientRepresentation client, ValidationMessages messages) { + boolean isValid = true; + + if (client.getRedirectUris() != null) { + long urisWithFragmentCount = client.getRedirectUris().stream().filter(p -> p.contains("#")).count(); + if (urisWithFragmentCount > 0) { + messages.add("redirectUris", "Redirect URIs must not contain an URI fragment", "clientRedirectURIsFragmentError"); + isValid = false; + } + } + + if (client.getRootUrl() != null && client.getRootUrl().contains("#")) { + messages.add("rootUrl", "Root URL must not contain an URL fragment", "clientRootURLFragmentError"); + isValid = false; + } + + return isValid; + } +} diff --git a/services/src/main/java/org/keycloak/services/validation/ValidationMessage.java b/services/src/main/java/org/keycloak/services/validation/ValidationMessage.java new file mode 100644 index 0000000000..7e4dac12a5 --- /dev/null +++ b/services/src/main/java/org/keycloak/services/validation/ValidationMessage.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.services.validation; + +import java.text.MessageFormat; +import java.util.Properties; + +/** + * @author Vaclav Muzikar + */ +public class ValidationMessage { + private String fieldId; + private String message; + private String localizedMessageKey; + private Object[] localizedMessageParameters; + + public ValidationMessage(String message) { + this.message = message; + } + + public ValidationMessage(String message, String localizedMessageKey, Object... localizedMessageParameters) { + this.message = message; + this.localizedMessageKey = localizedMessageKey; + this.localizedMessageParameters = localizedMessageParameters; + } + + public String getFieldId() { + return fieldId; + } + + public void setFieldId(String fieldId) { + this.fieldId = fieldId; + } + + public String getLocalizedMessageKey() { + return localizedMessageKey; + } + + public void setLocalizedMessageKey(String localizedMessageKey) { + this.localizedMessageKey = localizedMessageKey; + } + + public Object[] getLocalizedMessageParameters() { + return localizedMessageParameters; + } + + public void setLocalizedMessageParameters(Object[] localizedMessageParameters) { + this.localizedMessageParameters = localizedMessageParameters; + } + + public String getMessage() { + return message; + } + + public String getMessage(Properties localizedMessages) { + if (getLocalizedMessageKey() != null) { + return MessageFormat.format(localizedMessages.getProperty(getLocalizedMessageKey(), getMessage()), getLocalizedMessageParameters()); + } + else { + return getMessage(); + } + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + ValidationMessage message1 = (ValidationMessage) o; + + if (getFieldId() != null ? !getFieldId().equals(message1.getFieldId()) : message1.getFieldId() != null) + return false; + return getMessage() != null ? getMessage().equals(message1.getMessage()) : message1.getMessage() == null; + + } + + @Override + public int hashCode() { + int result = getFieldId() != null ? getFieldId().hashCode() : 0; + result = 31 * result + (getMessage() != null ? getMessage().hashCode() : 0); + return result; + } +} diff --git a/services/src/main/java/org/keycloak/services/validation/ValidationMessages.java b/services/src/main/java/org/keycloak/services/validation/ValidationMessages.java new file mode 100644 index 0000000000..e26ebff397 --- /dev/null +++ b/services/src/main/java/org/keycloak/services/validation/ValidationMessages.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.services.validation; + +import java.util.*; +import java.util.function.Function; +import java.util.stream.Collectors; + + +/** + * @author Vaclav Muzikar + */ +public class ValidationMessages { + private Set messages = new LinkedHashSet<>(); + + public ValidationMessages() {} + + public ValidationMessages(String... messages) { + for (String message : messages) { + add(message); + } + } + + public void add(String message) { + messages.add(new ValidationMessage(message)); + } + + public void add(String message, String localizedMessageKey) { + messages.add(new ValidationMessage(message, localizedMessageKey)); + } + + public void add(String fieldId, String message, String localizedMessageKey) { + ValidationMessage validationMessage = new ValidationMessage(message, localizedMessageKey); + validationMessage.setFieldId(fieldId); + add(validationMessage); + } + + public void add(ValidationMessage message) { + messages.add(message); + } + + public boolean fieldHasError(String fieldId) { + for (ValidationMessage message : messages) { + if (message.getFieldId().equals(fieldId)) { + return true; + } + } + return false; + } + + public Set getMessages() { + return Collections.unmodifiableSet(messages); + } + + protected String getStringMessages(Function function) { + return messages.stream().map(function).collect(Collectors.joining("; ")); + } + + public String getStringMessages() { + return getStringMessages(ValidationMessage::getMessage); + } + + public String getStringMessages(Properties localizedMessages) { + return getStringMessages(x -> x.getMessage(localizedMessages)); + } +} diff --git a/services/src/main/java/org/keycloak/truststore/SSLSocketFactory.java b/services/src/main/java/org/keycloak/truststore/SSLSocketFactory.java index fd1574a04e..f7b5a1d217 100755 --- a/services/src/main/java/org/keycloak/truststore/SSLSocketFactory.java +++ b/services/src/main/java/org/keycloak/truststore/SSLSocketFactory.java @@ -29,7 +29,7 @@ import java.net.Socket; *

* This SSLSocketFactory can only use truststore configured by TruststoreProvider after the ProviderFactory was * initialized using standard Spi load / init mechanism. That will only happen if "truststore" provider is configured - * in keycloak-server.json. + * in standalone.xml or domain.xml. *

* If TruststoreProvider is not available this SSLSocketFactory will delegate all operations to javax.net.ssl.SSLSocketFactory.getDefault(). * diff --git a/services/src/main/java/org/keycloak/utils/MediaType.java b/services/src/main/java/org/keycloak/utils/MediaType.java index 31ab972392..c34858dccd 100644 --- a/services/src/main/java/org/keycloak/utils/MediaType.java +++ b/services/src/main/java/org/keycloak/utils/MediaType.java @@ -31,4 +31,7 @@ public class MediaType { public static final String APPLICATION_FORM_URLENCODED = javax.ws.rs.core.MediaType.APPLICATION_FORM_URLENCODED; public static final javax.ws.rs.core.MediaType APPLICATION_FORM_URLENCODED_TYPE = javax.ws.rs.core.MediaType.APPLICATION_FORM_URLENCODED_TYPE; + public static final String APPLICATION_JWT = "application/jwt"; + public static final javax.ws.rs.core.MediaType APPLICATION_JWT_TYPE = new javax.ws.rs.core.MediaType("application", "jwt"); + } diff --git a/testsuite/integration-arquillian/servers/auth-server/jboss/build-truststore.xml b/testsuite/integration-arquillian/servers/auth-server/jboss/build-truststore.xml deleted file mode 100644 index 073f860a32..0000000000 --- a/testsuite/integration-arquillian/servers/auth-server/jboss/build-truststore.xml +++ /dev/null @@ -1,52 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/testsuite/integration-arquillian/servers/auth-server/jboss/build.xml b/testsuite/integration-arquillian/servers/auth-server/jboss/build.xml deleted file mode 100644 index 1e4ab786e5..0000000000 --- a/testsuite/integration-arquillian/servers/auth-server/jboss/build.xml +++ /dev/null @@ -1,42 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/testsuite/integration-arquillian/servers/auth-server/jboss/common/keycloak-server-subsystem.xsl b/testsuite/integration-arquillian/servers/auth-server/jboss/common/keycloak-server-subsystem.xsl new file mode 100644 index 0000000000..f32c036600 --- /dev/null +++ b/testsuite/integration-arquillian/servers/auth-server/jboss/common/keycloak-server-subsystem.xsl @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + module:org.keycloak.testsuite.integration-arquillian-testsuite-providers + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/testsuite/integration-arquillian/servers/auth-server/jboss/pom.xml b/testsuite/integration-arquillian/servers/auth-server/jboss/pom.xml index 4fab304829..eae0512470 100644 --- a/testsuite/integration-arquillian/servers/auth-server/jboss/pom.xml +++ b/testsuite/integration-arquillian/servers/auth-server/jboss/pom.xml @@ -142,74 +142,6 @@ - - org.apache.maven.plugins - maven-antrun-plugin - 1.8 - - - inject-into-keycloak-server-json - process-resources - - run - - - - - - - - - - - inject-truststore-into-keycloak-server-json - process-resources - - run - - - - - - - - - - - - - ant-contrib - ant-contrib - 1.0b3 - - - ant - ant - - - - - org.apache.ant - ant-apache-bsf - 1.9.3 - - - org.apache.bsf - bsf-api - 3.1 - - - rhino - js - 1.7R2 - - - org.keycloak - keycloak-core - ${project.version} - - - maven-enforcer-plugin @@ -247,6 +179,25 @@ + + inject-provider-and-truststore + process-resources + + transform + + + + +

${auth.server.home}/standalone/configuration + + standalone.xml + + ${common.resources}/keycloak-server-subsystem.xsl + ${auth.server.home}/standalone/configuration + + + + @@ -318,7 +269,6 @@ - server-overlay diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestApplicationResourceProvider.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestApplicationResourceProvider.java index bc63c99f70..5f392a0bc4 100644 --- a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestApplicationResourceProvider.java +++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestApplicationResourceProvider.java @@ -27,6 +27,8 @@ import org.keycloak.representations.adapters.action.PushNotBeforeAction; import org.keycloak.representations.adapters.action.TestAvailabilityAction; import org.keycloak.services.resource.RealmResourceProvider; import org.keycloak.services.resources.RealmsResource; +import org.keycloak.testsuite.rest.resource.TestingExportImportResource; +import org.keycloak.testsuite.rest.resource.TestingOIDCEndpointsApplicationResource; import javax.ws.rs.Consumes; import javax.ws.rs.GET; @@ -53,14 +55,16 @@ public class TestApplicationResourceProvider implements RealmResourceProvider { private final BlockingQueue adminLogoutActions; private final BlockingQueue adminPushNotBeforeActions; private final BlockingQueue adminTestAvailabilityAction; + private final TestApplicationResourceProviderFactory.OIDCClientData oidcClientData; public TestApplicationResourceProvider(KeycloakSession session, BlockingQueue adminLogoutActions, BlockingQueue adminPushNotBeforeActions, - BlockingQueue adminTestAvailabilityAction) { + BlockingQueue adminTestAvailabilityAction, TestApplicationResourceProviderFactory.OIDCClientData oidcClientData) { this.session = session; this.adminLogoutActions = adminLogoutActions; this.adminPushNotBeforeActions = adminPushNotBeforeActions; this.adminTestAvailabilityAction = adminTestAvailabilityAction; + this.oidcClientData = oidcClientData; } @POST @@ -164,6 +168,11 @@ public class TestApplicationResourceProvider implements RealmResourceProvider { return sb.toString(); } + @Path("/oidc-client-endpoints") + public TestingOIDCEndpointsApplicationResource getTestingOIDCClientEndpoints() { + return new TestingOIDCEndpointsApplicationResource(oidcClientData); + } + @Override public Object getResource() { return this; diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestApplicationResourceProviderFactory.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestApplicationResourceProviderFactory.java index 98ca2baa74..6bd7dc262d 100644 --- a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestApplicationResourceProviderFactory.java +++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestApplicationResourceProviderFactory.java @@ -28,6 +28,7 @@ import org.keycloak.representations.adapters.action.TestAvailabilityAction; import org.keycloak.services.resource.RealmResourceProvider; import org.keycloak.services.resource.RealmResourceProviderFactory; +import java.security.KeyPair; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingDeque; @@ -40,9 +41,11 @@ public class TestApplicationResourceProviderFactory implements RealmResourceProv private BlockingQueue pushNotBeforeActions = new LinkedBlockingDeque<>(); private BlockingQueue testAvailabilityActions = new LinkedBlockingDeque<>(); + private final OIDCClientData oidcClientData = new OIDCClientData(); + @Override public RealmResourceProvider create(KeycloakSession session) { - return new TestApplicationResourceProvider(session, adminLogoutActions, pushNotBeforeActions, testAvailabilityActions); + return new TestApplicationResourceProvider(session, adminLogoutActions, pushNotBeforeActions, testAvailabilityActions, oidcClientData); } @Override @@ -62,4 +65,26 @@ public class TestApplicationResourceProviderFactory implements RealmResourceProv return "app"; } + + public static class OIDCClientData { + + private KeyPair signingKeyPair; + private String oidcRequest; + + public KeyPair getSigningKeyPair() { + return signingKeyPair; + } + + public void setSigningKeyPair(KeyPair signingKeyPair) { + this.signingKeyPair = signingKeyPair; + } + + public String getOidcRequest() { + return oidcRequest; + } + + public void setOidcRequest(String oidcRequest) { + this.oidcRequest = oidcRequest; + } + } } 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 b658d35c3a..2085cfac14 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 @@ -23,15 +23,20 @@ import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; import org.infinispan.Cache; +import org.keycloak.OAuth2Constants; import org.keycloak.common.util.Time; import org.keycloak.connections.infinispan.InfinispanConnectionProvider; import org.keycloak.events.Event; import org.keycloak.events.admin.AdminEvent; import org.keycloak.events.admin.ResourceType; +import org.keycloak.jose.jws.Algorithm; +import org.keycloak.jose.jws.JWSBuilder; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserSessionModel; import org.keycloak.models.utils.ModelToRepresentation; +import org.keycloak.protocol.oidc.OIDCLoginProtocol; +import org.keycloak.protocol.oidc.endpoints.request.AuthorizationEndpointRequest; import org.keycloak.representations.idm.AdminEventRepresentation; import org.keycloak.representations.idm.EventRepresentation; import org.keycloak.services.managers.ClientSessionCode; @@ -76,6 +81,7 @@ import org.keycloak.models.UserProvider; import org.keycloak.representations.idm.AuthDetailsRepresentation; import org.keycloak.representations.idm.AuthenticationFlowRepresentation; import org.keycloak.representations.idm.UserRepresentation; +import org.keycloak.testsuite.rest.resource.TestingExportImportResource; import static org.keycloak.exportimport.ExportImportConfig.*; @@ -564,22 +570,6 @@ public class TestingResourceProvider implements RealmResourceProvider { return result; } - @GET - @Path("/run-import") - @Produces(MediaType.APPLICATION_JSON) - public Response runImport() { - new ExportImportManager(session).runImport(); - return Response.ok().build(); - } - - @GET - @Path("/run-export") - @Produces(MediaType.APPLICATION_JSON) - public Response runExport() { - new ExportImportManager(session).runExport(); - return Response.ok().build(); - } - @GET @Path("/valid-credentials") @Produces(MediaType.APPLICATION_JSON) @@ -647,83 +637,14 @@ public class TestingResourceProvider implements RealmResourceProvider { return ModelToRepresentation.toRepresentation(user); } + @Path("/export-import") + public TestingExportImportResource getExportImportResource() { + return new TestingExportImportResource(session); + } + private RealmModel getRealmByName(String realmName) { RealmProvider realmProvider = session.getProvider(RealmProvider.class); return realmProvider.getRealmByName(realmName); } - @GET - @Path("/get-users-per-file") - @Consumes(MediaType.APPLICATION_JSON) - @Produces(MediaType.APPLICATION_JSON) - public Integer getUsersPerFile() { - String usersPerFile = System.getProperty(USERS_PER_FILE, String.valueOf(DEFAULT_USERS_PER_FILE)); - return Integer.parseInt(usersPerFile.trim()); - } - - @PUT - @Path("/set-users-per-file") - @Consumes(MediaType.APPLICATION_JSON) - public void setUsersPerFile(@QueryParam("usersPerFile") Integer usersPerFile) { - System.setProperty(USERS_PER_FILE, String.valueOf(usersPerFile)); - } - - @GET - @Path("/get-dir") - @Consumes(MediaType.APPLICATION_JSON) - @Produces(MediaType.APPLICATION_JSON) - public String getDir() { - return System.getProperty(DIR); - } - - @PUT - @Path("/set-dir") - @Consumes(MediaType.APPLICATION_JSON) - @Produces(MediaType.APPLICATION_JSON) - public String setDir(@QueryParam("dir") String dir) { - return System.setProperty(DIR, dir); - } - - @PUT - @Path("/export-import-provider") - @Consumes(MediaType.APPLICATION_JSON) - public void setProvider(@QueryParam("exportImportProvider") String exportImportProvider) { - System.setProperty(PROVIDER, exportImportProvider); - } - - @PUT - @Path("/export-import-file") - @Consumes(MediaType.APPLICATION_JSON) - public void setFile(@QueryParam("file") String file) { - System.setProperty(FILE, file); - } - - @PUT - @Path("/export-import-action") - @Consumes(MediaType.APPLICATION_JSON) - public void setAction(@QueryParam("exportImportAction") String exportImportAction) { - System.setProperty(ACTION, exportImportAction); - } - - @PUT - @Path("/set-realm-name") - @Consumes(MediaType.APPLICATION_JSON) - public void setRealmName(@QueryParam("realmName") String realmName) { - if (realmName != null && !realmName.isEmpty()) { - System.setProperty(REALM_NAME, realmName); - } else { - System.getProperties().remove(REALM_NAME); - } - } - - @GET - @Path("/get-test-dir") - @Consumes(MediaType.APPLICATION_JSON) - @Produces(MediaType.APPLICATION_JSON) - public String getExportImportTestDirectory() { - System.setProperty("project.build.directory", "target"); - String absolutePath = new File(System.getProperty("project.build.directory", "target")).getAbsolutePath(); - return absolutePath; - } - } diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/resource/TestingExportImportResource.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/resource/TestingExportImportResource.java new file mode 100644 index 0000000000..4f9151c753 --- /dev/null +++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/resource/TestingExportImportResource.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.testsuite.rest.resource; + +import java.io.File; + +import javax.ws.rs.Consumes; +import javax.ws.rs.GET; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +import org.keycloak.exportimport.ExportImportManager; +import org.keycloak.models.KeycloakSession; + +import static org.keycloak.exportimport.ExportImportConfig.ACTION; +import static org.keycloak.exportimport.ExportImportConfig.DEFAULT_USERS_PER_FILE; +import static org.keycloak.exportimport.ExportImportConfig.DIR; +import static org.keycloak.exportimport.ExportImportConfig.FILE; +import static org.keycloak.exportimport.ExportImportConfig.PROVIDER; +import static org.keycloak.exportimport.ExportImportConfig.REALM_NAME; +import static org.keycloak.exportimport.ExportImportConfig.USERS_PER_FILE; + +/** + * @author Marek Posolda + */ +public class TestingExportImportResource { + + private final KeycloakSession session; + + public TestingExportImportResource(KeycloakSession session) { + this.session = session; + } + + @GET + @Path("/run-import") + @Produces(MediaType.APPLICATION_JSON) + public Response runImport() { + new ExportImportManager(session).runImport(); + return Response.ok().build(); + } + + @GET + @Path("/run-export") + @Produces(MediaType.APPLICATION_JSON) + public Response runExport() { + new ExportImportManager(session).runExport(); + return Response.ok().build(); + } + + @GET + @Path("/get-users-per-file") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + public Integer getUsersPerFile() { + String usersPerFile = System.getProperty(USERS_PER_FILE, String.valueOf(DEFAULT_USERS_PER_FILE)); + return Integer.parseInt(usersPerFile.trim()); + } + + @PUT + @Path("/set-users-per-file") + @Consumes(MediaType.APPLICATION_JSON) + public void setUsersPerFile(@QueryParam("usersPerFile") Integer usersPerFile) { + System.setProperty(USERS_PER_FILE, String.valueOf(usersPerFile)); + } + + @GET + @Path("/get-dir") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + public String getDir() { + return System.getProperty(DIR); + } + + @PUT + @Path("/set-dir") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + public String setDir(@QueryParam("dir") String dir) { + return System.setProperty(DIR, dir); + } + + @PUT + @Path("/export-import-provider") + @Consumes(MediaType.APPLICATION_JSON) + public void setProvider(@QueryParam("exportImportProvider") String exportImportProvider) { + System.setProperty(PROVIDER, exportImportProvider); + } + + @PUT + @Path("/export-import-file") + @Consumes(MediaType.APPLICATION_JSON) + public void setFile(@QueryParam("file") String file) { + System.setProperty(FILE, file); + } + + @PUT + @Path("/export-import-action") + @Consumes(MediaType.APPLICATION_JSON) + public void setAction(@QueryParam("exportImportAction") String exportImportAction) { + System.setProperty(ACTION, exportImportAction); + } + + @PUT + @Path("/set-realm-name") + @Consumes(MediaType.APPLICATION_JSON) + public void setRealmName(@QueryParam("realmName") String realmName) { + if (realmName != null && !realmName.isEmpty()) { + System.setProperty(REALM_NAME, realmName); + } else { + System.getProperties().remove(REALM_NAME); + } + } + + @GET + @Path("/get-test-dir") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + public String getExportImportTestDirectory() { + System.setProperty("project.build.directory", "target"); + String absolutePath = new File(System.getProperty("project.build.directory", "target")).getAbsolutePath(); + return absolutePath; + } +} diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/resource/TestingOIDCEndpointsApplicationResource.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/resource/TestingOIDCEndpointsApplicationResource.java new file mode 100644 index 0000000000..6ea488f5c8 --- /dev/null +++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/resource/TestingOIDCEndpointsApplicationResource.java @@ -0,0 +1,137 @@ +/* + * 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.rest.resource; + +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.util.HashMap; +import java.util.Map; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.MediaType; + +import org.jboss.resteasy.annotations.cache.NoCache; +import org.jboss.resteasy.spi.BadRequestException; +import org.keycloak.OAuth2Constants; +import org.keycloak.jose.jwk.JSONWebKeySet; +import org.keycloak.jose.jwk.JWK; +import org.keycloak.jose.jwk.JWKBuilder; +import org.keycloak.jose.jws.Algorithm; +import org.keycloak.jose.jws.JWSBuilder; +import org.keycloak.models.utils.KeycloakModelUtils; +import org.keycloak.protocol.oidc.OIDCLoginProtocol; +import org.keycloak.testsuite.rest.TestApplicationResourceProviderFactory; + +/** + * @author Marek Posolda + */ +public class TestingOIDCEndpointsApplicationResource { + + public static final String PRIVATE_KEY = "privateKey"; + public static final String PUBLIC_KEY = "publicKey"; + + private final TestApplicationResourceProviderFactory.OIDCClientData clientData; + + public TestingOIDCEndpointsApplicationResource(TestApplicationResourceProviderFactory.OIDCClientData oidcClientData) { + this.clientData = oidcClientData; + } + + @GET + @Produces(MediaType.APPLICATION_JSON) + @Path("/generate-keys") + @NoCache + public Map generateKeys() { + try { + KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA"); + generator.initialize(2048); + clientData.setSigningKeyPair(generator.generateKeyPair()); + } catch (NoSuchAlgorithmException e) { + throw new BadRequestException("Error generating signing keypair", e); + } + + String privateKeyPem = KeycloakModelUtils.getPemFromKey(clientData.getSigningKeyPair().getPrivate()); + String publicKeyPem = KeycloakModelUtils.getPemFromKey(clientData.getSigningKeyPair().getPublic()); + + Map res = new HashMap<>(); + res.put(PRIVATE_KEY, privateKeyPem); + res.put(PUBLIC_KEY, publicKeyPem); + return res; + } + + + @GET + @Produces(MediaType.APPLICATION_JSON) + @Path("/get-jwks") + @NoCache + public JSONWebKeySet getJwks() { + JSONWebKeySet keySet = new JSONWebKeySet(); + + if (clientData.getSigningKeyPair() == null) { + keySet.setKeys(new JWK[] {}); + } else { + keySet.setKeys(new JWK[] { JWKBuilder.create().rs256(clientData.getSigningKeyPair().getPublic()) }); + } + + return keySet; + } + + + @GET + @Path("/set-oidc-request") + @Produces(org.keycloak.utils.MediaType.APPLICATION_JWT) + @NoCache + public void setOIDCRequest(@QueryParam("realmName") String realmName, @QueryParam("clientId") String clientId, + @QueryParam("redirectUri") String redirectUri, @QueryParam("maxAge") String maxAge, + @QueryParam("jwaAlgorithm") String jwaAlgorithm) { + Map oidcRequest = new HashMap<>(); + oidcRequest.put(OIDCLoginProtocol.CLIENT_ID_PARAM, clientId); + oidcRequest.put(OIDCLoginProtocol.RESPONSE_TYPE_PARAM, OAuth2Constants.CODE); + oidcRequest.put(OIDCLoginProtocol.REDIRECT_URI_PARAM, redirectUri); + if (maxAge != null) { + oidcRequest.put(OIDCLoginProtocol.MAX_AGE_PARAM, Integer.parseInt(maxAge)); + } + + Algorithm alg = Enum.valueOf(Algorithm.class, jwaAlgorithm); + if (alg == Algorithm.none) { + clientData.setOidcRequest(new JWSBuilder().jsonContent(oidcRequest).none()); + } else if (alg == Algorithm.RS256) { + if (clientData.getSigningKeyPair() == null) { + throw new BadRequestException("Requested RS256, but signing key not set"); + } + + PrivateKey privateKey = clientData.getSigningKeyPair().getPrivate(); + clientData.setOidcRequest(new JWSBuilder().jsonContent(oidcRequest).rsa256(privateKey)); + } else { + throw new BadRequestException("Unknown argument: " + jwaAlgorithm); + } + } + + + @GET + @Path("/get-oidc-request") + @Produces(org.keycloak.utils.MediaType.APPLICATION_JWT) + @NoCache + public String getOIDCRequest() { + return clientData.getOidcRequest(); + } +} diff --git a/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/pom.xml b/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/pom.xml index 7885857cdd..f5e2d4c2b9 100755 --- a/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/pom.xml +++ b/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/pom.xml @@ -39,6 +39,12 @@ 1.0.0.Final provided + + javax.enterprise + cdi-api + 1.0-SP4 + provided + org.keycloak keycloak-authz-client diff --git a/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/admin/AdminAlbumService.java b/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/admin/AdminAlbumService.java index b349e026d4..428ba078bd 100644 --- a/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/admin/AdminAlbumService.java +++ b/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/admin/AdminAlbumService.java @@ -19,9 +19,8 @@ package org.keycloak.example.photoz.admin; import org.keycloak.example.photoz.entity.Album; -import javax.ejb.Stateless; +import javax.inject.Inject; import javax.persistence.EntityManager; -import javax.persistence.PersistenceContext; import javax.ws.rs.GET; import javax.ws.rs.Path; import javax.ws.rs.Produces; @@ -36,12 +35,11 @@ import java.util.List; * @author Pedro Igor */ @Path("/admin/album") -@Stateless public class AdminAlbumService { public static final String SCOPE_ADMIN_ALBUM_MANAGE = "urn:photoz.com:scopes:album:admin:manage"; - @PersistenceContext + @Inject private EntityManager entityManager; @Context diff --git a/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/album/AlbumService.java b/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/album/AlbumService.java index a5d7f161b4..d0e9c2d5c5 100644 --- a/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/album/AlbumService.java +++ b/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/album/AlbumService.java @@ -7,10 +7,12 @@ import org.keycloak.authorization.client.representation.ScopeRepresentation; import org.keycloak.authorization.client.resource.ProtectionResource; import org.keycloak.example.photoz.ErrorResponse; import org.keycloak.example.photoz.entity.Album; +import org.keycloak.example.photoz.util.Transaction; import org.keycloak.representations.adapters.config.AdapterConfig; import org.keycloak.util.JsonSerialization; import javax.ejb.Stateless; +import javax.inject.Inject; import javax.persistence.EntityManager; import javax.persistence.PersistenceContext; import javax.persistence.Query; @@ -31,14 +33,14 @@ import java.util.List; import java.util.Set; @Path("/album") -@Stateless +@Transaction public class AlbumService { public static final String SCOPE_ALBUM_VIEW = "urn:photoz.com:scopes:album:view"; public static final String SCOPE_ALBUM_CREATE = "urn:photoz.com:scopes:album:create"; public static final String SCOPE_ALBUM_DELETE = "urn:photoz.com:scopes:album:delete"; - @PersistenceContext + @Inject private EntityManager entityManager; @Context diff --git a/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/album/ProfileService.java b/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/album/ProfileService.java index be638b602e..f7b55cbc33 100644 --- a/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/album/ProfileService.java +++ b/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/album/ProfileService.java @@ -18,6 +18,7 @@ package org.keycloak.example.photoz.album; import javax.ejb.Stateless; +import javax.inject.Inject; import javax.persistence.EntityManager; import javax.persistence.PersistenceContext; import javax.servlet.http.HttpServletRequest; @@ -34,12 +35,11 @@ import java.util.List; * @author Pedro Igor */ @Path("/profile") -@Stateless public class ProfileService { private static final String PROFILE_VIEW = "urn:photoz.com:scopes:profile:view"; - @PersistenceContext + @Inject private EntityManager entityManager; @GET diff --git a/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/util/Resources.java b/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/util/Resources.java new file mode 100644 index 0000000000..c917da24db --- /dev/null +++ b/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/util/Resources.java @@ -0,0 +1,51 @@ +/* + * 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.photoz.util; + +import javax.annotation.PostConstruct; +import javax.annotation.PreDestroy; +import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.context.RequestScoped; +import javax.enterprise.inject.Produces; +import javax.persistence.EntityManager; +import javax.persistence.EntityManagerFactory; +import javax.persistence.Persistence; + +/** + * @author Pedro Igor + */ +@ApplicationScoped +public class Resources { + + private EntityManagerFactory entityManagerFactory; + + @PostConstruct + public void init() { + entityManagerFactory = Persistence.createEntityManagerFactory("primary"); + } + + @PreDestroy + public void dispose() { + entityManagerFactory.close(); + } + + @RequestScoped + @Produces + public EntityManager createEntityManager() { + return entityManagerFactory.createEntityManager(); + } +} diff --git a/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/util/Transaction.java b/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/util/Transaction.java new file mode 100644 index 0000000000..a3caa780a8 --- /dev/null +++ b/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/util/Transaction.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.example.photoz.util; + +import javax.interceptor.InterceptorBinding; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * @author Pedro Igor + */ +@InterceptorBinding +@Target({ TYPE }) +@Retention(RUNTIME) +public @interface Transaction { +} diff --git a/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/util/TransactionInterceptor.java b/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/util/TransactionInterceptor.java new file mode 100644 index 0000000000..36d35f319b --- /dev/null +++ b/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/util/TransactionInterceptor.java @@ -0,0 +1,56 @@ +/* + * 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.photoz.util; + +import javax.enterprise.inject.Instance; +import javax.inject.Inject; +import javax.interceptor.AroundInvoke; +import javax.interceptor.Interceptor; +import javax.interceptor.InvocationContext; +import javax.persistence.EntityManager; +import javax.persistence.EntityTransaction; + +/** + * @author Pedro Igor + */ +@Interceptor +@Transaction +public class TransactionInterceptor { + + @Inject + private Instance entityManager; + + @AroundInvoke + public Object aroundInvoke(InvocationContext context) { + EntityManager entityManager = this.entityManager.get(); + EntityTransaction transaction = entityManager.getTransaction(); + + try { + transaction.begin(); + Object proceed = context.proceed(); + transaction.commit(); + return proceed; + } catch (Exception cause) { + if (transaction != null && transaction.isActive()) { + transaction.rollback(); + } + throw new RuntimeException(cause); + } finally { + entityManager.close(); + } + } +} diff --git a/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/resources/META-INF/beans.xml b/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/resources/META-INF/beans.xml index 957dc8ac43..fbf2a32edf 100644 --- a/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/resources/META-INF/beans.xml +++ b/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/resources/META-INF/beans.xml @@ -3,5 +3,7 @@ xsi:schemaLocation=" http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/beans_1_0.xsd"> - + + org.keycloak.example.photoz.util.TransactionInterceptor + diff --git a/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/resources/META-INF/persistence.xml b/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/resources/META-INF/persistence.xml index 9323182405..8b6d226720 100644 --- a/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/resources/META-INF/persistence.xml +++ b/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/resources/META-INF/persistence.xml @@ -4,14 +4,18 @@ xsi:schemaLocation=" http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd"> - - java:jboss/datasources/PhotozDS + + org.hibernate.ejb.HibernatePersistence org.keycloak.example.photoz.entity.Album org.keycloak.example.photoz.entity.Photo - + + + + + diff --git a/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/webapp/META-INF/jboss-deployment-structure.xml b/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/webapp/META-INF/jboss-deployment-structure.xml index 4b23be62d7..455335898f 100644 --- a/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/webapp/META-INF/jboss-deployment-structure.xml +++ b/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/webapp/META-INF/jboss-deployment-structure.xml @@ -20,6 +20,7 @@ + diff --git a/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/webapp/WEB-INF/photoz-ds.xml b/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/webapp/WEB-INF/photoz-ds.xml deleted file mode 100644 index 247448f2ae..0000000000 --- a/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/webapp/WEB-INF/photoz-ds.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - jdbc:h2:${jboss.server.data.dir}/kc-authz-photo;AUTO_SERVER=TRUE - h2 - - sa - sa - - - \ No newline at end of file diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/BadAssertionSalesPostSig.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/BadAssertionSalesPostSig.java new file mode 100644 index 0000000000..8d10b22a04 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/BadAssertionSalesPostSig.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.testsuite.adapter.page; + +import org.jboss.arquillian.container.test.api.OperateOnDeployment; +import org.jboss.arquillian.test.api.ArquillianResource; + +import java.net.URL; + +/** + * @author mhajas + */ +public class BadAssertionSalesPostSig extends SAMLServlet { + public static final String DEPLOYMENT_NAME = "bad-assertion-sales-post-sig"; + + @ArquillianResource + @OperateOnDeployment(DEPLOYMENT_NAME) + private URL url; + + @Override + public URL getInjectedUrl() { + return url; + } +} \ No newline at end of file diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/EmployeeServlet.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/EmployeeServlet.java new file mode 100644 index 0000000000..c8a0d662a4 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/EmployeeServlet.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.testsuite.adapter.page; + +import org.jboss.arquillian.container.test.api.OperateOnDeployment; +import org.jboss.arquillian.test.api.ArquillianResource; + +import java.net.URL; + +/** + * @author mhajas + */ +public class EmployeeServlet extends SAMLServlet { + public static final String DEPLOYMENT_NAME = "employee"; + + @ArquillianResource + @OperateOnDeployment(DEPLOYMENT_NAME) + private URL url; + + @Override + public URL getInjectedUrl() { + return url; + } +} diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/InputPortal.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/InputPortal.java index 6e66e2c32d..7edc5c5bc8 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/InputPortal.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/InputPortal.java @@ -17,18 +17,18 @@ package org.keycloak.testsuite.adapter.page; -import java.net.URL; import org.jboss.arquillian.container.test.api.OperateOnDeployment; import org.jboss.arquillian.test.api.ArquillianResource; -import org.keycloak.testsuite.page.AbstractPageWithInjectedUrl; import org.openqa.selenium.WebElement; import org.openqa.selenium.support.FindBy; +import java.net.URL; + /** * * @author tkyjovsk */ -public class InputPortal extends AbstractPageWithInjectedUrl { +public class InputPortal extends SAMLServlet { public static final String DEPLOYMENT_NAME = "input-portal"; diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/MissingAssertionSig.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/MissingAssertionSig.java new file mode 100644 index 0000000000..78ff38c10a --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/MissingAssertionSig.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.testsuite.adapter.page; + +import org.jboss.arquillian.container.test.api.OperateOnDeployment; +import org.jboss.arquillian.test.api.ArquillianResource; + +import java.net.URL; + +/** + * @author mhajas + */ +public class MissingAssertionSig extends SAMLServlet { + public static final String DEPLOYMENT_NAME = "missing-assertion-sig"; + + @ArquillianResource + @OperateOnDeployment(DEPLOYMENT_NAME) + private URL url; + + @Override + public URL getInjectedUrl() { + return url; + } +} \ No newline at end of file diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/SAMLServlet.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/SAMLServlet.java index fe856dc14a..cc4a419725 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/SAMLServlet.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/SAMLServlet.java @@ -40,8 +40,8 @@ public abstract class SAMLServlet extends AbstractPageWithInjectedUrl { } } - public void checkRolesEndPoint() { - driver.navigate().to(getUriBuilder().build().toASCIIString() + "/checkRoles"); + public void checkRolesEndPoint(boolean value) { + driver.navigate().to(getUriBuilder().build().toASCIIString() + "/" + (value ? "" : "un") + "checkRoles"); pause(300); } } diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/SalesPost2Servlet.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/SalesPost2Servlet.java new file mode 100644 index 0000000000..d821d868d6 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/SalesPost2Servlet.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.testsuite.adapter.page; + +import org.jboss.arquillian.container.test.api.OperateOnDeployment; +import org.jboss.arquillian.test.api.ArquillianResource; + +import java.net.URL; + +/** + * @author mhajas + */ +public class SalesPost2Servlet extends SAMLServlet { + public static final String DEPLOYMENT_NAME = "sales-post2"; + + @ArquillianResource + @OperateOnDeployment(DEPLOYMENT_NAME) + private URL url; + + @Override + public URL getInjectedUrl() { + return url; + } +} diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/SalesPostAssertionAndResponseSig.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/SalesPostAssertionAndResponseSig.java new file mode 100644 index 0000000000..a4522b4b2d --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/SalesPostAssertionAndResponseSig.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.testsuite.adapter.page; + +import org.jboss.arquillian.container.test.api.OperateOnDeployment; +import org.jboss.arquillian.test.api.ArquillianResource; + +import java.net.URL; + +/** + * @author mhajas + */ +public class SalesPostAssertionAndResponseSig extends SAMLServlet { + public static final String DEPLOYMENT_NAME = "sales-post-assertion-and-response-sig"; + + @ArquillianResource + @OperateOnDeployment(DEPLOYMENT_NAME) + private URL url; + + @Override + public URL getInjectedUrl() { + return url; + } +} \ No newline at end of file diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/servlet/InputServlet.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/servlet/InputServlet.java index a2038bf698..5fb04e9435 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/servlet/InputServlet.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/servlet/InputServlet.java @@ -17,6 +17,8 @@ package org.keycloak.testsuite.adapter.servlet; +import org.junit.Assert; + import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; @@ -31,6 +33,8 @@ import java.io.PrintWriter; @WebServlet("/input-portal") public class InputServlet extends HttpServlet { + private static final String FORM_URLENCODED = "application/x-www-form-urlencoded"; + @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String appBase; @@ -41,6 +45,16 @@ public class InputServlet extends HttpServlet { } String actionUrl = appBase + "/input-portal/secured/post"; + if (req.getRequestURI().endsWith("insecure")) { + if (System.getProperty("insecure.user.principal.unsupported") == null) Assert.assertNotNull(req.getUserPrincipal()); + resp.setContentType("text/html"); + PrintWriter pw = resp.getWriter(); + pw.printf("Input Servlet%s\n", "Insecure Page"); + if (req.getUserPrincipal() != null) pw.printf("UserPrincipal: " + req.getUserPrincipal().getName()); + pw.print(""); + pw.flush(); + return; + } resp.setContentType("text/html"); PrintWriter pw = resp.getWriter(); @@ -56,6 +70,16 @@ public class InputServlet extends HttpServlet { @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + if (!FORM_URLENCODED.equals(req.getContentType())) { + resp.setStatus(HttpServletResponse.SC_BAD_REQUEST); + PrintWriter pw = resp.getWriter(); + resp.setContentType("text/plain"); + pw.printf("Expecting content type " + FORM_URLENCODED + + ", received " + req.getContentType() + " instead"); + pw.flush(); + return; + } + resp.setContentType("text/plain"); PrintWriter pw = resp.getWriter(); pw.printf("parameter="+req.getParameter("parameter")); diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/servlet/SamlSPFacade.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/servlet/SamlSPFacade.java new file mode 100755 index 0000000000..21f07b3316 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/servlet/SamlSPFacade.java @@ -0,0 +1,69 @@ +/* + * 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.adapter.servlet; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.ws.rs.core.UriBuilder; +import java.io.IOException; +import java.io.PrintWriter; + +/** +* @author Bill Burke +* @version $Revision: 1 $ +*/ +public class SamlSPFacade extends HttpServlet { + public static String samlResponse; + public static String RELAY_STATE = "http://test.com/foo/bar"; + public static String sentRelayState; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + handler(req, resp); + } + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + handler(req, resp); + } + + private void handler(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + System.out.println("In SamlSPFacade Servlet handler()"); + if (req.getParameterMap().isEmpty()) { + System.out.println("ParameterMap is empty, redirecting to keycloak server "); + resp.setStatus(302); + // Redirect + // UriBuilder builder = UriBuilder.fromUri("http://localhost:8081/auth/realms/demo/protocol/saml?SAMLRequest=jVLRTsIwFP2Vpe%2BjG4wxG0YyWYxL0BBAH3wx3XYnTbp29nYof%2B8YEvEBNOlD03vOveec2ynyWjYsae1WreC9BbTOZy0Vsr4Qk9YopjkKZIrXgMwWbJ08LNhw4LHGaKsLLcmRch3MEcFYoRVxktN1rhW2NZg1mJ0o4Gm1iMnW2oZRKnXB5VajZZEX%2BRTqRuo9ACVO2mkUih%2F4l9C8s0MNcFkjLaHW9KSUHlwR506bAnrPMam4RCBOlsYkS1%2BD3MvLcDJxAx9KN4jCkXszrG5cP%2BCVH4y8IM8PYFx2dsQOfuiILWQKLVc2JkPPH7te6HrRxh%2BzUdidwSSIXoiz%2FBZyK1Qp1Nv1yPIjCNn9ZrN0V1AKA4UlzjMY7N13IDKbHjyxXoA5291%2FtzH7I%2FApPet%2FHNawx65hli61FMXeSaTUH%2FMubtvlYU0LfcA1t5cl%2BAO%2FfxGlW%2FVQ1ipsoBCVgJLQ2XHo7385%2BwI%3D"); + UriBuilder builder = UriBuilder.fromUri("http://localhost:8180/auth/realms/demo/protocol/saml?SAMLRequest=jZJdS8MwFIbvBf9DyX2XNG62hnUwHeLAj7JNL7yRmJ65QJrUnNSPf29WHQp%2BIOQiJM%2FJed%2F3ZIyyMa2YdmFjF%2FDYAYbkpTEWRX9Rks5b4SRqFFY2gCIosZxenAs%2BYKL1LjjlDHkv%2BRuWiOCDdpYk0932xFnsGvBL8E9awfXivCSbEFpBqXFKmo3DIApeMApNa9wrACXJLGrUVm7rf6KzSMtoh3qQpkFaQ%2BPoTinduiLJqfMKes8lWUuDQJL5rCTz2d2wLmCkgKc5Z4fpMOf3qSyO8pTXxUHOjphibBRhrKId%2FQSf5YgdzC0GaUNJOMtGKTtI2eGKcxFXlg%2BK0fCWJNWHkGNta20f%2Fo7s%2Fh1CcbZaVWl1tVyR5AY89s4jQCb7e%2BOtI9G3918m999ZTL4HyIrsM%2B4x%2FfL%2Brl0rLuOT81nljFavydQY93wS4w4xj%2BA76ANuZPhdRDbI%2BhNdp%2BseFZ3FFpRea6gJ3Tai33%2Fm5A0%3D"); + builder.queryParam("RelayState", RELAY_STATE); + resp.setHeader("Location", builder.build().toString()); + return; + } + + System.out.println("Response was received"); + samlResponse = req.getParameter("SAMLResponse"); + sentRelayState = req.getParameter("RelayState"); + + PrintWriter pw = resp.getWriter(); + pw.println("Relay state: " + sentRelayState); + pw.println("SAML response: " + samlResponse); + pw.flush(); + } +} diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/servlet/SendUsernameServlet.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/servlet/SendUsernameServlet.java index 58feae3080..6e70f15851 100755 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/servlet/SendUsernameServlet.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/servlet/SendUsernameServlet.java @@ -19,6 +19,10 @@ package org.keycloak.testsuite.adapter.servlet; import org.jboss.resteasy.annotations.cache.NoCache; +import org.keycloak.adapters.saml.SamlAuthenticationError; +import org.keycloak.adapters.saml.SamlPrincipal; +import org.keycloak.adapters.spi.AuthenticationError; +import org.keycloak.saml.processing.core.saml.v2.constants.X500SAMLProfileConstants; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; @@ -28,6 +32,9 @@ import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import java.io.IOException; import java.security.Principal; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; /** * @author Bill Burke @@ -38,6 +45,9 @@ import java.security.Principal; public class SendUsernameServlet { private static boolean checkRoles = false; + private static SamlAuthenticationError authError; + private static Principal sentPrincipal; + private static List checkRolesList = Collections.singletonList("manager"); @Context private HttpServletRequest httpServletRequest; @@ -62,7 +72,15 @@ public class SendUsernameServlet { throw new RuntimeException("User: " + httpServletRequest.getUserPrincipal() + " do not have required role"); } - return Response.ok(getOutput(), MediaType.TEXT_PLAIN).build(); + return Response.ok(getOutput(), MediaType.TEXT_HTML_TYPE).build(); + } + + @GET + @Path("getAttributes") + public Response getSentPrincipal() throws IOException { + System.out.println("In SendUsername Servlet getSentPrincipal()"); + + return Response.ok(getAttributes(), MediaType.TEXT_HTML_TYPE).build(); } @GET @@ -79,6 +97,23 @@ public class SendUsernameServlet { return doPost(checkRolesFlag); } + @POST + @Path("error.html") + public Response errorPagePost() { + authError = (SamlAuthenticationError) httpServletRequest.getAttribute(AuthenticationError.class.getName()); + Integer statusCode = (Integer) httpServletRequest.getAttribute("javax.servlet.error.status_code"); + System.out.println("In SendUsername Servlet errorPage() status code: " + statusCode); + + return Response.ok(getErrorOutput(statusCode), MediaType.TEXT_HTML_TYPE).build(); + } + + @GET + @Path("error.html") + public Response errorPageGet() { + return errorPagePost(); + } + + @GET @Path("checkRoles") public String checkRolesEndPoint() { @@ -87,8 +122,35 @@ public class SendUsernameServlet { return "Roles will be checked"; } + @GET + @Path("uncheckRoles") + public String uncheckRolesEndPoint() { + checkRoles = false; + System.out.println("Setting checkRoles to false"); + checkRolesList = Collections.singletonList("manager"); + return "Roles will not be checked"; + } + + @GET + @Path("setCheckRoles") + public String setCheckRoles(@QueryParam("roles") String roles) { + checkRolesList = Arrays.asList(roles.split(",")); + checkRoles = true; + System.out.println("Setting checkRolesList to " + checkRolesList.toString()); + return "These roles will be checked: " + checkRolesList.toString(); + } + + private boolean checkRoles() { - return httpServletRequest.isUserInRole("manager"); + for (String role : checkRolesList) { + System.out.println("In checkRoles() checking role " + role + " for user " + httpServletRequest.getUserPrincipal().getName()); + if (!httpServletRequest.isUserInRole(role)) { + System.out.println("User is not in role " + role); + return false; + } + } + + return true; } private String getOutput() { @@ -102,6 +164,31 @@ public class SendUsernameServlet { return output + "null"; } + sentPrincipal = principal; + return output + principal.getName(); } + + private String getErrorOutput(Integer statusCode) { + String output = "Error Page

There was an error

"; + if (statusCode != null) + output += "
HTTP status code: " + statusCode; + if (authError != null) + output += "
Error info: " + authError.toString(); + return output + ""; + } + + private String getAttributes() { + SamlPrincipal principal = (SamlPrincipal) sentPrincipal; + String output = "attribute email: " + principal.getAttribute(X500SAMLProfileConstants.EMAIL.get()); + output += "
topAttribute: " + principal.getAttribute("topAttribute"); + output += "
level2Attribute: " + principal.getAttribute("level2Attribute"); + output += "
group: " + principal.getAttributes("group").toString(); + output += "
friendlyAttribute email: " + principal.getFriendlyAttribute("email"); + output += "
phone: " + principal.getAttribute("phone"); + output += "
friendlyAttribute phone: " + principal.getFriendlyAttribute("phone"); + output += "
hardcoded-attribute: " + principal.getAttribute("hardcoded-attribute"); + + return output; + } } diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/DeploymentArchiveProcessor.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/DeploymentArchiveProcessor.java index 68d7183a06..9cd7625cc1 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/DeploymentArchiveProcessor.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/DeploymentArchiveProcessor.java @@ -17,7 +17,6 @@ package org.keycloak.testsuite.arquillian; -import org.apache.commons.io.IOUtils; import org.apache.tools.ant.DirectoryScanner; import org.jboss.arquillian.container.test.spi.client.deployment.ApplicationArchiveProcessor; import org.jboss.arquillian.test.spi.TestClass; @@ -31,6 +30,7 @@ import org.keycloak.testsuite.arquillian.annotation.UseServletFilter; import org.keycloak.testsuite.util.IOUtil; import org.keycloak.util.JsonSerialization; import org.w3c.dom.Document; +import org.w3c.dom.Element; import javax.xml.transform.TransformerException; import java.io.File; @@ -112,6 +112,12 @@ public class DeploymentArchiveProcessor implements ApplicationArchiveProcessor { log.error("Can't transform document to String"); throw new RuntimeException(e); } + + // For running SAML tests it is necessary to have few dependencies on app-server side. + // Few of them are not in adapter zip so we need to add them manually here + log.info("Adding SAMLFilter dependencies to " + archive.getName()); + ((WebArchive) archive).addAsLibraries(KeycloakDependenciesResolver.resolveDependencies("org.keycloak:keycloak-saml-servlet-filter-adapter:" + System.getProperty("project.version"))); + } else { // OIDC adapter config try { AdapterConfig adapterConfig = loadJson(archive.get(adapterConfigPath) @@ -156,45 +162,58 @@ public class DeploymentArchiveProcessor implements ApplicationArchiveProcessor { protected void modifyWebXml(Archive archive, TestClass testClass) { try { - String webXmlContent = IOUtils.toString( + Document webXmlDoc = loadXML( archive.get(WEBXML_PATH).getAsset().openStream()); if (isTomcatAppServer(testClass.getJavaClass())) { - webXmlContent = webXmlContent.replace("KEYCLOAK", "BASIC"); + modifyDocElementValue(webXmlDoc, "auth-method", "KEYCLOAK", "BASIC"); } if (testClass.getJavaClass().isAnnotationPresent(UseServletFilter.class)) { //We need to add filter declaration to web.xml log.info("Adding filter to " + testClass.getAnnotation(UseServletFilter.class).filterClass() + " with mapping " + testClass.getAnnotation(UseServletFilter.class).filterPattern() + " for " + archive.getName()); - String filter = "\n\n" + - "" + testClass.getAnnotation(UseServletFilter.class).filterName() + "\n" + - "" + testClass.getAnnotation(UseServletFilter.class).filterClass() + "\n" + - "\n" + - "\n\n" + - "" + testClass.getAnnotation(UseServletFilter.class).filterName() + "\n" + - "" + testClass.getAnnotation(UseServletFilter.class).filterPattern() + "\n"; + + Element filter = webXmlDoc.createElement("filter"); + Element filterName = webXmlDoc.createElement("filter-name"); + Element filterClass = webXmlDoc.createElement("filter-class"); + + filterName.setTextContent(testClass.getAnnotation(UseServletFilter.class).filterName()); + filterClass.setTextContent(testClass.getAnnotation(UseServletFilter.class).filterClass()); + + filter.appendChild(filterName); + filter.appendChild(filterClass); + appendChildInDocument(webXmlDoc, "web-app", filter); + + Element filterMapping = webXmlDoc.createElement("filter-mapping"); + + + Element urlPattern = webXmlDoc.createElement("url-pattern"); + + filterName = webXmlDoc.createElement("filter-name"); + + filterName.setTextContent(testClass.getAnnotation(UseServletFilter.class).filterName()); + urlPattern.setTextContent(getElementTextContent(webXmlDoc, "web-app/security-constraint/web-resource-collection/url-pattern")); + + filterMapping.appendChild(filterName); + filterMapping.appendChild(urlPattern); + if (!testClass.getAnnotation(UseServletFilter.class).dispatcherType().isEmpty()) { - filter += "" + testClass.getAnnotation(UseServletFilter.class).dispatcherType() + "\n"; + Element dispatcher = webXmlDoc.createElement("dispatcher"); + dispatcher.setTextContent(testClass.getAnnotation(UseServletFilter.class).dispatcherType()); + filterMapping.appendChild(dispatcher); } - filter += "\n"; - - webXmlContent = webXmlContent.replace("", " " + filter); - - //Also we need to add all dependencies within war lib directory, because filter needs to work without installed adapter - log.info("Adding SAMLFilter dependencies to " + archive.getName()); - ((WebArchive) archive).addAsLibraries(new SAMLFilterDependency().getDependencies()); - + appendChildInDocument(webXmlDoc, "web-app", filterMapping); //finally we need to remove all keycloak related configuration from web.xml - int start = webXmlContent.indexOf(""); - int end = webXmlContent.indexOf("") + "".length(); - - - webXmlContent = webXmlContent.substring(0, start) + webXmlContent.substring(end); + removeElementFromDoc(webXmlDoc, "web-app", "security-constraint"); + removeElementFromDoc(webXmlDoc, "web-app", "login-config"); + removeElementFromDoc(webXmlDoc, "web-app", "security-role"); } - archive.add(new StringAsset((webXmlContent)), WEBXML_PATH); - } catch (IOException ex) { - throw new RuntimeException("Cannot load web.xml from archive."); + + archive.add(new StringAsset((documentToString(webXmlDoc))), WEBXML_PATH); + } catch (TransformerException e) { + log.error("Can't transform document to String"); + throw new RuntimeException(e); } } diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/KeycloakDependenciesResolver.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/KeycloakDependenciesResolver.java new file mode 100644 index 0000000000..2f62c2b29e --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/KeycloakDependenciesResolver.java @@ -0,0 +1,39 @@ +package org.keycloak.testsuite.arquillian; + +import org.jboss.logging.Logger; +import org.jboss.shrinkwrap.resolver.api.maven.Maven; +import org.jboss.shrinkwrap.resolver.api.maven.PomEquippedResolveStage; +import org.jboss.shrinkwrap.resolver.api.maven.ScopeType; +import org.jboss.shrinkwrap.resolver.api.maven.coordinate.MavenDependencies; + +import java.io.File; +import java.util.HashMap; +import java.util.Map; + +/** + * @author mhajas + */ +public class KeycloakDependenciesResolver { + + private static Map dependencies = new HashMap<>(); + + protected static final Logger log = org.jboss.logging.Logger.getLogger(KeycloakDependenciesResolver.class); + + public static File[] resolveDependencies(String canonicalForm) { + if (dependencies.containsKey(canonicalForm)) { + return dependencies.get(canonicalForm); + } + + log.info("Resolving " + canonicalForm + "'s dependencies"); + PomEquippedResolveStage resolver = Maven.configureResolverViaPlugin(); + + File[] files = resolver.addDependency(MavenDependencies.createDependency(canonicalForm, ScopeType.COMPILE, false)) + .resolve().withTransitivity().asFile(); + + dependencies.put(canonicalForm, files); + + log.info("Resolving dependencies is finished with " + files.length + " files"); + + return dependencies.get(canonicalForm); + } +} diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/SAMLFilterDependency.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/SAMLFilterDependency.java deleted file mode 100644 index 808a3839da..0000000000 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/SAMLFilterDependency.java +++ /dev/null @@ -1,87 +0,0 @@ -package org.keycloak.testsuite.arquillian; - -import org.jboss.logging.Logger; -import org.jboss.shrinkwrap.resolver.api.maven.Maven; -import org.jboss.shrinkwrap.resolver.api.maven.PackagingType; -import org.jboss.shrinkwrap.resolver.api.maven.ScopeType; -import org.jboss.shrinkwrap.resolver.api.maven.coordinate.MavenDependency; -import org.jboss.shrinkwrap.resolver.api.maven.coordinate.MavenDependencyExclusion; - -import java.io.File; -import java.util.Collections; -import java.util.Set; - -/** - * @author mhajas - */ -public class SAMLFilterDependency implements MavenDependency { - - private static File[] files; - - protected final Logger log = org.jboss.logging.Logger.getLogger(this.getClass()); - - @Override - public Set getExclusions() { - return Collections.EMPTY_SET; - } - - @Override - public ScopeType getScope() { - return ScopeType.COMPILE; - } - - @Override - public boolean isOptional() { - return false; - } - - @Override - public PackagingType getPackaging() { - return PackagingType.JAR; - } - - @Override - public PackagingType getType() { - return PackagingType.JAR; - } - - @Override - public String getClassifier() { - return null; - } - - @Override - public String getVersion() { - return System.getProperty("project.version"); - } - - @Override - public String getGroupId() { - return "org.keycloak"; - } - - @Override - public String getArtifactId() { - return "keycloak-saml-servlet-filter-adapter"; - } - - @Override - public String toCanonicalForm() { - return getGroupId() + ":" + getArtifactId() + ":" + getVersion(); - } - - private void resolve() { - log.info("Resolving SAMLFilter dependencies"); - files = Maven.configureResolver().addDependency(this) - .resolve().withTransitivity().asFile(); - log.info("Resolving dependencies is finished with " + files.length + " files"); - } - - public File[] getDependencies() { - if (files == null) { - resolve(); - } - - return files; - } -} diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestApplicationResource.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestApplicationResource.java index 2efc26b3f7..2d277fd6e9 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestApplicationResource.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestApplicationResource.java @@ -20,7 +20,9 @@ package org.keycloak.testsuite.client.resources; import org.keycloak.representations.adapters.action.LogoutAction; import org.keycloak.representations.adapters.action.PushNotBeforeAction; import org.keycloak.representations.adapters.action.TestAvailabilityAction; +import org.keycloak.testsuite.rest.resource.TestingOIDCEndpointsApplicationResource; +import javax.ws.rs.Consumes; import javax.ws.rs.GET; import javax.ws.rs.POST; import javax.ws.rs.Path; @@ -53,4 +55,6 @@ public interface TestApplicationResource { @Path("/clear-admin-actions") Response clearAdminActions(); + @Path("/oidc-client-endpoints") + TestOIDCEndpointsApplicationResource oidcClientEndpoints(); } diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestApplicationResourceUrls.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestApplicationResourceUrls.java new file mode 100644 index 0000000000..8c5f98b6d6 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestApplicationResourceUrls.java @@ -0,0 +1,48 @@ +/* + * 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.client.resources; + +import javax.ws.rs.core.UriBuilder; + +import org.keycloak.testsuite.util.OAuthClient; + +/** + * @author Marek Posolda + */ +public class TestApplicationResourceUrls { + + private static UriBuilder oidcClientEndpoints() { + return UriBuilder.fromUri(OAuthClient.AUTH_SERVER_ROOT) + .path(TestApplicationResource.class) + .path(TestApplicationResource.class, "oidcClientEndpoints"); + } + + public static String clientRequestUri() { + UriBuilder builder = oidcClientEndpoints() + .path(TestOIDCEndpointsApplicationResource.class, "getOIDCRequest"); + + return builder.build().toString(); + } + + public static String clientJwksUri() { + UriBuilder builder = oidcClientEndpoints() + .path(TestOIDCEndpointsApplicationResource.class, "getJwks"); + + return builder.build().toString(); + } +} diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestOIDCEndpointsApplicationResource.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestOIDCEndpointsApplicationResource.java new file mode 100644 index 0000000000..54d6c3573d --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestOIDCEndpointsApplicationResource.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.testsuite.client.resources; + +import java.util.Map; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.MediaType; + +import org.keycloak.jose.jwk.JSONWebKeySet; + +/** + * @author Marek Posolda + */ +public interface TestOIDCEndpointsApplicationResource { + + @GET + @Produces(MediaType.APPLICATION_JSON) + @Path("/generate-keys") + Map generateKeys(); + + + @GET + @Produces(MediaType.APPLICATION_JSON) + @Path("/get-jwks") + JSONWebKeySet getJwks(); + + + @GET + @Path("/set-oidc-request") + @Produces(org.keycloak.utils.MediaType.APPLICATION_JWT) + void setOIDCRequest(@QueryParam("realmName") String realmName, @QueryParam("clientId") String clientId, + @QueryParam("redirectUri") String redirectUri, @QueryParam("maxAge") String maxAge, + @QueryParam("jwaAlgorithm") String jwaAlgorithm); + + @GET + @Path("/get-oidc-request") + @Produces(org.keycloak.utils.MediaType.APPLICATION_JWT) + String getOIDCRequest(); +} diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestingExportImportResource.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestingExportImportResource.java new file mode 100644 index 0000000000..27fa3604c4 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestingExportImportResource.java @@ -0,0 +1,93 @@ +/* + * 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.client.resources; + +import javax.ws.rs.Consumes; +import javax.ws.rs.GET; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +/** + * @author Marek Posolda + */ +public interface TestingExportImportResource { + + @GET + @Path("/run-import") + @Produces(MediaType.APPLICATION_JSON) + public Response runImport(); + + @GET + @Path("/run-export") + @Produces(MediaType.APPLICATION_JSON) + public Response runExport(); + + @GET + @Path("/get-users-per-file") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + public Integer getUsersPerFile(); + + @PUT + @Path("/set-users-per-file") + @Consumes(MediaType.APPLICATION_JSON) + public void setUsersPerFile(@QueryParam("usersPerFile") Integer usersPerFile); + + @GET + @Path("/get-dir") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + public String getDir(); + + @PUT + @Path("/set-dir") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + public String setDir(@QueryParam("dir") String dir); + + @PUT + @Path("/export-import-provider") + @Consumes(MediaType.APPLICATION_JSON) + public void setProvider(@QueryParam("exportImportProvider") String exportImportProvider); + + @PUT + @Path("/export-import-file") + @Consumes(MediaType.APPLICATION_JSON) + public void setFile(@QueryParam("file") String file); + + @PUT + @Path("/export-import-action") + @Consumes(MediaType.APPLICATION_JSON) + public void setAction(@QueryParam("exportImportAction") String exportImportAction); + + @PUT + @Path("/set-realm-name") + @Consumes(MediaType.APPLICATION_JSON) + public void setRealmName(@QueryParam("realmName") String realmName); + + @GET + @Path("/get-test-dir") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + public String getExportImportTestDirectory(); + +} 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 fcf5d8317a..0dbcd58f55 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 @@ -17,8 +17,8 @@ package org.keycloak.testsuite.client.resources; -import java.util.Date; import java.util.List; + import org.keycloak.representations.idm.AdminEventRepresentation; import org.keycloak.representations.idm.AuthenticationFlowRepresentation; import org.keycloak.representations.idm.EventRepresentation; @@ -37,7 +37,6 @@ import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import java.util.Map; import org.jboss.resteasy.annotations.cache.NoCache; -import org.keycloak.exportimport.ExportImportManager; /** * @author Marko Strukelj @@ -205,16 +204,6 @@ public interface TestingResource { @Path("/update-pass-through-auth-state") @Produces(MediaType.APPLICATION_JSON) AuthenticatorState updateAuthenticator(AuthenticatorState state); - - @GET - @Path("/run-import") - @Produces(MediaType.APPLICATION_JSON) - public Response runImport(); - - @GET - @Path("/run-export") - @Produces(MediaType.APPLICATION_JSON) - public Response runExport(); @GET @Path("/valid-credentials") @@ -250,53 +239,7 @@ public interface TestingResource { @Produces(MediaType.APPLICATION_JSON) public UserRepresentation getUserByServiceAccountClient(@QueryParam("realmName") String realmName, @QueryParam("clientId") String clientId); + @Path("export-import") + TestingExportImportResource exportImport(); - @GET - @Path("/get-users-per-file") - @Consumes(MediaType.APPLICATION_JSON) - @Produces(MediaType.APPLICATION_JSON) - public Integer getUsersPerFile(); - - @PUT - @Path("/set-users-per-file") - @Consumes(MediaType.APPLICATION_JSON) - public void setUsersPerFile(@QueryParam("usersPerFile") Integer usersPerFile); - - @GET - @Path("/get-dir") - @Consumes(MediaType.APPLICATION_JSON) - @Produces(MediaType.APPLICATION_JSON) - public String getDir(); - - @PUT - @Path("/set-dir") - @Consumes(MediaType.APPLICATION_JSON) - @Produces(MediaType.APPLICATION_JSON) - public String setDir(@QueryParam("dir") String dir); - - @PUT - @Path("/export-import-provider") - @Consumes(MediaType.APPLICATION_JSON) - public void setProvider(@QueryParam("exportImportProvider") String exportImportProvider); - - @PUT - @Path("/export-import-file") - @Consumes(MediaType.APPLICATION_JSON) - public void setFile(@QueryParam("file") String file); - - @PUT - @Path("/export-import-action") - @Consumes(MediaType.APPLICATION_JSON) - public void setAction(@QueryParam("exportImportAction") String exportImportAction); - - @PUT - @Path("/set-realm-name") - @Consumes(MediaType.APPLICATION_JSON) - public void setRealmName(@QueryParam("realmName") String realmName); - - @GET - @Path("/get-test-dir") - @Consumes(MediaType.APPLICATION_JSON) - @Produces(MediaType.APPLICATION_JSON) - public String getExportImportTestDirectory(); } diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/IOUtil.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/IOUtil.java index d8729149ac..89084d4b1c 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/IOUtil.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/IOUtil.java @@ -16,9 +16,11 @@ */ package org.keycloak.testsuite.util; +import org.jboss.logging.Logger; import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.util.JsonSerialization; import org.w3c.dom.Document; +import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.xml.sax.SAXException; @@ -33,7 +35,6 @@ import javax.xml.transform.dom.DOMSource; import javax.xml.transform.stream.StreamResult; import java.io.*; import java.util.concurrent.TimeUnit; -import org.jboss.logging.Logger; /** * @@ -93,18 +94,99 @@ public class IOUtil { public static void modifyDocElementAttribute(Document doc, String tagName, String attributeName, String regex, String replacement) { NodeList nodes = doc.getElementsByTagName(tagName); if (nodes.getLength() != 1) { - System.out.println("Not able to find element: " + tagName); + log.warn("Not able or ambiguous to find element: " + tagName); return; } Node node = nodes.item(0).getAttributes().getNamedItem(attributeName); if (node == null) { - System.out.println("Not able to find attribute " + attributeName + " within element: " + tagName); + log.warn("Not able to find attribute " + attributeName + " within element: " + tagName); return; } node.setTextContent(node.getTextContent().replace(regex, replacement)); } + public static void modifyDocElementValue(Document doc, String tagName, String regex, String replacement) { + NodeList nodes = doc.getElementsByTagName(tagName); + if (nodes.getLength() != 1) { + log.warn("Not able or ambiguous to find element: " + tagName); + return; + } + + Node node = nodes.item(0); + if (node == null) { + log.warn("Not able to find element: " + tagName); + return; + } + + node.setTextContent(node.getTextContent().replace(regex, replacement)); + } + + public static void removeElementFromDoc(Document doc, String parentTag, String removeNode) { + NodeList nodes = doc.getElementsByTagName(parentTag); + if (nodes.getLength() != 1) { + log.warn("Not able or ambiguous to find element: " + parentTag); + return; + } + + Element parentElement = (Element) nodes.item(0); + if (parentElement == null) { + log.warn("Not able to find element: " + parentTag); + return; + } + + NodeList removeNodes = parentElement.getElementsByTagName(removeNode); + if (removeNodes.getLength() != 1) { + log.warn("Not able or ambiguous to find element: " + removeNode + " within node " + parentTag); + return; + } + + Element removeElement = (Element) removeNodes.item(0); + if (removeElement == null) { + log.warn("Not able to find element: " + removeNode + " within node " + parentTag); + return; + } + + parentElement.removeChild(removeElement); + } + + public static String getElementTextContent(Document doc, String path) { + String[] pathSegments = path.split("/"); + + Element currentElement = (Element) doc.getElementsByTagName(pathSegments[0]).item(0); + if (currentElement == null) { + log.warn("Not able to find element: " + pathSegments[0] + " in document"); + return null; + } + + for (int i = 1; i < pathSegments.length; i++) { + currentElement = (Element) currentElement.getElementsByTagName(pathSegments[i]).item(0); + + if (currentElement == null) { + log.warn("Not able to find element: " + pathSegments[i] + " in " + pathSegments[i - 1]); + return null; + } + } + + return currentElement.getTextContent(); + } + + public static void appendChildInDocument(Document doc, String parentTag, Element node) { + NodeList nodes = doc.getElementsByTagName(parentTag); + if (nodes.getLength() != 1) { + log.warn("Not able or ambiguous to find element: " + parentTag); + return; + } + + Element parentElement = (Element) nodes.item(0); + if (parentElement == null) { + log.warn("Not able to find element: " + parentTag); + return; + } + + parentElement.appendChild(node); + } + public static void execCommand(String command, File dir) throws IOException, InterruptedException { Process process = Runtime.getRuntime().exec(command, null, dir); if (process.waitFor(10, TimeUnit.SECONDS)) { diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java index 586351d1fd..2253828bd5 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java @@ -105,6 +105,10 @@ public class OAuthClient { private String nonce; + private String request; + + private String requestUri; + private Map publicKeys = new HashMap<>(); public void init(Keycloak adminClient, WebDriver driver) { @@ -121,6 +125,9 @@ public class OAuthClient { clientSessionState = null; clientSessionHost = null; maxAge = null; + nonce = null; + request = null; + requestUri = null; } public AuthorizationEndpointResponse doLogin(String username, String password) { @@ -536,6 +543,12 @@ public class OAuthClient { if (maxAge != null) { b.queryParam(OIDCLoginProtocol.MAX_AGE_PARAM, maxAge); } + if (request != null) { + b.queryParam(OIDCLoginProtocol.REQUEST_PARAM, request); + } + if (requestUri != null) { + b.queryParam(OIDCLoginProtocol.REQUEST_URI_PARAM, requestUri); + } return b.build(realm).toString(); } @@ -644,6 +657,16 @@ public class OAuthClient { return this; } + public OAuthClient request(String request) { + this.request = request; + return this; + } + + public OAuthClient requestUri(String requestUri) { + this.requestUri = requestUri; + return this; + } + public String getRealm() { return realm; } diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/UserInfoClientUtil.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/UserInfoClientUtil.java index dd6a3db2c9..b5af7c9012 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/UserInfoClientUtil.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/UserInfoClientUtil.java @@ -28,6 +28,7 @@ import javax.ws.rs.core.UriBuilder; import org.junit.Assert; import org.keycloak.protocol.oidc.OIDCLoginProtocolService; import org.keycloak.representations.UserInfo; +import org.keycloak.utils.MediaType; /** * @author Marek Posolda @@ -51,6 +52,7 @@ public class UserInfoClientUtil { public static void testSuccessfulUserInfoResponse(Response response, String expectedUsername, String expectedEmail) { Assert.assertEquals(Response.Status.OK.getStatusCode(), response.getStatus()); + Assert.assertEquals(response.getHeaderString(HttpHeaders.CONTENT_TYPE), MediaType.APPLICATION_JSON); UserInfo userInfo = response.readEntity(UserInfo.class); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/AbstractAdapterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/AbstractAdapterTest.java index 0ef7215866..63821b3bcc 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/AbstractAdapterTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/AbstractAdapterTest.java @@ -31,6 +31,7 @@ import java.io.IOException; import java.net.URL; import java.util.ArrayList; import java.util.List; +import java.util.Map; /** * @@ -63,10 +64,12 @@ public abstract class AbstractAdapterTest extends AbstractAuthTest { modifyClientUrls(tr, appServerContextRootPage.toString(), ""); modifyClientWebOrigins(tr, "8080", System.getProperty("auth.server.http.port", null)); modifySamlMasterURLs(tr, "/", "http://localhost:" + System.getProperty("auth.server.http.port", null) + "/"); + modifySAMLClientsAttributes(tr, "8080", System.getProperty("auth.server.http.port", "8180")); } else { modifyClientRedirectUris(tr, "^(/.*/\\*)", appServerContextRootPage.toString() + "$1"); modifyClientUrls(tr, "^(/.*)", appServerContextRootPage.toString() + "$1"); modifySamlMasterURLs(tr, "8080", System.getProperty("auth.server.http.port", null)); + modifySAMLClientsAttributes(tr, "8080", System.getProperty("app.server.http.port", "8280")); } if ("true".equals(System.getProperty("auth.server.ssl.required"))) { tr.setSslRequired("all"); @@ -125,6 +128,19 @@ public abstract class AbstractAdapterTest extends AbstractAuthTest { } } + protected void modifySAMLClientsAttributes(RealmRepresentation realm, String regex, String replacement) { + if (realm.getClients() != null) { + for (ClientRepresentation client : realm.getClients()) { + if (client.getProtocol() != null && client.getProtocol().equals("saml")) { + log.info("Modifying attributes of SAML client: " + client.getClientId()); + for (Map.Entry entry : client.getAttributes().entrySet()) { + client.getAttributes().put(entry.getKey(), entry.getValue().replaceAll(regex, replacement)); + } + } + } + } + } + protected void modifySamlMasterURLs(RealmRepresentation realm, String regex, String replacement) { if (realm.getClients() != null) { for (ClientRepresentation client : realm.getClients()) { diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/AbstractServletsAdapterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/AbstractServletsAdapterTest.java index badf51cd77..127c863fa8 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/AbstractServletsAdapterTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/AbstractServletsAdapterTest.java @@ -25,12 +25,11 @@ import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.testsuite.util.WaitUtils; import org.openqa.selenium.By; +import javax.ws.rs.core.UriBuilder; import java.io.IOException; import java.net.URL; import java.util.List; -import javax.ws.rs.core.UriBuilder; - import static org.keycloak.testsuite.auth.page.AuthRealm.DEMO; import static org.keycloak.testsuite.util.IOUtil.loadRealm; @@ -66,15 +65,15 @@ public abstract class AbstractServletsAdapterTest extends AbstractAdapterTest { } protected static WebArchive samlServletDeployment(String name, Class... servletClasses) { - return samlServletDeployment(name, "keycloak-saml.xml", servletClasses); + return samlServletDeployment(name, "web.xml", servletClasses); } - protected static WebArchive samlServletDeployment(String name, String adapterConfig ,Class... servletClasses) { + protected static WebArchive samlServletDeployment(String name, String webXMLPath, Class... servletClasses) { String baseSAMLPath = "/adapter-test/keycloak-saml/"; String webInfPath = baseSAMLPath + name + "/WEB-INF/"; - URL keycloakSAMLConfig = AbstractServletsAdapterTest.class.getResource(webInfPath + adapterConfig); - URL webXML = AbstractServletsAdapterTest.class.getResource(baseSAMLPath + "web.xml"); + URL keycloakSAMLConfig = AbstractServletsAdapterTest.class.getResource(webInfPath + "keycloak-saml.xml"); + URL webXML = AbstractServletsAdapterTest.class.getResource(baseSAMLPath + webXMLPath); WebArchive deployment = ShrinkWrap.create(WebArchive.class, name + ".war") .addClasses(servletClasses) diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/EnforcerConfigTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/EnforcerConfigTest.java new file mode 100644 index 0000000000..4236b2e14f --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/EnforcerConfigTest.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.testsuite.adapter.example.authorization; + +import org.junit.Assert; +import org.junit.Test; +import org.keycloak.adapters.KeycloakDeployment; +import org.keycloak.adapters.KeycloakDeploymentBuilder; +import org.keycloak.adapters.authorization.PolicyEnforcer; +import org.keycloak.representations.adapters.config.PolicyEnforcerConfig; +import org.keycloak.representations.idm.RealmRepresentation; +import org.keycloak.testsuite.AbstractKeycloakTest; + +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.keycloak.testsuite.util.IOUtil.loadRealm; + +/** + * @author Pedro Igor + */ +public class EnforcerConfigTest extends AbstractKeycloakTest { + + @Override + public void addTestRealms(List testRealms) { + RealmRepresentation realm = loadRealm(getClass().getResourceAsStream("/authorization-test/test-authz-realm.json")); + testRealms.add(realm); + } + + @Test + public void testMultiplePathsWithSameName() throws Exception{ + KeycloakDeployment deployment = KeycloakDeploymentBuilder.build(getClass().getResourceAsStream("/authorization-test/enforcer-config-paths-same-name.json")); + PolicyEnforcer policyEnforcer = deployment.getPolicyEnforcer(); + List paths = policyEnforcer.getPaths(); + assertEquals(1, paths.size()); + assertEquals(4, paths.get(0).getMethods().size()); + } +} diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractSAMLFilterServletAdapterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractSAMLFilterServletAdapterTest.java index 0df65a4f6c..268285a5b4 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractSAMLFilterServletAdapterTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractSAMLFilterServletAdapterTest.java @@ -2,6 +2,8 @@ package org.keycloak.testsuite.adapter.servlet; import org.junit.After; import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; import org.keycloak.testsuite.arquillian.annotation.UseServletFilter; /** @@ -25,11 +27,12 @@ public abstract class AbstractSAMLFilterServletAdapterTest extends AbstractSAMLS salesPostSigEmailServletPage.checkRoles(true); salesPostSigPersistentServletPage.checkRoles(true); salesPostSigTransientServletPage.checkRoles(true); - employee2ServletPage.navigateTo(); + salesPostAssertionAndResponseSigPage.checkRoles(true); //using endpoint instead of query param because we are not able to put query param to IDP initiated login + employee2ServletPage.navigateTo(); testRealmLoginPage.form().login(bburkeUser); - employee2ServletPage.checkRolesEndPoint(); + employee2ServletPage.checkRolesEndPoint(true); employee2ServletPage.logout(); forbiddenIfNotAuthenticated = false; @@ -51,4 +54,18 @@ public abstract class AbstractSAMLFilterServletAdapterTest extends AbstractSAMLS salesPostSigPersistentServletPage.checkRoles(false); salesPostSigTransientServletPage.checkRoles(false); } + + @Test + @Override + @Ignore + public void testSavedPostRequest() { + + } + + @Test + @Override + @Ignore + public void testErrorHandling() { + + } } 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 ddc23b7a1d..2973caa1dc 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 @@ -20,11 +20,19 @@ package org.keycloak.testsuite.adapter.servlet; import org.jboss.arquillian.container.test.api.Deployment; import org.jboss.arquillian.graphene.page.Page; import org.jboss.shrinkwrap.api.spec.WebArchive; +import org.junit.Assert; import org.junit.Test; import org.keycloak.admin.client.resource.ClientResource; +import org.keycloak.admin.client.resource.ProtocolMappersResource; +import org.keycloak.protocol.saml.mappers.AttributeStatementHelper; +import org.keycloak.protocol.saml.mappers.RoleListMapper; import org.keycloak.representations.idm.ClientRepresentation; +import org.keycloak.representations.idm.ProtocolMapperRepresentation; import org.keycloak.representations.idm.RealmRepresentation; 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.testsuite.adapter.AbstractServletsAdapterTest; import org.keycloak.testsuite.adapter.page.*; import org.keycloak.testsuite.admin.ApiUtil; @@ -35,8 +43,15 @@ import org.keycloak.testsuite.util.IOUtil; import org.openqa.selenium.By; import org.w3c.dom.Document; +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.client.Entity; +import javax.ws.rs.core.Form; import javax.ws.rs.core.Response; +import java.net.URI; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; @@ -70,6 +85,9 @@ public abstract class AbstractSAMLServletsAdapterTest extends AbstractServletsAd @Page protected SalesPostServlet salesPostServletPage; + @Page + private SalesPost2Servlet salesPost2ServletPage; + @Page protected SalesPostEncServlet salesPostEncServletPage; @@ -93,6 +111,26 @@ public abstract class AbstractSAMLServletsAdapterTest extends AbstractServletsAd protected boolean forbiddenIfNotAuthenticated = true; + @Page + protected SalesPostAssertionAndResponseSig salesPostAssertionAndResponseSigPage; + + @Page + protected BadAssertionSalesPostSig badAssertionSalesPostSigPage; + + @Page + protected MissingAssertionSig missingAssertionSigPage; + + @Page + protected EmployeeServlet employeeServletPage; + + @Page + private InputPortal inputPortalPage; + + @Page + private SAMLIDPInitiatedLogin samlidpInitiatedLoginPage; + + public static final String FORBIDDEN_TEXT = "HTTP status code: 403"; + @Deployment(name = BadClientSalesPostSigServlet.DEPLOYMENT_NAME) protected static WebArchive badClientSalesPostSig() { return samlServletDeployment(BadClientSalesPostSigServlet.DEPLOYMENT_NAME, SendUsernameServlet.class); @@ -158,6 +196,36 @@ public abstract class AbstractSAMLServletsAdapterTest extends AbstractServletsAd return samlServletDeployment(SalesPostSigTransientServlet.DEPLOYMENT_NAME, SendUsernameServlet.class); } + @Deployment(name = InputPortal.DEPLOYMENT_NAME) + protected static WebArchive inputPortal() { + return samlServletDeployment(InputPortal.DEPLOYMENT_NAME, "input-portal/WEB-INF/web.xml" , InputServlet.class); + } + + @Deployment(name = SalesPost2Servlet.DEPLOYMENT_NAME) + protected static WebArchive salesPost2() { + return samlServletDeployment(SalesPost2Servlet.DEPLOYMENT_NAME, SendUsernameServlet.class); + } + + @Deployment(name = SalesPostAssertionAndResponseSig.DEPLOYMENT_NAME) + protected static WebArchive salesPostAssertionAndResponseSig() { + return samlServletDeployment(SalesPostAssertionAndResponseSig.DEPLOYMENT_NAME, SendUsernameServlet.class); + } + + @Deployment(name = BadAssertionSalesPostSig.DEPLOYMENT_NAME) + protected static WebArchive badAssertionSalesPostSig() { + return samlServletDeployment(BadAssertionSalesPostSig.DEPLOYMENT_NAME, SendUsernameServlet.class); + } + + @Deployment(name = MissingAssertionSig.DEPLOYMENT_NAME) + protected static WebArchive missingAssertionSig() { + return samlServletDeployment(MissingAssertionSig.DEPLOYMENT_NAME, SendUsernameServlet.class); + } + + @Deployment(name = EmployeeServlet.DEPLOYMENT_NAME) + protected static WebArchive employeeServlet() { + return samlServletDeployment(EmployeeServlet.DEPLOYMENT_NAME, "employee/WEB-INF/web.xml", SamlSPFacade.class); + } + @Override public void addAdapterTestRealms(List testRealms) { testRealms.add(loadRealm("/adapter-test/keycloak-saml/testsaml.json")); @@ -171,38 +239,54 @@ public abstract class AbstractSAMLServletsAdapterTest extends AbstractServletsAd testRealmSAMLPostLoginPage.setAuthRealm(SAMLSERVLETDEMO); } - private void assertForbidden(AbstractPage page) { + private void assertForbidden(AbstractPage page, String expectedNotContains) { page.navigateTo(); - waitUntilElement(By.xpath("//body")).text().not().contains("principal="); - assertTrue(driver.getPageSource().contains("Forbidden") || driver.getPageSource().contains("Status 403")); + waitUntilElement(By.xpath("//body")).text().not().contains(expectedNotContains); + assertTrue(driver.getPageSource().contains("Forbidden") || driver.getPageSource().contains(FORBIDDEN_TEXT)); } - private void assertSuccessfullyLoggedIn(AbstractPage page) { + private void assertSuccessfullyLoggedIn(AbstractPage page, String expectedText) { page.navigateTo(); - waitUntilElement(By.xpath("//body")).text().contains("principal=bburke"); + waitUntilElement(By.xpath("//body")).text().contains(expectedText); } - private void assertForbiddenLogin(AbstractPage page, String username, String password, Login loginPage) { + private void assertForbiddenLogin(AbstractPage page, String username, String password, Login loginPage, String expectedNotContains) { page.navigateTo(); assertCurrentUrlStartsWith(loginPage); loginPage.form().login(username, password); - waitUntilElement(By.xpath("//body")).text().not().contains("principal="); + waitUntilElement(By.xpath("//body")).text().not().contains(expectedNotContains); //Different 403 status page on EAP and Wildfly - assertTrue(driver.getPageSource().contains("Forbidden") || driver.getPageSource().contains("Status 403")); + assertTrue(driver.getPageSource().contains("Forbidden") || driver.getPageSource().contains(FORBIDDEN_TEXT)); } - private void assertSuccessfulLogin(AbstractPage page, UserRepresentation user, Login loginPage) { + private void assertSuccessfulLogin(AbstractPage page, UserRepresentation user, Login loginPage, String expectedString) { page.navigateTo(); assertCurrentUrlStartsWith(loginPage); loginPage.form().login(user); - waitUntilElement(By.xpath("//body")).text().contains("principal=bburke"); + waitUntilElement(By.xpath("//body")).text().contains(expectedString); } private void testSuccessfulAndUnauthorizedLogin(SAMLServlet page, Login loginPage) { - assertSuccessfulLogin(page, bburkeUser, loginPage); + testSuccessfulAndUnauthorizedLogin(page, loginPage, "principal=bburke"); + } + + private void testSuccessfulAndUnauthorizedLogin(SAMLServlet page, Login loginPage, String expectedText) { + testSuccessfulAndUnauthorizedLogin(page, loginPage, expectedText, "principal="); + } + + private void testSuccessfulAndUnauthorizedLogin(SAMLServlet page, Login loginPage, String expectedText, String expectedNotContains) { + assertSuccessfulLogin(page, bburkeUser, loginPage, expectedText); page.logout(); - assertForbiddenLogin(page, "unauthorized", "password", loginPage); + checkLoggedOut(page, loginPage); + assertForbiddenLogin(page, "unauthorized", "password", loginPage, expectedNotContains); page.logout(); + checkLoggedOut(page, loginPage); + } + + private void checkLoggedOut(AbstractPage page, Login loginPage) { + page.navigateTo(); + waitUntilElement(By.xpath("//body")).is().present(); + assertCurrentUrlStartsWith(loginPage); } @Test @@ -221,38 +305,35 @@ public abstract class AbstractSAMLServletsAdapterTest extends AbstractServletsAd @Test public void unauthorizedSSOTest() { - assertForbiddenLogin(salesPostServletPage, "unauthorized", "password", testRealmSAMLPostLoginPage); - assertForbidden(employee2ServletPage); - assertForbidden(employeeSigFrontServletPage); - assertForbidden(salesPostSigPersistentServletPage); + assertForbiddenLogin(salesPostServletPage, "unauthorized", "password", testRealmSAMLPostLoginPage, "principal="); + assertForbidden(employee2ServletPage, "principal="); + assertForbidden(employeeSigFrontServletPage, "principal="); + assertForbidden(salesPostSigPersistentServletPage, "principal="); salesPostServletPage.logout(); + checkLoggedOut(salesPostServletPage, testRealmSAMLPostLoginPage); } @Test public void singleLoginAndLogoutSAMLTest() { - assertSuccessfulLogin(salesPostServletPage, bburkeUser, testRealmSAMLPostLoginPage); - assertSuccessfullyLoggedIn(salesPostSigServletPage); - assertSuccessfullyLoggedIn(employee2ServletPage); - assertSuccessfullyLoggedIn(salesPostEncServletPage); + assertSuccessfulLogin(salesPostServletPage, bburkeUser, testRealmSAMLPostLoginPage, "principal=bburke"); + assertSuccessfullyLoggedIn(salesPostSigServletPage, "principal=bburke"); + assertSuccessfullyLoggedIn(employee2ServletPage, "principal=bburke"); + assertSuccessfullyLoggedIn(salesPostEncServletPage, "principal=bburke"); employeeSigFrontServletPage.logout(); - employeeSigFrontServletPage.navigateTo(); - assertCurrentUrlStartsWith(testRealmSAMLRedirectLoginPage); - - employeeSigServletPage.navigateTo(); - assertCurrentUrlStartsWith(testRealmSAMLRedirectLoginPage); + checkLoggedOut(employeeSigFrontServletPage, testRealmSAMLRedirectLoginPage); + checkLoggedOut(employeeSigServletPage, testRealmSAMLRedirectLoginPage); salesPostPassiveServletPage.navigateTo(); if (forbiddenIfNotAuthenticated) { waitUntilElement(By.xpath("//body")).text().not().contains("principal="); - assertTrue(driver.getPageSource().contains("Forbidden") || driver.getPageSource().contains("") || driver.getPageSource().equals("")); + assertTrue(driver.getPageSource().contains("Forbidden") || driver.getPageSource().contains(FORBIDDEN_TEXT)); } else { waitUntilElement(By.xpath("//body")).text().contains("principal=null"); } - salesPostSigEmailServletPage.navigateTo(); - assertCurrentUrlStartsWith(testRealmSAMLPostLoginPage); + checkLoggedOut(salesPostSigEmailServletPage, testRealmSAMLPostLoginPage); } @Test @@ -268,7 +349,7 @@ public abstract class AbstractSAMLServletsAdapterTest extends AbstractServletsAd waitUntilElement(By.xpath("//body")).text().not().contains("principal="); //Different 403 status page on EAP and Wildfly - assertTrue(driver.getPageSource().contains("Forbidden") || driver.getPageSource().contains("Status 403")); + assertTrue(driver.getPageSource().contains("Forbidden") || driver.getPageSource().contains(FORBIDDEN_TEXT)); } @Test @@ -328,14 +409,14 @@ public abstract class AbstractSAMLServletsAdapterTest extends AbstractServletsAd if (forbiddenIfNotAuthenticated) { waitUntilElement(By.xpath("//body")).text().not().contains("principal="); //Different 403 status page on EAP and Wildfly - assertTrue(driver.getPageSource().contains("Forbidden") || driver.getPageSource().contains("") || driver.getPageSource().equals("")); + assertTrue(driver.getPageSource().contains("Forbidden") || driver.getPageSource().contains(FORBIDDEN_TEXT)); } else { waitUntilElement(By.xpath("//body")).text().contains("principal=null"); } - assertSuccessfulLogin(salesPostServletPage, bburkeUser, testRealmSAMLPostLoginPage); + assertSuccessfulLogin(salesPostServletPage, bburkeUser, testRealmSAMLPostLoginPage, "principal=bburke"); - assertSuccessfullyLoggedIn(salesPostPassiveServletPage); + assertSuccessfullyLoggedIn(salesPostPassiveServletPage, "principal=bburke"); salesPostPassiveServletPage.logout(); salesPostPassiveServletPage.navigateTo(); @@ -343,12 +424,13 @@ public abstract class AbstractSAMLServletsAdapterTest extends AbstractServletsAd if (forbiddenIfNotAuthenticated) { waitUntilElement(By.xpath("//body")).text().not().contains("principal="); //Different 403 status page on EAP and Wildfly - assertTrue(driver.getPageSource().contains("Forbidden") || driver.getPageSource().contains("") || driver.getPageSource().equals("")); + assertTrue(driver.getPageSource().contains("Forbidden") || driver.getPageSource().contains(FORBIDDEN_TEXT)); } else { waitUntilElement(By.xpath("//body")).text().contains("principal=null"); } - assertForbiddenLogin(salesPostServletPage, "unauthorized", "password", testRealmSAMLPostLoginPage); - assertForbidden(salesPostPassiveServletPage); + + assertForbiddenLogin(salesPostServletPage, "unauthorized", "password", testRealmSAMLPostLoginPage, "principal="); + assertForbidden(salesPostPassiveServletPage, "principal="); salesPostPassiveServletPage.logout(); } @@ -360,7 +442,7 @@ public abstract class AbstractSAMLServletsAdapterTest extends AbstractServletsAd @Test public void salesPostSigEmailTest() { - testSuccessfulAndUnauthorizedLogin(salesPostSigEmailServletPage, testRealmSAMLPostLoginPage); + testSuccessfulAndUnauthorizedLogin(salesPostSigEmailServletPage, testRealmSAMLPostLoginPage, "principal=bburke@redhat.com"); } @Test @@ -371,9 +453,11 @@ public abstract class AbstractSAMLServletsAdapterTest extends AbstractServletsAd waitUntilElement(By.xpath("//body")).text().contains("principal=G-"); salesPostSigPersistentServletPage.logout(); + checkLoggedOut(salesPostSigPersistentServletPage, testRealmSAMLPostLoginPage); - assertForbiddenLogin(salesPostSigPersistentServletPage, "unauthorized", "password", testRealmSAMLPostLoginPage); + assertForbiddenLogin(salesPostSigPersistentServletPage, "unauthorized", "password", testRealmSAMLPostLoginPage, "principal="); salesPostSigPersistentServletPage.logout(); + checkLoggedOut(salesPostSigPersistentServletPage, testRealmSAMLPostLoginPage); } @Test @@ -384,36 +468,260 @@ public abstract class AbstractSAMLServletsAdapterTest extends AbstractServletsAd waitUntilElement(By.xpath("//body")).text().contains("principal=G-"); salesPostSigTransientServletPage.logout(); + checkLoggedOut(salesPostSigTransientServletPage, testRealmSAMLPostLoginPage); - assertForbiddenLogin(salesPostSigTransientServletPage, "unauthorized", "password", testRealmSAMLPostLoginPage); + assertForbiddenLogin(salesPostSigTransientServletPage, "unauthorized", "password", testRealmSAMLPostLoginPage, "principal="); salesPostSigTransientServletPage.logout(); + checkLoggedOut(salesPostSigTransientServletPage, testRealmSAMLPostLoginPage); } @Test public void idpInitiatedLogin() { - samlidpInitiatedLogin.setAuthRealm(SAMLSERVLETDEMO); - samlidpInitiatedLogin.setUrlName("employee2"); - samlidpInitiatedLogin.navigateTo(); - samlidpInitiatedLogin.form().login(bburkeUser); + samlidpInitiatedLoginPage.setAuthRealm(SAMLSERVLETDEMO); + samlidpInitiatedLoginPage.setUrlName("employee2"); + samlidpInitiatedLoginPage.navigateTo(); + samlidpInitiatedLoginPage.form().login(bburkeUser); waitUntilElement(By.xpath("//body")).text().contains("principal=bburke"); - assertSuccessfullyLoggedIn(salesPostSigServletPage); + assertSuccessfullyLoggedIn(salesPostSigServletPage, "principal=bburke"); employee2ServletPage.logout(); + checkLoggedOut(employee2ServletPage, testRealmSAMLPostLoginPage); } @Test public void idpInitiatedUnauthorizedLoginTest() { - samlidpInitiatedLogin.setAuthRealm(SAMLSERVLETDEMO); - samlidpInitiatedLogin.setUrlName("employee2"); - samlidpInitiatedLogin.navigateTo(); - samlidpInitiatedLogin.form().login("unauthorized", "password"); + samlidpInitiatedLoginPage.setAuthRealm(SAMLSERVLETDEMO); + samlidpInitiatedLoginPage.setUrlName("employee2"); + samlidpInitiatedLoginPage.navigateTo(); + samlidpInitiatedLoginPage.form().login("unauthorized", "password"); waitUntilElement(By.xpath("//body")).text().not().contains("bburke"); - assertTrue(driver.getPageSource().contains("Forbidden") || driver.getPageSource().contains("Status 403")); + assertTrue(driver.getPageSource().contains("Forbidden") || driver.getPageSource().contains(FORBIDDEN_TEXT)); - assertForbidden(employee2ServletPage); + assertForbidden(employee2ServletPage, "principal="); + employee2ServletPage.logout(); + checkLoggedOut(employee2ServletPage, testRealmSAMLPostLoginPage); + } + + @Test + public void testSavedPostRequest() { + inputPortalPage.navigateTo(); + assertCurrentUrlStartsWith(inputPortalPage); + inputPortalPage.execute("hello"); + + assertCurrentUrlStartsWith(testRealmSAMLPostLoginPage); + testRealmLoginPage.form().login("bburke@redhat.com", "password"); + Assert.assertEquals(driver.getCurrentUrl(), inputPortalPage + "/secured/post"); + waitUntilElement(By.xpath("//body")).text().contains("parameter=hello"); + + // test that user principal and KeycloakSecurityContext available + driver.navigate().to(inputPortalPage + "/insecure"); + waitUntilElement(By.xpath("//body")).text().contains("Insecure Page"); + + if (System.getProperty("insecure.user.principal.unsupported") == null) waitUntilElement(By.xpath("//body")).text().contains("UserPrincipal"); + + // test logout + + inputPortalPage.logout(); + + // test unsecured POST KEYCLOAK-901 + + Client client = ClientBuilder.newClient(); + Form form = new Form(); + form.param("parameter", "hello"); + String text = client.target(inputPortalPage + "/unsecured").request().post(Entity.form(form), String.class); + Assert.assertTrue(text.contains("parameter=hello")); + client.close(); + } + + @Test + public void testPostSimpleLoginLogoutIdpInitiatedRedirectTo() { + samlidpInitiatedLoginPage.setAuthRealm(SAMLSERVLETDEMO); + samlidpInitiatedLoginPage.setUrlName("sales-post2"); + samlidpInitiatedLoginPage.navigateTo(); + + samlidpInitiatedLoginPage.form().login(bburkeUser); + assertCurrentUrlStartsWith(salesPost2ServletPage); + assertTrue(driver.getCurrentUrl().endsWith("/foo")); + waitUntilElement(By.xpath("//body")).text().contains("principal=bburke"); + salesPost2ServletPage.logout(); + checkLoggedOut(salesPost2ServletPage, testRealmSAMLPostLoginPage); + } + + @Test + public void salesPostAssertionAndResponseSigTest() { + testSuccessfulAndUnauthorizedLogin(salesPostAssertionAndResponseSigPage, testRealmSAMLPostLoginPage); + } + + @Test + public void testPostBadAssertionSignature() { + badAssertionSalesPostSigPage.navigateTo(); + assertCurrentUrlStartsWith(testRealmSAMLPostLoginPage); + testRealmSAMLPostLoginPage.form().login("bburke", "password"); + + waitUntilElement(By.xpath("//body")).text().contains("Error info: SamlAuthenticationError [reason=INVALID_SIGNATURE, status=null]"); + assertEquals(driver.getCurrentUrl(), badAssertionSalesPostSigPage + "/saml"); + } + + @Test + public void testMissingAssertionSignature() { + missingAssertionSigPage.navigateTo(); + assertCurrentUrlStartsWith(testRealmSAMLPostLoginPage); + testRealmSAMLPostLoginPage.form().login("bburke", "password"); + + waitUntilElement(By.xpath("//body")).text().contains("Error info: SamlAuthenticationError [reason=INVALID_SIGNATURE, status=null]"); + assertEquals(driver.getCurrentUrl(), missingAssertionSigPage + "/saml"); + } + + @Test + public void testErrorHandling() throws Exception { + Client client = ClientBuilder.newClient(); + // make sure + Response response = client.target(employeeSigServletPage.toString()).request().get(); + response.close(); + SAML2ErrorResponseBuilder builder = new SAML2ErrorResponseBuilder() + .destination(employeeSigServletPage.toString() + "/saml") + .issuer("http://localhost:" + System.getProperty("auth.server.http.port", "8180") + "/realms/demo") + .status(JBossSAMLURIConstants.STATUS_REQUEST_DENIED.get()); + BaseSAML2BindingBuilder binding = new BaseSAML2BindingBuilder() + .relayState(null); + Document document = builder.buildDocument(); + URI uri = binding.redirectBinding(document).generateURI(employeeSigServletPage.toString() + "/saml", false); + response = client.target(uri).request().get(); + String errorPage = response.readEntity(String.class); + response.close(); + Assert.assertTrue(errorPage.contains("Error info: SamlAuthenticationError [reason=ERROR_STATUS")); + Assert.assertFalse(errorPage.contains("status=null")); + client.close(); + } + + @Test + public void testRelayStateEncoding() throws Exception { + // this test has a hardcoded SAMLRequest and we hack a SP face servlet to get the SAMLResponse so we can look + // at the relay state + employeeServletPage.navigateTo(); + assertCurrentUrlStartsWith(testRealmSAMLPostLoginPage); + testRealmSAMLPostLoginPage.form().login("bburke", "password"); + assertCurrentUrlStartsWith(employeeServletPage); + waitUntilElement(By.xpath("//body")).text().contains("Relay state: " + SamlSPFacade.RELAY_STATE); + waitUntilElement(By.xpath("//body")).text().not().contains("SAML response: null"); + } + + @Test + public void testAttributes() throws Exception { + ClientResource clientResource = ApiUtil.findClientResourceByClientId(testRealmResource(), "http://localhost:8081/employee2/"); + ProtocolMappersResource protocolMappersResource = clientResource.getProtocolMappers(); + + Map config = new LinkedHashMap<>(); + config.put("attribute.nameformat", "Basic"); + config.put("user.attribute", "topAttribute"); + config.put("attribute.name", "topAttribute"); + createProtocolMapper(protocolMappersResource, "topAttribute", "saml", "saml-user-attribute-mapper", config); + + config = new LinkedHashMap<>(); + config.put("attribute.nameformat", "Basic"); + config.put("user.attribute", "level2Attribute"); + config.put("attribute.name", "level2Attribute"); + createProtocolMapper(protocolMappersResource, "level2Attribute", "saml", "saml-user-attribute-mapper", config); + + config = new LinkedHashMap<>(); + config.put("attribute.nameformat", "Basic"); + config.put("single", "true"); + config.put("attribute.name", "group"); + createProtocolMapper(protocolMappersResource, "groups", "saml", "saml-group-membership-mapper", config); + + setRolesToCheck("manager,user"); + + employee2ServletPage.navigateTo(); + assertCurrentUrlStartsWith(testRealmSAMLPostLoginPage); + testRealmSAMLPostLoginPage.form().login("level2GroupUser", "password"); + + driver.navigate().to(employee2ServletPage.toString() + "/getAttributes"); + waitUntilElement(By.xpath("//body")).text().contains("topAttribute: true"); + waitUntilElement(By.xpath("//body")).text().contains("level2Attribute: true"); + waitUntilElement(By.xpath("//body")).text().contains("attribute email: level2@redhat.com"); + waitUntilElement(By.xpath("//body")).text().not().contains("group: []"); + waitUntilElement(By.xpath("//body")).text().not().contains("group: null"); + waitUntilElement(By.xpath("//body")).text().contains("group: [level2]"); + + employee2ServletPage.logout(); + checkLoggedOut(employee2ServletPage, testRealmSAMLPostLoginPage); + + setRolesToCheck("manager,employee,user"); + + employee2ServletPage.navigateTo(); + assertCurrentUrlStartsWith(testRealmSAMLPostLoginPage); + testRealmSAMLPostLoginPage.form().login(bburkeUser); + + driver.navigate().to(employee2ServletPage.toString() + "/getAttributes"); + waitUntilElement(By.xpath("//body")).text().contains("attribute email: bburke@redhat.com"); + waitUntilElement(By.xpath("//body")).text().contains("friendlyAttribute email: bburke@redhat.com"); + waitUntilElement(By.xpath("//body")).text().contains("phone: 617"); + waitUntilElement(By.xpath("//body")).text().contains("friendlyAttribute phone: null"); + + employee2ServletPage.logout(); + checkLoggedOut(employee2ServletPage, testRealmSAMLPostLoginPage); + + config = new LinkedHashMap<>(); + config.put("attribute.value", "hard"); + config.put("attribute.nameformat", "Basic"); + config.put("attribute.name", "hardcoded-attribute"); + createProtocolMapper(protocolMappersResource, "hardcoded-attribute", "saml", "saml-hardcode-attribute-mapper", config); + + config = new LinkedHashMap<>(); + config.put("role", "hardcoded-role"); + createProtocolMapper(protocolMappersResource, "hardcoded-role", "saml", "saml-hardcode-role-mapper", config); + + config = new LinkedHashMap<>(); + config.put("new.role.name", "pee-on"); + config.put("role", "http://localhost:8081/employee/.employee"); + createProtocolMapper(protocolMappersResource, "renamed-employee-role", "saml", "saml-role-name-mapper", config); + + for (ProtocolMapperRepresentation mapper : clientResource.toRepresentation().getProtocolMappers()) { + if (mapper.getName().equals("role-list")) { + protocolMappersResource.delete(mapper.getId()); + + mapper.setId(null); + mapper.getConfig().put(RoleListMapper.SINGLE_ROLE_ATTRIBUTE, "true"); + mapper.getConfig().put(AttributeStatementHelper.SAML_ATTRIBUTE_NAME, "memberOf"); + protocolMappersResource.createMapper(mapper); + } + } + + setRolesToCheck("pee-on,el-jefe,manager,hardcoded-role"); + + config = new LinkedHashMap<>(); + config.put("new.role.name", "el-jefe"); + config.put("role", "user"); + createProtocolMapper(protocolMappersResource, "renamed-role", "saml", "saml-role-name-mapper", config); + + employee2ServletPage.navigateTo(); + assertCurrentUrlStartsWith(testRealmSAMLPostLoginPage); + testRealmSAMLPostLoginPage.form().login(bburkeUser); + + driver.navigate().to(employee2ServletPage.toString() + "/getAttributes"); + waitUntilElement(By.xpath("//body")).text().contains("hardcoded-attribute: hard"); + employee2ServletPage.checkRolesEndPoint(false); + employee2ServletPage.logout(); + checkLoggedOut(employee2ServletPage, testRealmSAMLPostLoginPage); + } + + private void createProtocolMapper(ProtocolMappersResource resource, String name, String protocol, String protocolMapper, Map config) { + ProtocolMapperRepresentation representation = new ProtocolMapperRepresentation(); + representation.setName(name); + representation.setProtocol(protocol); + representation.setProtocolMapper(protocolMapper); + representation.setConfig(config); + resource.createMapper(representation); + } + + private void setRolesToCheck(String roles) { + employee2ServletPage.navigateTo(); + assertCurrentUrlStartsWith(testRealmSAMLPostLoginPage); + testRealmSAMLPostLoginPage.form().login(bburkeUser); + driver.navigate().to(employee2ServletPage.toString() + "/setCheckRoles?roles=" + roles); employee2ServletPage.logout(); } } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/ClientTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/ClientTest.java index ee06750e93..b6f9bdea8b 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/ClientTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/ClientTest.java @@ -33,16 +33,14 @@ import org.keycloak.representations.adapters.action.PushNotBeforeAction; import org.keycloak.representations.adapters.action.TestAvailabilityAction; import org.keycloak.representations.idm.*; +import javax.ws.rs.BadRequestException; import javax.ws.rs.NotFoundException; import javax.ws.rs.core.Response; import java.io.IOException; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; +import java.util.*; +import org.keycloak.services.ErrorResponseException; import org.keycloak.testsuite.Assert; import org.keycloak.testsuite.util.AdminEventPaths; import org.keycloak.testsuite.util.ClientBuilder; @@ -212,6 +210,52 @@ public class ClientTest extends AbstractAdminTest { assertEquals("service-account-serviceclient", userRep.getUsername()); } + // KEYCLOAK-3421 + @Test + public void createClientWithFragments() { + ClientRepresentation client = ClientBuilder.create() + .clientId("client-with-fragment") + .rootUrl("http://localhost/base#someFragment") + .redirectUris("http://localhost/auth", "http://localhost/auth#fragment", "http://localhost/auth*", "/relative") + .build(); + + Response response = realm.clients().create(client); + assertUriFragmentError(response); + } + + // KEYCLOAK-3421 + @Test + public void updateClientWithFragments() { + ClientRepresentation client = ClientBuilder.create() + .clientId("client-with-fragment") + .redirectUris("http://localhost/auth", "http://localhost/auth*") + .build(); + Response response = realm.clients().create(client); + ClientResource clientResource = realm.clients().get(ApiUtil.getCreatedId(response)); + + client = clientResource.toRepresentation(); + client.setRootUrl("http://localhost/base#someFragment"); + List redirectUris = client.getRedirectUris(); + redirectUris.add("http://localhost/auth#fragment"); + redirectUris.add("/relative"); + client.setRedirectUris(redirectUris); + + try { + clientResource.update(client); + fail("Should fail"); + } + catch (BadRequestException e) { + assertUriFragmentError(e.getResponse()); + } + } + + private void assertUriFragmentError(Response response) { + assertEquals(response.getStatus(), 400); + String error = response.readEntity(OAuth2ErrorRepresentation.class).getError(); + assertTrue("Error response doesn't mention Redirect URIs fragments", error.contains("Redirect URIs must not contain an URI fragment")); + assertTrue("Error response doesn't mention Root URL fragments", error.contains("Root URL must not contain an URL fragment")); + } + @Test public void pushRevocation() { testingClient.testApp().clearAdminActions(); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/OIDCClientRegistrationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/OIDCClientRegistrationTest.java index 5d7d2c14f0..19d64135a3 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/OIDCClientRegistrationTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/OIDCClientRegistrationTest.java @@ -35,24 +35,34 @@ import org.keycloak.common.util.CollectionUtil; import org.keycloak.common.util.KeycloakUriBuilder; import org.keycloak.constants.ServiceUrlConstants; import org.keycloak.jose.jwk.JSONWebKeySet; +import org.keycloak.jose.jws.Algorithm; import org.keycloak.models.utils.KeycloakModelUtils; +import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper; import org.keycloak.protocol.oidc.OIDCLoginProtocol; +import org.keycloak.protocol.oidc.OIDCLoginProtocolService; import org.keycloak.protocol.oidc.utils.OIDCResponseType; import org.keycloak.representations.AccessToken; +import org.keycloak.representations.JsonWebToken; import org.keycloak.representations.idm.ClientInitialAccessCreatePresentation; import org.keycloak.representations.idm.ClientInitialAccessPresentation; import org.keycloak.representations.idm.ClientRegistrationTrustedHostRepresentation; +import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.oidc.OIDCClientRepresentation; import org.keycloak.testsuite.Assert; +import org.keycloak.testsuite.client.resources.TestApplicationResourceUrls; +import org.keycloak.testsuite.client.resources.TestOIDCEndpointsApplicationResource; +import org.keycloak.testsuite.rest.resource.TestingOIDCEndpointsApplicationResource; import org.keycloak.testsuite.util.OAuthClient; import java.security.PrivateKey; import java.util.Arrays; import java.util.Collections; import java.util.LinkedList; import java.util.List; +import java.util.Map; import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriBuilder; import static org.junit.Assert.*; import static org.keycloak.testsuite.admin.AbstractAdminTest.loadJson; @@ -96,6 +106,23 @@ public class OIDCClientRegistrationTest extends AbstractClientRegistrationTest { return response; } + private void assertCreateFail(OIDCClientRepresentation client, int expectedStatusCode) { + assertCreateFail(client, expectedStatusCode, null); + } + + private void assertCreateFail(OIDCClientRepresentation client, int expectedStatusCode, String expectedErrorContains) { + try { + reg.oidc().create(client); + Assert.fail("Not expected to successfuly register client"); + } catch (ClientRegistrationException expected) { + HttpErrorException httpEx = (HttpErrorException) expected.getCause(); + Assert.assertEquals(expectedStatusCode, httpEx.getStatusLine().getStatusCode()); + if (expectedErrorContains != null) { + assertTrue("Error response doesn't contain expected text", httpEx.getErrorResponse().contains(expectedErrorContains)); + } + } + } + @Test public void testCreateWithTrustedHost() throws Exception { reg.auth(null); @@ -103,17 +130,10 @@ public class OIDCClientRegistrationTest extends AbstractClientRegistrationTest { OIDCClientRepresentation client = createRep(); // Failed to create client - try { - reg.oidc().create(client); - Assert.fail("Not expected to successfuly register client"); - } catch (ClientRegistrationException expected) { - HttpErrorException httpEx = (HttpErrorException) expected.getCause(); - Assert.assertEquals(401, httpEx.getStatusLine().getStatusCode()); - } + assertCreateFail(client, 401); // Create trusted host entry - Response response = adminClient.realm(REALM_NAME).clientRegistrationTrustedHost().create(ClientRegistrationTrustedHostRepresentation.create("localhost", 2, 2)); - Assert.assertEquals(201, response.getStatus()); + createTrustedHost("localhost", 2); // Successfully register client reg.oidc().create(client); @@ -126,13 +146,20 @@ public class OIDCClientRegistrationTest extends AbstractClientRegistrationTest { reg.oidc().create(client); // Failed to create 3rd client - try { - reg.oidc().create(client); - Assert.fail("Not expected to successfuly register client"); - } catch (ClientRegistrationException expected) { - HttpErrorException httpEx = (HttpErrorException) expected.getCause(); - Assert.assertEquals(401, httpEx.getStatusLine().getStatusCode()); - } + assertCreateFail(client, 401); + } + + // KEYCLOAK-3421 + @Test + public void createClientWithUriFragment() { + reg.auth(null); + + createTrustedHost("localhost", 1); + + OIDCClientRepresentation client = createRep(); + client.setRedirectUris(Arrays.asList("http://localhost/auth", "http://localhost/auth#fragment", "http://localhost/auth*")); + + assertCreateFail(client, 400, "URI fragment"); } @Test @@ -152,6 +179,7 @@ public class OIDCClientRegistrationTest extends AbstractClientRegistrationTest { assertEquals(Arrays.asList("code", "none"), response.getResponseTypes()); assertEquals(Arrays.asList(OAuth2Constants.AUTHORIZATION_CODE, OAuth2Constants.REFRESH_TOKEN), response.getGrantTypes()); assertEquals(OIDCLoginProtocol.CLIENT_SECRET_BASIC, response.getTokenEndpointAuthMethod()); + Assert.assertNull(response.getUserinfoSignedResponseAlg()); } @Test @@ -212,8 +240,11 @@ public class OIDCClientRegistrationTest extends AbstractClientRegistrationTest { clientRep.setGrantTypes(Collections.singletonList(OAuth2Constants.CLIENT_CREDENTIALS)); clientRep.setTokenEndpointAuthMethod(OIDCLoginProtocol.PRIVATE_KEY_JWT); - // Corresponds to PRIVATE_KEY - JSONWebKeySet keySet = loadJson(getClass().getResourceAsStream("/clientreg-test/jwks.json"), JSONWebKeySet.class); + // Generate keys for client + TestOIDCEndpointsApplicationResource oidcClientEndpointsResource = testingClient.testApp().oidcClientEndpoints(); + Map generatedKeys = oidcClientEndpointsResource.generateKeys(); + + JSONWebKeySet keySet = oidcClientEndpointsResource.getJwks(); clientRep.setJwks(keySet); OIDCClientRepresentation response = reg.oidc().create(clientRep); @@ -222,7 +253,7 @@ public class OIDCClientRegistrationTest extends AbstractClientRegistrationTest { Assert.assertNull(response.getClientSecretExpiresAt()); // Tries to authenticate client with privateKey JWT - String signedJwt = getClientSignedJWT(response.getClientId()); + String signedJwt = getClientSignedJWT(response.getClientId(), generatedKeys.get(TestingOIDCEndpointsApplicationResource.PRIVATE_KEY)); OAuthClient.AccessTokenResponse accessTokenResponse = doClientCredentialsGrantRequest(signedJwt); Assert.assertEquals(200, accessTokenResponse.getStatusCode()); AccessToken accessToken = oauth.verifyToken(accessTokenResponse.getAccessToken()); @@ -236,8 +267,11 @@ public class OIDCClientRegistrationTest extends AbstractClientRegistrationTest { clientRep.setGrantTypes(Collections.singletonList(OAuth2Constants.CLIENT_CREDENTIALS)); clientRep.setTokenEndpointAuthMethod(OIDCLoginProtocol.PRIVATE_KEY_JWT); - // Use the realmKey for client authentication too - clientRep.setJwksUri(oauth.getCertsUrl(REALM_NAME)); + // Generate keys for client + TestOIDCEndpointsApplicationResource oidcClientEndpointsResource = testingClient.testApp().oidcClientEndpoints(); + Map generatedKeys = oidcClientEndpointsResource.generateKeys(); + + clientRep.setJwksUri(TestApplicationResourceUrls.clientJwksUri()); OIDCClientRepresentation response = reg.oidc().create(clientRep); Assert.assertEquals(OIDCLoginProtocol.PRIVATE_KEY_JWT, response.getTokenEndpointAuthMethod()); @@ -245,22 +279,51 @@ public class OIDCClientRegistrationTest extends AbstractClientRegistrationTest { Assert.assertNull(response.getClientSecretExpiresAt()); // Tries to authenticate client with privateKey JWT - String signedJwt = getClientSignedJWT(response.getClientId()); + String signedJwt = getClientSignedJWT(response.getClientId(), generatedKeys.get(TestingOIDCEndpointsApplicationResource.PRIVATE_KEY)); OAuthClient.AccessTokenResponse accessTokenResponse = doClientCredentialsGrantRequest(signedJwt); Assert.assertEquals(200, accessTokenResponse.getStatusCode()); AccessToken accessToken = oauth.verifyToken(accessTokenResponse.getAccessToken()); Assert.assertEquals(response.getClientId(), accessToken.getAudience()[0]); } + @Test + public void testSignaturesRequired() throws Exception { + OIDCClientRepresentation clientRep = createRep(); + clientRep.setUserinfoSignedResponseAlg(Algorithm.RS256.toString()); + clientRep.setRequestObjectSigningAlg(Algorithm.RS256.toString()); + + OIDCClientRepresentation response = reg.oidc().create(clientRep); + Assert.assertEquals(Algorithm.RS256.toString(), response.getUserinfoSignedResponseAlg()); + Assert.assertEquals(Algorithm.RS256.toString(), response.getRequestObjectSigningAlg()); + Assert.assertNotNull(response.getClientSecret()); + + // Test Keycloak representation + ClientRepresentation kcClient = getClient(response.getClientId()); + OIDCAdvancedConfigWrapper config = OIDCAdvancedConfigWrapper.fromClientRepresentation(kcClient); + Assert.assertEquals(config.getUserInfoSignedResponseAlg(), Algorithm.RS256); + Assert.assertEquals(config.getRequestObjectSignatureAlg(), Algorithm.RS256); + } + // Client auth with signedJWT - helper methods - private String getClientSignedJWT(String clientId) { + private String getClientSignedJWT(String clientId, String privateKeyPem) { String realmInfoUrl = KeycloakUriBuilder.fromUri(getAuthServerRoot()).path(ServiceUrlConstants.REALM_INFO_PATH).build(REALM_NAME).toString(); - PrivateKey privateKey = KeycloakModelUtils.getPrivateKey(PRIVATE_KEY); + PrivateKey privateKey = KeycloakModelUtils.getPrivateKey(privateKeyPem); - JWTClientCredentialsProvider jwtProvider = new JWTClientCredentialsProvider(); + // Use token-endpoint as audience as OIDC conformance testsuite is using it too. + JWTClientCredentialsProvider jwtProvider = new JWTClientCredentialsProvider() { + + @Override + protected JsonWebToken createRequestToken(String clientId, String realmInfoUrl) { + JsonWebToken jwt = super.createRequestToken(clientId, realmInfoUrl); + String tokenEndpointUrl = OIDCLoginProtocolService.tokenUrl(UriBuilder.fromUri(getAuthServerRoot())).build(REALM_NAME).toString(); + jwt.audience(tokenEndpointUrl); + return jwt; + } + + }; jwtProvider.setPrivateKey(privateKey); jwtProvider.setTokenTimeout(10); return jwtProvider.createSignedRequestToken(clientId, realmInfoUrl); @@ -290,4 +353,9 @@ public class OIDCClientRegistrationTest extends AbstractClientRegistrationTest { } } + private void createTrustedHost(String name, int count) { + Response response = adminClient.realm(REALM_NAME).clientRegistrationTrustedHost().create(ClientRegistrationTrustedHostRepresentation.create(name, count, count)); + Assert.assertEquals(201, response.getStatus()); + } + } 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 6abaf97456..43c6fa9bb9 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 @@ -75,11 +75,11 @@ public class ExportImportTest extends AbstractExportImportTest { @Test public void testDirFullExportImport() throws Throwable { - testingClient.testing().setProvider(DirExportProviderFactory.PROVIDER_ID); - String targetDirPath = testingClient.testing().getExportImportTestDirectory()+ File.separator + "dirExport"; + testingClient.testing().exportImport().setProvider(DirExportProviderFactory.PROVIDER_ID); + String targetDirPath = testingClient.testing().exportImport().getExportImportTestDirectory()+ File.separator + "dirExport"; DirExportProvider.recursiveDeleteDir(new File(targetDirPath)); - testingClient.testing().setDir(targetDirPath); - testingClient.testing().setUsersPerFile(ExportImportConfig.DEFAULT_USERS_PER_FILE); + testingClient.testing().exportImport().setDir(targetDirPath); + testingClient.testing().exportImport().setUsersPerFile(ExportImportConfig.DEFAULT_USERS_PER_FILE); testFullExportImport(); @@ -89,11 +89,13 @@ public class ExportImportTest extends AbstractExportImportTest { @Test public void testDirRealmExportImport() throws Throwable { - testingClient.testing().setProvider(DirExportProviderFactory.PROVIDER_ID); - String targetDirPath = testingClient.testing().getExportImportTestDirectory() + File.separator + "dirRealmExport"; + testingClient.testing() + .exportImport() + .setProvider(DirExportProviderFactory.PROVIDER_ID); + String targetDirPath = testingClient.testing().exportImport().getExportImportTestDirectory() + File.separator + "dirRealmExport"; DirExportProvider.recursiveDeleteDir(new File(targetDirPath)); - testingClient.testing().setDir(targetDirPath); - testingClient.testing().setUsersPerFile(3); + testingClient.testing().exportImport().setDir(targetDirPath); + testingClient.testing().exportImport().setUsersPerFile(3); testRealmExportImport(); @@ -104,18 +106,18 @@ public class ExportImportTest extends AbstractExportImportTest { @Test public void testSingleFileFullExportImport() throws Throwable { - testingClient.testing().setProvider(SingleFileExportProviderFactory.PROVIDER_ID); - String targetFilePath = testingClient.testing().getExportImportTestDirectory() + File.separator + "singleFile-full.json"; - testingClient.testing().setFile(targetFilePath); + testingClient.testing().exportImport().setProvider(SingleFileExportProviderFactory.PROVIDER_ID); + String targetFilePath = testingClient.testing().exportImport().getExportImportTestDirectory() + File.separator + "singleFile-full.json"; + testingClient.testing().exportImport().setFile(targetFilePath); testFullExportImport(); } @Test public void testSingleFileRealmExportImport() throws Throwable { - testingClient.testing().setProvider(SingleFileExportProviderFactory.PROVIDER_ID); - String targetFilePath = testingClient.testing().getExportImportTestDirectory() + File.separator + "singleFile-realm.json"; - testingClient.testing().setFile(targetFilePath); + testingClient.testing().exportImport().setProvider(SingleFileExportProviderFactory.PROVIDER_ID); + String targetFilePath = testingClient.testing().exportImport().getExportImportTestDirectory() + File.separator + "singleFile-realm.json"; + testingClient.testing().exportImport().setFile(targetFilePath); testRealmExportImport(); } @@ -126,14 +128,14 @@ public class ExportImportTest extends AbstractExportImportTest { removeRealm("test-realm"); // Set the realm, which doesn't have builtin clients/roles inside JSON - testingClient.testing().setProvider(SingleFileExportProviderFactory.PROVIDER_ID); + testingClient.testing().exportImport().setProvider(SingleFileExportProviderFactory.PROVIDER_ID); URL url = ExportImportTest.class.getResource("/model/testrealm.json"); String targetFilePath = new File(url.getFile()).getAbsolutePath(); - testingClient.testing().setFile(targetFilePath); + testingClient.testing().exportImport().setFile(targetFilePath); - testingClient.testing().setAction(ExportImportConfig.ACTION_IMPORT); + testingClient.testing().exportImport().setAction(ExportImportConfig.ACTION_IMPORT); - testingClient.testing().runImport(); + testingClient.testing().exportImport().runImport(); RealmResource testRealmRealm = adminClient.realm("test-realm"); @@ -158,14 +160,14 @@ public class ExportImportTest extends AbstractExportImportTest { realm.components().add(component); - testingClient.testing().setProvider(SingleFileExportProviderFactory.PROVIDER_ID); + testingClient.testing().exportImport().setProvider(SingleFileExportProviderFactory.PROVIDER_ID); - String targetFilePath = testingClient.testing().getExportImportTestDirectory() + File.separator + "singleFile-realm.json"; - testingClient.testing().setFile(targetFilePath); - testingClient.testing().setAction(ExportImportConfig.ACTION_EXPORT); - testingClient.testing().setRealmName("component-realm"); + String targetFilePath = testingClient.testing().exportImport().getExportImportTestDirectory() + File.separator + "singleFile-realm.json"; + testingClient.testing().exportImport().setFile(targetFilePath); + testingClient.testing().exportImport().setAction(ExportImportConfig.ACTION_EXPORT); + testingClient.testing().exportImport().setRealmName("component-realm"); - testingClient.testing().runExport(); + testingClient.testing().exportImport().runExport(); // Delete some realm (and some data in admin realm) adminClient.realm("component-realm").remove(); @@ -173,9 +175,9 @@ public class ExportImportTest extends AbstractExportImportTest { Assert.assertEquals(3, adminClient.realms().findAll().size()); // Configure import - testingClient.testing().setAction(ExportImportConfig.ACTION_IMPORT); + testingClient.testing().exportImport().setAction(ExportImportConfig.ACTION_IMPORT); - testingClient.testing().runImport(); + testingClient.testing().exportImport().runImport(); realmRep = realm.toRepresentation(); @@ -203,10 +205,10 @@ public class ExportImportTest extends AbstractExportImportTest { } private void testFullExportImport() throws LifecycleException { - testingClient.testing().setAction(ExportImportConfig.ACTION_EXPORT); - testingClient.testing().setRealmName(""); + testingClient.testing().exportImport().setAction(ExportImportConfig.ACTION_EXPORT); + testingClient.testing().exportImport().setRealmName(""); - testingClient.testing().runExport(); + testingClient.testing().exportImport().runExport(); removeRealm("test"); removeRealm("test-realm"); @@ -218,9 +220,9 @@ public class ExportImportTest extends AbstractExportImportTest { assertNotAuthenticated("test", "user3", "password"); // Configure import - testingClient.testing().setAction(ExportImportConfig.ACTION_IMPORT); + testingClient.testing().exportImport().setAction(ExportImportConfig.ACTION_IMPORT); - testingClient.testing().runImport(); + testingClient.testing().exportImport().runImport(); // Ensure data are imported back Assert.assertEquals(3, adminClient.realms().findAll().size()); @@ -232,10 +234,10 @@ public class ExportImportTest extends AbstractExportImportTest { } private void testRealmExportImport() throws LifecycleException { - testingClient.testing().setAction(ExportImportConfig.ACTION_EXPORT); - testingClient.testing().setRealmName("test"); + testingClient.testing().exportImport().setAction(ExportImportConfig.ACTION_EXPORT); + testingClient.testing().exportImport().setRealmName("test"); - testingClient.testing().runExport(); + testingClient.testing().exportImport().runExport(); // Delete some realm (and some data in admin realm) adminClient.realm("test").remove(); @@ -248,9 +250,9 @@ public class ExportImportTest extends AbstractExportImportTest { assertNotAuthenticated("test", "user3", "password"); // Configure import - testingClient.testing().setAction(ExportImportConfig.ACTION_IMPORT); + testingClient.testing().exportImport().setAction(ExportImportConfig.ACTION_IMPORT); - testingClient.testing().runImport(); + testingClient.testing().exportImport().runImport(); // Ensure data are imported back, but just for "test" realm Assert.assertEquals(3, adminClient.realms().findAll().size()); @@ -273,27 +275,4 @@ public class ExportImportTest extends AbstractExportImportTest { Assert.assertEquals(expectedResult, testingClient.testing().validCredentials(realmName, username, password)); } - private static String getExportImportTestDirectory() { - String dirPath = null; - String relativeDirExportImportPath = "testsuite" + File.separator + - "integration-arquillian" + File.separator + - "tests" + File.separator + - "base" + File.separator + - "target" + File.separator + - "export-import"; - - if (System.getProperties().containsKey("maven.home")) { - dirPath = System.getProperty("user.dir").replaceFirst("testsuite.integration.*", Matcher.quoteReplacement(relativeDirExportImportPath)); - } else { - for (String c : System.getProperty("java.class.path").split(File.pathSeparator)) { - if (c.contains(File.separator + "testsuite" + File.separator + "integration-arquillian" + File.separator)) { - dirPath = c.replaceFirst("testsuite.integration-arquillian.*", Matcher.quoteReplacement(relativeDirExportImportPath)); - } - } - } - - String absolutePath = new File(dirPath).getAbsolutePath(); - return absolutePath; - } - } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/ClientAuthSignedJWTTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/ClientAuthSignedJWTTest.java index 087a1e651b..c1cbdea6ac 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/ClientAuthSignedJWTTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/ClientAuthSignedJWTTest.java @@ -55,7 +55,9 @@ import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.EventRepresentation; import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.UserRepresentation; +import org.keycloak.services.util.CertificateInfoHelper; import org.keycloak.testsuite.AbstractKeycloakTest; +import org.keycloak.testsuite.Assert; import org.keycloak.testsuite.AssertEvents; import org.keycloak.testsuite.admin.ApiUtil; import org.keycloak.testsuite.auth.page.AuthRealm; @@ -70,6 +72,7 @@ import java.net.URL; import java.nio.file.Files; import java.security.KeyStore; import java.security.PrivateKey; +import java.security.PublicKey; import java.security.cert.X509Certificate; import java.util.*; @@ -81,7 +84,6 @@ import static org.junit.Assert.assertNotEquals; * @author Vaclav Muzikar */ public class ClientAuthSignedJWTTest extends AbstractKeycloakTest { - public static final String CERTIFICATE_PEM = "Certificate PEM"; @Rule public AssertEvents events = new AssertEvents(this); @@ -349,8 +351,18 @@ public class ClientAuthSignedJWTTest extends AbstractKeycloakTest { } @Test - public void testUploadPEM() throws Exception { - testUploadKeystore(CERTIFICATE_PEM, "client-auth-test/certificate.pem", "undefined", "undefined"); + public void testUploadCertificatePEM() throws Exception { + testUploadKeystore(org.keycloak.services.resources.admin.ClientAttributeCertificateResource.CERTIFICATE_PEM, "client-auth-test/certificate.pem", "undefined", "undefined"); + } + + @Test + public void testUploadPublicKeyPEM() throws Exception { + testUploadKeystore(org.keycloak.services.resources.admin.ClientAttributeCertificateResource.PUBLIC_KEY_PEM, "client-auth-test/publickey.pem", "undefined", "undefined"); + } + + @Test + public void testUploadJWKS() throws Exception { + testUploadKeystore(org.keycloak.services.resources.admin.ClientAttributeCertificateResource.JSON_WEB_KEY_SET, "clientreg-test/jwks.json", "undefined", "undefined"); } // We need to test this as a genuine REST API HTTP request @@ -395,20 +407,27 @@ public class ClientAuthSignedJWTTest extends AbstractKeycloakTest { assertEquals(200, httpResponse.getStatusLine().getStatusCode()); client = getClient(testRealm.getRealm(), client.getId()).toRepresentation(); - String pem; // Assert the uploaded certificate - if (!keystoreFormat.equals(CERTIFICATE_PEM)) { + if (keystoreFormat.equals(org.keycloak.services.resources.admin.ClientAttributeCertificateResource.PUBLIC_KEY_PEM)) { + String pem = new String(Files.readAllBytes(keystoreFile.toPath())); + final String publicKeyNew = client.getAttributes().get(JWTClientAuthenticator.ATTR_PREFIX + "." + CertificateInfoHelper.PUBLIC_KEY); + assertEquals("Certificates don't match", pem, publicKeyNew); + } else if (keystoreFormat.equals(org.keycloak.services.resources.admin.ClientAttributeCertificateResource.JSON_WEB_KEY_SET)) { + final String publicKeyNew = client.getAttributes().get(JWTClientAuthenticator.ATTR_PREFIX + "." + CertificateInfoHelper.PUBLIC_KEY); + // Just assert it's valid public key + PublicKey pk = KeycloakModelUtils.getPublicKey(publicKeyNew); + Assert.assertNotNull(pk); + } else if (keystoreFormat.equals(org.keycloak.services.resources.admin.ClientAttributeCertificateResource.CERTIFICATE_PEM)) { + String pem = new String(Files.readAllBytes(keystoreFile.toPath())); + assertCertificate(client, certOld, pem); + } else { InputStream keystoreIs = new FileInputStream(keystoreFile); KeyStore keyStore = getKeystore(keystoreIs, storePassword, keystoreFormat); keystoreIs.close(); - pem = KeycloakModelUtils.getPemFromCertificate((X509Certificate) keyStore.getCertificate(keyAlias)); + String pem = KeycloakModelUtils.getPemFromCertificate((X509Certificate) keyStore.getCertificate(keyAlias)); + assertCertificate(client, certOld, pem); } - else { - pem = new String(Files.readAllBytes(keystoreFile.toPath())); - } - - assertCertificate(client, certOld, pem); } // TEST ERRORS diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OAuthRedirectUriTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OAuthRedirectUriTest.java index f80e789ef7..520f910e77 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OAuthRedirectUriTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OAuthRedirectUriTest.java @@ -97,6 +97,11 @@ public class OAuthRedirectUriTest extends AbstractKeycloakTest { .secret("password"); realm.client(installedApp6); + ClientBuilder installedApp7 = ClientBuilder.create().id("test-query-component").name("test-query-component") + .redirectUris("http://localhost?foo=bar", "http://localhost?foo=bar*") + .secret("password"); + realm.client(installedApp7); + testRealms.add(realm.build()); } @@ -185,6 +190,26 @@ public class OAuthRedirectUriTest extends AbstractKeycloakTest { Assert.assertTrue(url.getQuery().contains("code=")); } + @Test + public void testQueryComponents() throws IOException { + // KEYCLOAK-3420 + oauth.clientId("test-query-component"); + checkRedirectUri("http://localhost?foo=bar", true); + checkRedirectUri("http://localhost?foo=bara", false); + checkRedirectUri("http://localhost?foo=bar/", false); + checkRedirectUri("http://localhost?foo2=bar2&foo=bar", false); + checkRedirectUri("http://localhost?foo=b", false); + checkRedirectUri("http://localhost?foo", false); + checkRedirectUri("http://localhost?foo=bar&bar=foo", false); + checkRedirectUri("http://localhost?foo&bar=foo", false); + checkRedirectUri("http://localhost?foo&bar", false); + checkRedirectUri("http://localhost", false); + + // KEYCLOAK-3418 + oauth.clientId("test-installed"); + checkRedirectUri("http://localhost?foo=bar", false); + } + @Test public void testWildcard() throws IOException { oauth.clientId("test-wildcard"); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/OIDCAdvancedRequestParamsTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/OIDCAdvancedRequestParamsTest.java index 191581e600..e01057d418 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/OIDCAdvancedRequestParamsTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/OIDCAdvancedRequestParamsTest.java @@ -19,30 +19,42 @@ package org.keycloak.testsuite.oidc; import java.util.List; - import org.jboss.arquillian.graphene.page.Page; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.keycloak.OAuthErrorException; +import org.keycloak.admin.client.resource.ClientResource; +import org.keycloak.authentication.authenticators.client.JWTClientAuthenticator; import org.keycloak.common.util.Time; import org.keycloak.events.Details; +import org.keycloak.jose.jws.Algorithm; import org.keycloak.models.Constants; +import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper; import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.representations.IDToken; +import org.keycloak.representations.idm.CertificateRepresentation; +import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.EventRepresentation; import org.keycloak.representations.idm.RealmRepresentation; +import org.keycloak.services.util.CertificateInfoHelper; import org.keycloak.testsuite.Assert; import org.keycloak.testsuite.AssertEvents; import org.keycloak.testsuite.TestRealmKeycloakTest; import org.keycloak.testsuite.admin.AbstractAdminTest; +import org.keycloak.testsuite.admin.ApiUtil; +import org.keycloak.testsuite.client.resources.TestApplicationResourceUrls; +import org.keycloak.testsuite.client.resources.TestOIDCEndpointsApplicationResource; import org.keycloak.testsuite.pages.AccountUpdateProfilePage; import org.keycloak.testsuite.pages.AppPage; +import org.keycloak.testsuite.pages.ErrorPage; import org.keycloak.testsuite.pages.LoginPage; import org.keycloak.testsuite.pages.OAuthGrantPage; +import org.keycloak.testsuite.rest.resource.TestingOIDCEndpointsApplicationResource; import org.keycloak.testsuite.util.ClientManager; import org.keycloak.testsuite.util.OAuthClient; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; @@ -68,6 +80,9 @@ public class OIDCAdvancedRequestParamsTest extends TestRealmKeycloakTest { @Page protected OAuthGrantPage grantPage; + @Page + protected ErrorPage errorPage; + @Override public void configureTestRealm(RealmRepresentation testRealm) { @@ -308,29 +323,98 @@ public class OIDCAdvancedRequestParamsTest extends TestRealmKeycloakTest { // REQUEST & REQUEST_URI @Test - public void requestParam() { - driver.navigate().to(oauth.getLoginFormUrl() + "&request=abc"); + public void requestParamUnsigned() throws Exception { + String validRedirectUri = oauth.getRedirectUri(); + TestOIDCEndpointsApplicationResource oidcClientEndpointsResource = testingClient.testApp().oidcClientEndpoints(); - assertFalse(loginPage.isCurrent()); + // Send request object with invalid redirect uri. + oidcClientEndpointsResource.setOIDCRequest("test", "test-app", "http://invalid", null, Algorithm.none.toString()); + String requestStr = oidcClientEndpointsResource.getOIDCRequest(); + + oauth.request(requestStr); + oauth.openLoginForm(); + Assert.assertTrue(errorPage.isCurrent()); + assertEquals("Invalid parameter: redirect_uri", errorPage.getError()); + + // Assert the value from request object has bigger priority then from the query parameter. + oauth.redirectUri("http://invalid"); + oidcClientEndpointsResource.setOIDCRequest("test", "test-app", validRedirectUri, "10", Algorithm.none.toString()); + requestStr = oidcClientEndpointsResource.getOIDCRequest(); + + oauth.request(requestStr); + OAuthClient.AuthorizationEndpointResponse response = oauth.doLogin("test-user@localhost", "password"); + Assert.assertNotNull(response.getCode()); + Assert.assertEquals("mystate", response.getState()); assertTrue(appPage.isCurrent()); - - // Assert error response was sent because not logged in - OAuthClient.AuthorizationEndpointResponse resp = new OAuthClient.AuthorizationEndpointResponse(oauth); - Assert.assertNull(resp.getCode()); - Assert.assertEquals(OAuthErrorException.REQUEST_NOT_SUPPORTED, resp.getError()); } @Test - public void requestUriParam() { - driver.navigate().to(oauth.getLoginFormUrl() + "&request_uri=https%3A%2F%2Flocalhost%3A60784%2Fexport%2FqzHTG11W48.jwt"); + public void requestUriParamUnsigned() throws Exception { + String validRedirectUri = oauth.getRedirectUri(); + TestOIDCEndpointsApplicationResource oidcClientEndpointsResource = testingClient.testApp().oidcClientEndpoints(); - assertFalse(loginPage.isCurrent()); + // Send request object with invalid redirect uri. + oidcClientEndpointsResource.setOIDCRequest("test", "test-app", "http://invalid", null, Algorithm.none.toString()); + + oauth.requestUri(TestApplicationResourceUrls.clientRequestUri()); + oauth.openLoginForm(); + Assert.assertTrue(errorPage.isCurrent()); + assertEquals("Invalid parameter: redirect_uri", errorPage.getError()); + + // Assert the value from request object has bigger priority then from the query parameter. + oauth.redirectUri("http://invalid"); + oidcClientEndpointsResource.setOIDCRequest("test", "test-app", validRedirectUri, "10", Algorithm.none.toString()); + + OAuthClient.AuthorizationEndpointResponse response = oauth.doLogin("test-user@localhost", "password"); + Assert.assertNotNull(response.getCode()); + Assert.assertEquals("mystate", response.getState()); + assertTrue(appPage.isCurrent()); + } + + @Test + public void requestUriParamSigned() throws Exception { + String validRedirectUri = oauth.getRedirectUri(); + TestOIDCEndpointsApplicationResource oidcClientEndpointsResource = testingClient.testApp().oidcClientEndpoints(); + + // Set required signature for request_uri + ClientResource clientResource = ApiUtil.findClientByClientId(adminClient.realm("test"), "test-app"); + ClientRepresentation clientRep = clientResource.toRepresentation(); + OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).setRequestObjectSignatureAlg(Algorithm.RS256); + clientResource.update(clientRep); + + // Verify unsigned request_uri will fail + oidcClientEndpointsResource.setOIDCRequest("test", "test-app", validRedirectUri, "10", Algorithm.none.toString()); + oauth.requestUri(TestApplicationResourceUrls.clientRequestUri()); + oauth.openLoginForm(); + Assert.assertTrue(errorPage.isCurrent()); + assertEquals("Invalid Request", errorPage.getError()); + + // Generate keypair for client + String clientPublicKeyPem = oidcClientEndpointsResource.generateKeys().get(TestingOIDCEndpointsApplicationResource.PUBLIC_KEY); + + // Verify signed request_uri will fail due to failed signature validation + oidcClientEndpointsResource.setOIDCRequest("test", "test-app", validRedirectUri, "10", Algorithm.RS256.toString()); + oauth.openLoginForm(); + Assert.assertTrue(errorPage.isCurrent()); + assertEquals("Invalid Request", errorPage.getError()); + + + // Update clientModel with publicKey for signing + clientRep = clientResource.toRepresentation(); + CertificateRepresentation cert = new CertificateRepresentation(); + cert.setPublicKey(clientPublicKeyPem); + CertificateInfoHelper.updateClientRepresentationCertificateInfo(clientRep, cert, JWTClientAuthenticator.ATTR_PREFIX); + clientResource.update(clientRep); + + // Check signed request_uri will pass + OAuthClient.AuthorizationEndpointResponse response = oauth.doLogin("test-user@localhost", "password"); + Assert.assertNotNull(response.getCode()); + Assert.assertEquals("mystate", response.getState()); assertTrue(appPage.isCurrent()); - // Assert error response was sent because not logged in - OAuthClient.AuthorizationEndpointResponse resp = new OAuthClient.AuthorizationEndpointResponse(oauth); - Assert.assertNull(resp.getCode()); - Assert.assertEquals(OAuthErrorException.REQUEST_URI_NOT_SUPPORTED, resp.getError()); + // Revert requiring signature for client + OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).setRequestObjectSignatureAlg(null); + clientResource.update(clientRep); } // LOGIN_HINT diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/OIDCWellKnownProviderTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/OIDCWellKnownProviderTest.java index ab5c2304be..7fea65748b 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/OIDCWellKnownProviderTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/OIDCWellKnownProviderTest.java @@ -86,6 +86,8 @@ public class OIDCWellKnownProviderTest extends AbstractKeycloakTest { Assert.assertNames(oidcConfig.getSubjectTypesSupported(), "public"); Assert.assertNames(oidcConfig.getIdTokenSigningAlgValuesSupported(), Algorithm.RS256.toString()); + Assert.assertNames(oidcConfig.getUserInfoSigningAlgValuesSupported(), Algorithm.RS256.toString()); + Assert.assertNames(oidcConfig.getRequestObjectSigningAlgValuesSupported(), Algorithm.none.toString(), Algorithm.RS256.toString()); // Client authentication Assert.assertNames(oidcConfig.getTokenEndpointAuthMethodsSupported(), "client_secret_basic", "client_secret_post", "private_key_jwt"); @@ -100,8 +102,8 @@ public class OIDCWellKnownProviderTest extends AbstractKeycloakTest { Assert.assertNames(oidcConfig.getScopesSupported(), OAuth2Constants.SCOPE_OPENID, OAuth2Constants.OFFLINE_ACCESS); // Request and Request_Uri - Assert.assertFalse(oidcConfig.getRequestParameterSupported()); - Assert.assertFalse(oidcConfig.getRequestUriParameterSupported()); + Assert.assertTrue(oidcConfig.getRequestParameterSupported()); + Assert.assertTrue(oidcConfig.getRequestUriParameterSupported()); } finally { client.close(); } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/UserInfoTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/UserInfoTest.java index 268333f0e5..e0588e79e5 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/UserInfoTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/UserInfoTest.java @@ -21,19 +21,31 @@ import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.keycloak.OAuth2Constants; +import org.keycloak.admin.client.resource.ClientResource; import org.keycloak.events.Details; import org.keycloak.events.Errors; import org.keycloak.events.EventType; +import org.keycloak.jose.jws.Algorithm; +import org.keycloak.jose.jws.JWSInput; +import org.keycloak.jose.jws.crypto.RSAProvider; +import org.keycloak.models.utils.KeycloakModelUtils; +import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper; import org.keycloak.protocol.oidc.OIDCLoginProtocolService; import org.keycloak.representations.AccessTokenResponse; import org.keycloak.representations.UserInfo; +import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.RealmRepresentation; +import org.keycloak.services.Urls; import org.keycloak.testsuite.AbstractKeycloakTest; +import org.keycloak.testsuite.Assert; import org.keycloak.testsuite.AssertEvents; +import org.keycloak.testsuite.admin.ApiUtil; import org.keycloak.testsuite.util.ClientManager; import org.keycloak.testsuite.util.RealmBuilder; import org.keycloak.testsuite.util.UserInfoClientUtil; import org.keycloak.util.BasicAuthHelper; +import org.keycloak.util.JsonSerialization; +import org.keycloak.utils.MediaType; import javax.ws.rs.client.Client; import javax.ws.rs.client.ClientBuilder; @@ -45,6 +57,7 @@ import javax.ws.rs.core.Response; import javax.ws.rs.core.Response.Status; import javax.ws.rs.core.UriBuilder; import java.net.URI; +import java.security.PublicKey; import java.util.List; import static org.junit.Assert.assertEquals; @@ -152,6 +165,62 @@ public class UserInfoTest extends AbstractKeycloakTest { } } + @Test + public void testSuccessSignedResponse() throws Exception { + // Require signed userInfo request + ClientResource clientResource = ApiUtil.findClientByClientId(adminClient.realm("test"), "test-app"); + ClientRepresentation clientRep = clientResource.toRepresentation(); + OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).setUserInfoSignedResponseAlg(Algorithm.RS256); + clientResource.update(clientRep); + + // test signed response + Client client = ClientBuilder.newClient(); + + try { + AccessTokenResponse accessTokenResponse = executeGrantAccessTokenRequest(client); + + Response response = UserInfoClientUtil.executeUserInfoRequest_getMethod(client, accessTokenResponse.getToken()); + + events.expect(EventType.USER_INFO_REQUEST) + .session(Matchers.notNullValue(String.class)) + .detail(Details.AUTH_METHOD, Details.VALIDATE_ACCESS_TOKEN) + .detail(Details.USERNAME, "test-user@localhost") + .detail(Details.SIGNATURE_REQUIRED, "true") + .detail(Details.SIGNATURE_ALGORITHM, Algorithm.RS256.toString()) + .assertEvent(); + + // Check signature and content + RealmRepresentation realmRep = adminClient.realm("test").toRepresentation(); + PublicKey publicKey = KeycloakModelUtils.getPublicKey(realmRep.getPublicKey()); + + Assert.assertEquals(200, response.getStatus()); + Assert.assertEquals(response.getHeaderString(HttpHeaders.CONTENT_TYPE), MediaType.APPLICATION_JWT); + String signedResponse = response.readEntity(String.class); + response.close(); + + JWSInput jwsInput = new JWSInput(signedResponse); + Assert.assertTrue(RSAProvider.verify(jwsInput, publicKey)); + + UserInfo userInfo = JsonSerialization.readValue(jwsInput.getContent(), UserInfo.class); + + Assert.assertNotNull(userInfo); + Assert.assertNotNull(userInfo.getSubject()); + Assert.assertEquals("test-user@localhost", userInfo.getEmail()); + Assert.assertEquals("test-user@localhost", userInfo.getPreferredUsername()); + + Assert.assertTrue(userInfo.hasAudience("test-app")); + String expectedIssuer = Urls.realmIssuer(new URI(AUTH_SERVER_ROOT), "test"); + Assert.assertEquals(expectedIssuer, userInfo.getIssuer()); + + } finally { + client.close(); + } + + // Revert signed userInfo request + OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).setUserInfoSignedResponseAlg(null); + clientResource.update(clientRep); + } + @Test public void testSessionExpired() throws Exception { Client client = ClientBuilder.newClient(); @@ -235,6 +304,7 @@ public class UserInfoTest extends AbstractKeycloakTest { .session(Matchers.notNullValue(String.class)) .detail(Details.AUTH_METHOD, Details.VALIDATE_ACCESS_TOKEN) .detail(Details.USERNAME, "test-user@localhost") + .detail(Details.SIGNATURE_REQUIRED, "false") .assertEvent(); UserInfoClientUtil.testSuccessfulUserInfoResponse(response, "test-user@localhost", "test-user@localhost"); } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/SslMailServer.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/SslMailServer.java index 0088e42070..0f87d49ab7 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/SslMailServer.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/SslMailServer.java @@ -91,7 +91,7 @@ public class SslMailServer { ksKeys.load(keyStoreIS, keyStorePassphrase); // KeyManager decides which key material to use. - KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509"); + KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); kmf.init(ksKeys, keyStorePassphrase); // Trust store for client authentication. @@ -101,7 +101,7 @@ public class SslMailServer { ksTrust.load(trustStoreIS, trustStorePassphrase); // TrustManager decides which certificate authorities to use. - TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509"); + TrustManagerFactory tmf = TrustManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); tmf.init(ksTrust); final SSLContext sslContext = SSLContext.getInstance("TLS"); diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/keycloak-server.json b/testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/keycloak-server.json index 99e8614d0e..d8c4dc1b0c 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/keycloak-server.json +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/keycloak-server.json @@ -90,7 +90,8 @@ "driverDialect": "${keycloak.connectionsJpa.driverDialect:}", "user": "${keycloak.connectionsJpa.user:sa}", "password": "${keycloak.connectionsJpa.password:}", - "databaseSchema": "${keycloak.connectionsJpa.databaseSchema:update}", + "initializeEmpty": true, + "migrationStrategy": "update", "showSql": "${keycloak.connectionsJpa.showSql:false}", "formatSql": "${keycloak.connectionsJpa.formatSql:true}", "globalStatsInterval": "${keycloak.connectionsJpa.globalStatsInterval:-1}" diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/bad-assertion-sales-post-sig/WEB-INF/keycloak-saml.xml b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/bad-assertion-sales-post-sig/WEB-INF/keycloak-saml.xml new file mode 100755 index 0000000000..2ea7ff73c6 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/bad-assertion-sales-post-sig/WEB-INF/keycloak-saml.xml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/bad-assertion-sales-post-sig/WEB-INF/keystore.jks b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/bad-assertion-sales-post-sig/WEB-INF/keystore.jks new file mode 100755 index 0000000000..215384cdef Binary files /dev/null and b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/bad-assertion-sales-post-sig/WEB-INF/keystore.jks differ diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/employee/WEB-INF/keycloak-saml.xml b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/employee/WEB-INF/keycloak-saml.xml new file mode 100755 index 0000000000..96f0b86483 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/employee/WEB-INF/keycloak-saml.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/employee/WEB-INF/web.xml b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/employee/WEB-INF/web.xml new file mode 100644 index 0000000000..019958832b --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/employee/WEB-INF/web.xml @@ -0,0 +1,54 @@ + + + + + + %CONTEXT_PATH% + + + Servlet + org.keycloak.testsuite.adapter.servlet.SamlSPFacade + + + + Servlet + /* + + + + + Users + /* + + + manager + + + + + KEYCLOAK-SAML + demo + + + + manager + + diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/input-portal/WEB-INF/keycloak-saml.xml b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/input-portal/WEB-INF/keycloak-saml.xml new file mode 100755 index 0000000000..e1cd76a88e --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/input-portal/WEB-INF/keycloak-saml.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/input-portal/WEB-INF/web.xml b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/input-portal/WEB-INF/web.xml new file mode 100644 index 0000000000..865dc8a170 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/input-portal/WEB-INF/web.xml @@ -0,0 +1,54 @@ + + + + + + %CONTEXT_PATH% + + + Servlet + org.keycloak.testsuite.adapter.servlet.InputServlet + + + + Servlet + /* + + + + + Users + /secured/* + + + manager + + + + + KEYCLOAK-SAML + demo + + + + manager + + diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/missing-assertion-sig/WEB-INF/keycloak-saml.xml b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/missing-assertion-sig/WEB-INF/keycloak-saml.xml new file mode 100755 index 0000000000..936fb21711 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/missing-assertion-sig/WEB-INF/keycloak-saml.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/missing-assertion-sig/WEB-INF/keystore.jks b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/missing-assertion-sig/WEB-INF/keystore.jks new file mode 100755 index 0000000000..144830bc77 Binary files /dev/null and b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/missing-assertion-sig/WEB-INF/keystore.jks differ diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/sales-post-assertion-and-response-sig/WEB-INF/keycloak-saml.xml b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/sales-post-assertion-and-response-sig/WEB-INF/keycloak-saml.xml new file mode 100755 index 0000000000..94c3f4da01 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/sales-post-assertion-and-response-sig/WEB-INF/keycloak-saml.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/sales-post-assertion-and-response-sig/WEB-INF/keystore.jks b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/sales-post-assertion-and-response-sig/WEB-INF/keystore.jks new file mode 100755 index 0000000000..144830bc77 Binary files /dev/null and b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/sales-post-assertion-and-response-sig/WEB-INF/keystore.jks differ diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/sales-post2/WEB-INF/keycloak-saml.xml b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/sales-post2/WEB-INF/keycloak-saml.xml new file mode 100755 index 0000000000..eae5b14985 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/sales-post2/WEB-INF/keycloak-saml.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/testsaml.json b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/testsaml.json index 5a1d2c6c20..0e25d89f60 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/testsaml.json +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/testsaml.json @@ -67,6 +67,101 @@ } ], "clients": [ + { + "clientId": "http://localhost:8081/missing-assertion-sig/", + "enabled": true, + "protocol": "saml", + "fullScopeAllowed": true, + "baseUrl": "http://localhost:8080/missing-assertion-sig", + "redirectUris": [ + "http://localhost:8080/missing-assertion-sig/*" + ], + "attributes": { + "saml_assertion_consumer_url_post": "http://localhost:8080/missing-assertion-sig/saml", + "saml_assertion_consumer_url_redirect": "http://localhost:8080/missing-assertion-sig/saml", + "saml_single_logout_service_url_post": "http://localhost:8080/missing-assertion-sig/saml", + "saml_single_logout_service_url_redirect": "http://localhost:8080/missing-assertion-sig/saml", + "saml.server.signature": "true", + "saml.assertion.signature": "false", + "saml.signature.algorithm": "RSA_SHA256", + "saml.client.signature": "true", + "saml.authnstatement": "true", + "saml.signing.certificate": "MIIB1DCCAT0CBgFJGP5dZDANBgkqhkiG9w0BAQsFADAwMS4wLAYDVQQDEyVodHRwOi8vbG9jYWxob3N0OjgwODAvc2FsZXMtcG9zdC1zaWcvMB4XDTE0MTAxNjEyNDQyM1oXDTI0MTAxNjEyNDYwM1owMDEuMCwGA1UEAxMlaHR0cDovL2xvY2FsaG9zdDo4MDgwL3NhbGVzLXBvc3Qtc2lnLzCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA1RvGu8RjemSJA23nnMksoHA37MqY1DDTxOECY4rPAd9egr7GUNIXE0y1MokaR5R2crNpN8RIRwR8phQtQDjXL82c6W+NLQISxztarQJ7rdNJIYwHY0d5ri1XRpDP8zAuxubPYiMAVYcDkIcvlbBpwh/dRM5I2eElRK+eSiaMkCUCAwEAATANBgkqhkiG9w0BAQsFAAOBgQCLms6htnPaY69k1ntm9a5jgwSn/K61cdai8R8B0ccY7zvinn9AfRD7fiROQpFyY29wKn8WCLrJ86NBXfgFUGyR5nLNHVy3FghE36N2oHy53uichieMxffE6vhkKJ4P8ChfJMMOZlmCPsQPDvjoAghHt4mriFiQgRdPgIy/zDjSNw==" + } + }, + { + "clientId": "http://localhost:8081/bad-assertion-sales-post-sig/", + "enabled": true, + "protocol": "saml", + "fullScopeAllowed": true, + "baseUrl": "http://localhost:8080/bad-assertion-sales-post-sig/", + "adminUrl": "http://localhost:8080/bad-assertion-sales-post-sig/saml", + "redirectUris": [ + "http://localhost:8080/bad-assertion-sales-post-sig/*" + ], + "attributes": { + "saml.assertion.signature": "true", + "saml.client.signature": "true", + "saml.authnstatement": "true", + "saml.signing.certificate": "MIIB1DCCAT0CBgFJGP5dZDANBgkqhkiG9w0BAQsFADAwMS4wLAYDVQQDEyVodHRwOi8vbG9jYWxob3N0OjgwODAvc2FsZXMtcG9zdC1zaWcvMB4XDTE0MTAxNjEyNDQyM1oXDTI0MTAxNjEyNDYwM1owMDEuMCwGA1UEAxMlaHR0cDovL2xvY2FsaG9zdDo4MDgwL3NhbGVzLXBvc3Qtc2lnLzCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA1RvGu8RjemSJA23nnMksoHA37MqY1DDTxOECY4rPAd9egr7GUNIXE0y1MokaR5R2crNpN8RIRwR8phQtQDjXL82c6W+NLQISxztarQJ7rdNJIYwHY0d5ri1XRpDP8zAuxubPYiMAVYcDkIcvlbBpwh/dRM5I2eElRK+eSiaMkCUCAwEAATANBgkqhkiG9w0BAQsFAAOBgQCLms6htnPaY69k1ntm9a5jgwSn/K61cdai8R8B0ccY7zvinn9AfRD7fiROQpFyY29wKn8WCLrJ86NBXfgFUGyR5nLNHVy3FghE36N2oHy53uichieMxffE6vhkKJ4P8ChfJMMOZlmCPsQPDvjoAghHt4mriFiQgRdPgIy/zDjSNw==" + } + }, + { + "clientId": "http://localhost:8081/input-portal/", + "enabled": true, + "fullScopeAllowed": true, + "protocol": "saml", + "baseUrl": "http://localhost:8080/input-portal", + "redirectUris": [ + "http://localhost:8080/input-portal/*" + ], + "attributes": { + "saml.authnstatement": "true", + "saml_assertion_consumer_url_post": "http://localhost:8080/input-portal/saml", + "saml_assertion_consumer_url_redirect": "http://localhost:8080/input-portal/saml", + "saml_single_logout_service_url_post": "http://localhost:8080/input-portal/saml", + "saml_single_logout_service_url_redirect": "http://localhost:8080/input-portal/saml" + } + }, + { + "clientId": "http://localhost:8081/sales-post-assertion-and-response-sig/", + "enabled": true, + "protocol": "saml", + "fullScopeAllowed": true, + "baseUrl": "http://localhost:8080/sales-post-assertion-and-response-sig", + "redirectUris": [ + "http://localhost:8080/sales-post-assertion-and-response-sig/*" + ], + "attributes": { + "saml_assertion_consumer_url_post": "http://localhost:8080/sales-post-assertion-and-response-sig/saml", + "saml_assertion_consumer_url_redirect": "http://localhost:8080/sales-post-assertion-and-response-sig/saml", + "saml_single_logout_service_url_post": "http://localhost:8080/sales-post-assertion-and-response-sig/saml", + "saml_single_logout_service_url_redirect": "http://localhost:8080/sales-post-assertion-and-response-sig/saml", + "saml.server.signature": "true", + "saml.assertion.signature": "true", + "saml.signature.algorithm": "RSA_SHA256", + "saml.client.signature": "true", + "saml.authnstatement": "true", + "saml.signing.certificate": "MIIB1DCCAT0CBgFJGP5dZDANBgkqhkiG9w0BAQsFADAwMS4wLAYDVQQDEyVodHRwOi8vbG9jYWxob3N0OjgwODAvc2FsZXMtcG9zdC1zaWcvMB4XDTE0MTAxNjEyNDQyM1oXDTI0MTAxNjEyNDYwM1owMDEuMCwGA1UEAxMlaHR0cDovL2xvY2FsaG9zdDo4MDgwL3NhbGVzLXBvc3Qtc2lnLzCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA1RvGu8RjemSJA23nnMksoHA37MqY1DDTxOECY4rPAd9egr7GUNIXE0y1MokaR5R2crNpN8RIRwR8phQtQDjXL82c6W+NLQISxztarQJ7rdNJIYwHY0d5ri1XRpDP8zAuxubPYiMAVYcDkIcvlbBpwh/dRM5I2eElRK+eSiaMkCUCAwEAATANBgkqhkiG9w0BAQsFAAOBgQCLms6htnPaY69k1ntm9a5jgwSn/K61cdai8R8B0ccY7zvinn9AfRD7fiROQpFyY29wKn8WCLrJ86NBXfgFUGyR5nLNHVy3FghE36N2oHy53uichieMxffE6vhkKJ4P8ChfJMMOZlmCPsQPDvjoAghHt4mriFiQgRdPgIy/zDjSNw==" + } + }, + { + "clientId": "http://localhost:8081/sales-post2/", + "enabled": true, + "fullScopeAllowed": true, + "protocol": "saml", + "baseUrl": "http://localhost:8080/sales-post2", + "redirectUris": [ + "http://localhost:8080/sales-post2/*" + ], + "attributes": { + "saml.authnstatement": "true", + "saml_assertion_consumer_url_post": "http://localhost:8080/sales-post2/saml", + "saml_single_logout_service_url_post": "http://localhost:8080/sales-post2/saml", + "saml_idp_initiated_sso_url_name": "sales-post2", + "saml_idp_initiated_sso_relay_state": "redirectTo=/foo" + } + }, { "clientId": "http://localhost:8081/sales-post/", "enabled": true, diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/web.xml b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/web.xml index 4207f91795..aa2c4b74ab 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/web.xml +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/web.xml @@ -28,6 +28,10 @@ /* + + /error.html + + Application diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/authorization-test/enforcer-config-paths-same-name.json b/testsuite/integration-arquillian/tests/base/src/test/resources/authorization-test/enforcer-config-paths-same-name.json new file mode 100644 index 0000000000..19d8c55fe5 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/authorization-test/enforcer-config-paths-same-name.json @@ -0,0 +1,59 @@ +{ + "realm": "test-realm-authz", + "realm-public-key": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB", + "auth-server-url": "http://localhost:8180/auth", + "ssl-required": "external", + "resource": "test-app-authz", + "bearer-only": true, + "credentials": { + "secret": "secret" + }, + "policy-enforcer": { + "paths": [ + { + "path": "/v1/product/*", + "methods": [ + { + "method": "POST", + "scopes": [ + "create" + ] + } + ] + }, + { + "path": "/v1/product/*", + "methods": [ + { + "method": "GET", + "scopes": [ + "view" + ] + } + ] + }, + { + "path": "/v1/product/*", + "methods": [ + { + "method": "PUT", + "scopes": [ + "update" + ] + } + ] + }, + { + "path": "/v1/product/*", + "methods": [ + { + "method": "DELETE", + "scopes": [ + "delete" + ] + } + ] + } + ] + } +} diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/authorization-test/test-authz-realm.json b/testsuite/integration-arquillian/tests/base/src/test/resources/authorization-test/test-authz-realm.json new file mode 100644 index 0000000000..b1a6f5a07f --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/authorization-test/test-authz-realm.json @@ -0,0 +1,50 @@ +{ + "id": "test-realm-authz", + "realm": "test-realm-authz", + "enabled": true, + "sslRequired": "external", + "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" ], + "users": [ + { + "username": "service-account-test-app-authz", + "enabled": true, + "serviceAccountClientId": "test-app-authz", + "clientRoles": { + "test-app-authz" : ["uma_protection"] + } + } + ], + "clients": [ + { + "clientId": "test-app-authz", + "enabled": true, + "baseUrl": "/test-app-authz", + "adminUrl": "/test-app-authz", + "bearerOnly": false, + "authorizationSettings": { + "allowRemoteResourceManagement": true, + "policyEnforcementMode": "ENFORCING", + "resources": [ + { + "name": "Product Resource", + "uri": "/v1/product/*", + "scopes": [ + { + "name": "view", + "name": "create", + "name": "delete", + "name": "update" + } + ] + } + ] + }, + "redirectUris": [ + "/test-app-authz/*" + ], + "secret": "secret" + } + ] +} diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/client-auth-test/certificate.pem b/testsuite/integration-arquillian/tests/base/src/test/resources/client-auth-test/certificate.pem index 4c589e81ed..a526c93da5 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/resources/client-auth-test/certificate.pem +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/client-auth-test/certificate.pem @@ -1,13 +1,21 @@ -MIICWwIBAAKBgQCqXOsp76WfwXSNCU5AOix6SDhNR7MOdEtawJRmdUcBuJd/QeFh -usLVOwm3a7s1134nlVKBEhOYBzKOY6LulvGDT4wHZUxE6cGJW+dEUd/6jrfZz9Ka -XyATzLUn2BKBQZqRleQPKupwaOGQqQGQ4OmJrWKqG77Rb5O7GLNNDRCGpwIDAQAB -AoGAA4sJLQcZ57erR9+gq/9Ju4VS/nB2Td+nTYzhyTu5LVT8eZ4SJOkwciTXL+Ri -sVz497PIUnzgwXGs4H87/UBQZ4UJNz+kTC3L6nr4b5bWoIxoiY76R+gzlYiNEV9s -QsX+K1FNKKMnEVa7t3DvbCFdlmt+4Agh/KQCU92Q13mj+WECQQDRF+BY1emtFfVC -2miuSWorMLoaKv5At3BMgcs4KPzlfpMiEOMM/m1jmGrqUm40RZlnkhKEqQB0VeqF -27gMGZuxAkEA0JTHqZDfcf5bBYhWzE3+Ec9A22/ZQgCiuV+DmWhCYxHLFsOYPkhT -0jAibfJc8E8VchXMjeDktBAyotylQO1V1wJAcZisLPdCdeOpFMH9/zopcP+PC+px -qY0/eIFHe+JgyvnCS86q3ANaQLKs21MfRkzNtAQbBXGfqpSGzmR5kDFVcQJAX1YR -qMSKDS2IZw/5NGrVnUhQybvm3s8xkW9B1GWeKnAglqgSNy2R+nl70ys7Ja/QCpRC -KftILbFVo7Eca3/PgwJAM5xMmg3Yx/WKkiCREvKXIGKOKN5rGWhHoLnYJHlsOi1r -Z7Bef90rMp2kDrfLUAgrIJH06kxB+3oPRcmgjWlmDQ== +-----BEGIN CERTIFICATE----- +MIIDXTCCAkWgAwIBAgIJAIzE3vQp7EQWMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV +BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX +aWRnaXRzIFB0eSBMdGQwHhcNMTYwMjI5MDgzMDU0WhcNNDMwNzE2MDgzMDU0WjBF +MQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50 +ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB +CgKCAQEAp1+GzdEkt2FZbISXYO12503FL6Oh8s4+tJ2fE66N8IezhugP8xiySDfW +TEMaO5Z2TaTnQQoF9SSZ9Edq1GPxpBX0cdkCOBopEGdlb3hUYDeMaDMs18KGemUc +Fj+CWB5VVcbmWMJ36WCz7FC+Oe38tmujR1AJpJL3pwqazyWIZzPqX8rW+rrNPGKP +C96oBPZMb4RJWivLBJi/o5MGSpo1sJNtxyF4zUUI00LX0wZAV1HH1XErd1Vz41on +nmB+tj9nevVRR4rDV280IELp9Ud0PIb3w843uJtwfSAwVG0pT6hv1VBDrBxTS08N +dPU8CtkQAXzCCr8nqfAbUFOhcWRQgQIDAQABo1AwTjAdBgNVHQ4EFgQUFE+uUZAI +n57ArEylqhCmHkAenTEwHwYDVR0jBBgwFoAUFE+uUZAIn57ArEylqhCmHkAenTEw +DAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEApkgD3OCtw3+sk7GR1YaJ +xNd8HT+fxXmnLqGnCQWX8lRIg5vj1PDMRev6vlIK3JfQV3zajcpKFfpy96klWsJy +ZLYBVW2QOtMzDdQ9I8dS4Pn/SJ/Vo/M/ucfY4ttcuUL3oQCrI/c/u9tcamGMfbwd +658MlXrUvt4B6qXY5AbgUvYR25P86uw7hSFMq5tQftNQsLbOh2FEeIiKhpgI7w8S +SPajaWjUXsfHc5H7f9MciE2NS1Vd3AViGrVWP1rgQ1Iv0UyQVQrnjmIs12ENJmTd +5lDqra5FJhaO7+RUG6er8n8HwXzhHkPmezGqtxWKikjitqvDY9prB3omJSa4Led+ +AQ== +-----END CERTIFICATE----- diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/client-auth-test/publickey.pem b/testsuite/integration-arquillian/tests/base/src/test/resources/client-auth-test/publickey.pem index 906ff55712..3169a04fc4 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/resources/client-auth-test/publickey.pem +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/client-auth-test/publickey.pem @@ -1 +1,9 @@ -MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApnZ/E2BUULHjsRiSnEgZ +4vGe15BRqZPdkHR+NcvVYpThc7JqY6nZrdrwO9sOjlMC5e2Q18Fypi4KbJpGSe9r +0DPgcbPsHSoe2xFO3M8XBE0DyoRblaQFhe6p/sj3ak32k2zn+fMZUmlx/MTNQh1I +Cki7So0NDCBXt8XGZNnEyvKeXOUZP5qicP9KxVAQiWJvlkaTjc8rrRTmf+HWw/Qf +gQC0tzBRpa7T+RpW9O+rnWfOaNfTkTb9itIc+ZOa2Z4iidZ7+ifMOp9cNT641Wb6 +iYqJ2ufqY+msxI54tYM1tPgGS7r4SnCwmnqTaO383wXUl8TQ7qStmAWIepV3nNyu +AQIDAQAB +-----END PUBLIC KEY----- \ No newline at end of file diff --git a/testsuite/integration-arquillian/tests/other/adapters/jboss/eap6/src/test/java/org/keycloak/testsuite/adapter/EAP6SAMLFilterAdapterTest.java b/testsuite/integration-arquillian/tests/other/adapters/jboss/eap6/src/test/java/org/keycloak/testsuite/adapter/EAP6SAMLFilterAdapterTest.java index 627e5f3d8e..1dcf846eb3 100644 --- a/testsuite/integration-arquillian/tests/other/adapters/jboss/eap6/src/test/java/org/keycloak/testsuite/adapter/EAP6SAMLFilterAdapterTest.java +++ b/testsuite/integration-arquillian/tests/other/adapters/jboss/eap6/src/test/java/org/keycloak/testsuite/adapter/EAP6SAMLFilterAdapterTest.java @@ -8,5 +8,5 @@ import org.keycloak.testsuite.arquillian.annotation.UseServletFilter; * @author mhajas */ @AppServerContainer("app-server-eap6") -public class EAPSAMLFilterAdapterTest extends AbstractSAMLFilterServletAdapterTest { +public class EAP6SAMLFilterAdapterTest extends AbstractSAMLFilterServletAdapterTest { } diff --git a/testsuite/integration-arquillian/tests/other/adapters/pom.xml b/testsuite/integration-arquillian/tests/other/adapters/pom.xml index 9c49390b93..15fc1c4836 100644 --- a/testsuite/integration-arquillian/tests/other/adapters/pom.xml +++ b/testsuite/integration-arquillian/tests/other/adapters/pom.xml @@ -172,6 +172,18 @@
+ + org.jboss.shrinkwrap.resolver + shrinkwrap-resolver-maven-plugin + ${version.shrinkwrap.resolvers} + + + + propagate-execution-context + + + + diff --git a/testsuite/integration-arquillian/tests/other/console/pom.xml b/testsuite/integration-arquillian/tests/other/console/pom.xml index d9232e8c0c..2459c6d98d 100644 --- a/testsuite/integration-arquillian/tests/other/console/pom.xml +++ b/testsuite/integration-arquillian/tests/other/console/pom.xml @@ -36,6 +36,26 @@ + + + org.codehaus.mojo + properties-maven-plugin + 1.0.0 + + + process-resources + + read-project-properties + + + + ${testsuite.constants} + + + + + + @@ -48,10 +68,11 @@ copy-resources - ${auth.server.home}/themes + ${keycloak.theme.dir} src/main/resources/themes + true 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 41e62c9413..d19bae7054 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 @@ -45,6 +45,10 @@ public class KerberosUserProviderForm extends Form { setInputValue(consoleDisplayNameInput, name); } + public String getConsoleDisplayNameInput() { + return getInputValue(consoleDisplayNameInput); + } + public void setPriorityInput(Integer priority) { setInputValue(priorityInput, String.valueOf(priority)); } diff --git a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/federation/UserFederation.java b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/federation/UserFederation.java index c0dd0781e4..06405eefb7 100644 --- a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/federation/UserFederation.java +++ b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/federation/UserFederation.java @@ -2,13 +2,18 @@ package org.keycloak.testsuite.console.page.federation; import org.jboss.arquillian.graphene.findby.FindByJQuery; import org.keycloak.testsuite.console.page.AdminConsoleRealm; +import org.keycloak.testsuite.console.page.fragment.DataTable; +import org.keycloak.testsuite.console.page.fragment.ModalDialog; +import org.keycloak.testsuite.util.UIUtils; import org.openqa.selenium.By; +import org.openqa.selenium.support.FindBy; import org.openqa.selenium.support.ui.Select; import static org.keycloak.testsuite.util.WaitUtils.waitUntilElement; /** - * Created by fkiss. + * @author fkiss + * @author Vaclav Muzikar */ public class UserFederation extends AdminConsoleRealm { @@ -20,9 +25,37 @@ public class UserFederation extends AdminConsoleRealm { @FindByJQuery("select[ng-model*='selectedProvider']") private Select addProviderSelect; - public void addProvider(String provider) { - waitUntilElement(By.cssSelector("select[ng-model*='selectedProvider']")).is().present(); + @FindBy(xpath = "//div[./h1/span[text()='User Federation']]/table") + private FederationsTable federationsTable; + + public FederationsTable table() { + return federationsTable; + } + + public void addFederation(String provider) { addProviderSelect.selectByVisibleText(provider); } + public boolean hasProvider(String provider) { + return UIUtils.selectContainsOption(addProviderSelect, provider); + } + + public class FederationsTable extends DataTable { + @FindBy(xpath = "//div[@class='modal-dialog']") + private ModalDialog modalDialog; + + public void editFederation(String federation) { + clickRowActionButton(getRowByLinkText(federation), "Edit"); + } + + public void removeFederation(String federation) { + clickRowActionButton(getRowByLinkText(federation), "Delete"); + modalDialog.confirmDeletion(); + } + + public int getFederationsCount() { + return rows().size(); + } + } + } diff --git a/testsuite/integration-arquillian/tests/other/console/src/main/resources/themes/internat-test/account/messages/messages_cs.properties b/testsuite/integration-arquillian/tests/other/console/src/main/resources/themes/internat-test/account/messages/messages_cs.properties new file mode 100644 index 0000000000..f2fd7e02cd --- /dev/null +++ b/testsuite/integration-arquillian/tests/other/console/src/main/resources/themes/internat-test/account/messages/messages_cs.properties @@ -0,0 +1,5 @@ +#encoding: utf-8 +doSave=Uložit +doCancel=Zrušit +editAccountHtmlTitle=Upravit účet +locale_cs=Čeština diff --git a/testsuite/integration-arquillian/tests/other/console/src/main/resources/themes/internat-test/account/messages/messages_en.properties b/testsuite/integration-arquillian/tests/other/console/src/main/resources/themes/internat-test/account/messages/messages_en.properties new file mode 100644 index 0000000000..cc26e68227 --- /dev/null +++ b/testsuite/integration-arquillian/tests/other/console/src/main/resources/themes/internat-test/account/messages/messages_en.properties @@ -0,0 +1,2 @@ +#encoding: utf-8 +locale_cs=Čeština diff --git a/testsuite/integration-arquillian/tests/other/console/src/main/resources/themes/internat-test/account/theme.properties b/testsuite/integration-arquillian/tests/other/console/src/main/resources/themes/internat-test/account/theme.properties new file mode 100644 index 0000000000..7bdb8fe5ec --- /dev/null +++ b/testsuite/integration-arquillian/tests/other/console/src/main/resources/themes/internat-test/account/theme.properties @@ -0,0 +1,2 @@ +parent=${theme-default-name} +locales=en,cs diff --git a/testsuite/integration-arquillian/tests/other/console/src/main/resources/themes/internat-test/admin/messages/admin-messages_cs.properties b/testsuite/integration-arquillian/tests/other/console/src/main/resources/themes/internat-test/admin/messages/admin-messages_cs.properties new file mode 100644 index 0000000000..9a9ef0e171 --- /dev/null +++ b/testsuite/integration-arquillian/tests/other/console/src/main/resources/themes/internat-test/admin/messages/admin-messages_cs.properties @@ -0,0 +1,7 @@ +#encoding: utf-8 +enabled=Povolit +save=Uložit +cancel=Zrušit +onText=ZAP +offText=VYP +realm-settings=Nastavení Realmu diff --git a/testsuite/integration-arquillian/tests/other/console/src/main/resources/themes/internat-test/admin/messages/messages_cs.properties b/testsuite/integration-arquillian/tests/other/console/src/main/resources/themes/internat-test/admin/messages/messages_cs.properties new file mode 100644 index 0000000000..e03207609e --- /dev/null +++ b/testsuite/integration-arquillian/tests/other/console/src/main/resources/themes/internat-test/admin/messages/messages_cs.properties @@ -0,0 +1,2 @@ +#encoding: utf-8 +invalidPasswordMinLengthMessage=Neplatné heslo: musí mít alespoň {0} znaků. diff --git a/testsuite/integration-arquillian/tests/other/console/src/main/resources/themes/internat-test/admin/theme.properties b/testsuite/integration-arquillian/tests/other/console/src/main/resources/themes/internat-test/admin/theme.properties new file mode 100644 index 0000000000..7bdb8fe5ec --- /dev/null +++ b/testsuite/integration-arquillian/tests/other/console/src/main/resources/themes/internat-test/admin/theme.properties @@ -0,0 +1,2 @@ +parent=${theme-default-name} +locales=en,cs diff --git a/testsuite/integration-arquillian/tests/other/console/src/main/resources/themes/internat-test/email/theme.properties b/testsuite/integration-arquillian/tests/other/console/src/main/resources/themes/internat-test/email/theme.properties new file mode 100644 index 0000000000..7bdb8fe5ec --- /dev/null +++ b/testsuite/integration-arquillian/tests/other/console/src/main/resources/themes/internat-test/email/theme.properties @@ -0,0 +1,2 @@ +parent=${theme-default-name} +locales=en,cs diff --git a/testsuite/integration-arquillian/tests/other/console/src/main/resources/themes/internat-test/login/messages/messages_cs.properties b/testsuite/integration-arquillian/tests/other/console/src/main/resources/themes/internat-test/login/messages/messages_cs.properties new file mode 100644 index 0000000000..3c43a7bd92 --- /dev/null +++ b/testsuite/integration-arquillian/tests/other/console/src/main/resources/themes/internat-test/login/messages/messages_cs.properties @@ -0,0 +1,11 @@ +#encoding: utf-8 +doLogIn=Přihlásit +doRegister=Registrovat se +doCancel=Zrušit +doSubmit=Odeslat +doForgotPassword=Zapomněli jste heslo? +locale_cs=Čeština +username=Uživatelské jméno +usernameOrEmail=Uživatelské jméno nebo email +email=Email +password=Heslo diff --git a/testsuite/integration-arquillian/tests/other/console/src/main/resources/themes/internat-test/login/messages/messages_en.properties b/testsuite/integration-arquillian/tests/other/console/src/main/resources/themes/internat-test/login/messages/messages_en.properties new file mode 100644 index 0000000000..cc26e68227 --- /dev/null +++ b/testsuite/integration-arquillian/tests/other/console/src/main/resources/themes/internat-test/login/messages/messages_en.properties @@ -0,0 +1,2 @@ +#encoding: utf-8 +locale_cs=Čeština diff --git a/testsuite/integration-arquillian/tests/other/console/src/main/resources/themes/internat-test/login/theme.properties b/testsuite/integration-arquillian/tests/other/console/src/main/resources/themes/internat-test/login/theme.properties new file mode 100644 index 0000000000..7bdb8fe5ec --- /dev/null +++ b/testsuite/integration-arquillian/tests/other/console/src/main/resources/themes/internat-test/login/theme.properties @@ -0,0 +1,2 @@ +parent=${theme-default-name} +locales=en,cs diff --git a/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/federation/UserFederationTest.java b/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/federation/UserFederationTest.java new file mode 100644 index 0000000000..d136a0f354 --- /dev/null +++ b/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/federation/UserFederationTest.java @@ -0,0 +1,58 @@ +package org.keycloak.testsuite.console.federation; + +import org.jboss.arquillian.graphene.page.Page; +import org.junit.Before; +import org.junit.Test; +import org.keycloak.testsuite.console.AbstractConsoleTest; +import org.keycloak.testsuite.console.page.federation.CreateKerberosUserProvider; +import org.keycloak.testsuite.console.page.federation.UserFederation; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +/** + * @author Vaclav Muzikar + */ +public class UserFederationTest extends AbstractConsoleTest { + @Page + private UserFederation userFederationPage; + + @Page + private CreateKerberosUserProvider createKerberosUserProviderPage; + + @Before + public void beforeUserFederationTest() { + userFederationPage.navigateTo(); + } + + @Test + public void availableProviders() { + assertTrue(userFederationPage.hasProvider("ldap")); + assertTrue(userFederationPage.hasProvider("kerberos")); + } + + @Test + public void addRemoveFederation() { + final String federationName = "KerberosProvider"; + + userFederationPage.addFederation("kerberos"); + assertTrue(createKerberosUserProviderPage.isCurrent()); + + createKerberosUserProviderPage.form().setConsoleDisplayNameInput(federationName); + createKerberosUserProviderPage.form().setKerberosRealmInput("KEYCLOAK.ORG"); + createKerberosUserProviderPage.form().setServerPrincipalInput("HTTP/localhost@KEYCLOAK.ORG"); + createKerberosUserProviderPage.form().setKeyTabInput("http.keytab"); + createKerberosUserProviderPage.form().save(); + assertAlertSuccess(); + userFederationPage.navigateTo(); + + assertEquals(1, userFederationPage.table().getFederationsCount()); + + userFederationPage.table().editFederation(federationName); + assertEquals(federationName, createKerberosUserProviderPage.form().getConsoleDisplayNameInput()); + userFederationPage.navigateTo(); + + userFederationPage.table().removeFederation(federationName); + assertEquals(0, userFederationPage.table().getFederationsCount()); + } +} diff --git a/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/realm/InternationalizationTest.java b/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/realm/InternationalizationTest.java index 090288b453..97cb3086bc 100644 --- a/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/realm/InternationalizationTest.java +++ b/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/realm/InternationalizationTest.java @@ -4,6 +4,7 @@ import org.jboss.arquillian.graphene.page.Page; import org.junit.After; import org.junit.Before; import org.junit.Test; +import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.testsuite.auth.page.AuthRealm; import org.keycloak.testsuite.console.page.fragment.Dropdown; import org.keycloak.testsuite.console.page.realm.ThemeSettings; @@ -17,6 +18,13 @@ import static org.keycloak.testsuite.util.URLAssert.*; * @author Vaclav Muzikar */ public class InternationalizationTest extends AbstractRealmTest { + private static final String THEME_NAME = "internat-test"; + private static final String LOCALE_CS_NAME = "Čeština"; + + private static final String LABEL_CS_PASSWORD = "Heslo"; + private static final String LABEL_CS_REALM_SETTINGS = "Nastavení Realmu"; + private static final String LABEL_CS_EDIT_ACCOUNT = "Upravit účet"; + @Page private ThemeSettings themeSettingsPage; @@ -25,6 +33,13 @@ public class InternationalizationTest extends AbstractRealmTest { @Before public void beforeInternationalizationTest() { + RealmRepresentation realmRepresentation = testRealmResource().toRepresentation(); + realmRepresentation.setAccountTheme(THEME_NAME); + realmRepresentation.setAdminTheme(THEME_NAME); + realmRepresentation.setEmailTheme(THEME_NAME); + realmRepresentation.setLoginTheme(THEME_NAME); + testRealmResource().update(realmRepresentation); + realmSettingsPage.navigateTo(); tabs().themes(); themeSettingsPage.setInternatEnabled(true); @@ -47,14 +62,14 @@ public class InternationalizationTest extends AbstractRealmTest { public void loginInternationalization() { testRealmAdminConsolePage.navigateTo(); - localeDropdown.selectByText("Español"); - assertLocale(".//label[@for='password']", "Contraseña"); // Password + localeDropdown.selectByText(LOCALE_CS_NAME); + assertLocale(".//label[@for='password']", LABEL_CS_PASSWORD); loginToTestRealmConsoleAs(testUser); - assertConsoleLocale("Temas"); + assertConsoleLocale(LABEL_CS_REALM_SETTINGS); testRealmAccountPage.navigateTo(); - assertAccountLocale("Cuenta"); + assertAccountLocale(LABEL_CS_EDIT_ACCOUNT); } /** @@ -65,24 +80,24 @@ public class InternationalizationTest extends AbstractRealmTest { testRealmAccountPage.navigateTo(); loginPage.form().login(testUser); - localeDropdown.selectByText("Français"); + localeDropdown.selectByText(LOCALE_CS_NAME); testRealmAccountPage.navigateTo(); - assertAccountLocale("Compte"); + assertAccountLocale(LABEL_CS_EDIT_ACCOUNT); deleteAllCookiesForTestRealm(); loginToTestRealmConsoleAs(testUser); - assertConsoleLocale("Thèmes"); + assertConsoleLocale(LABEL_CS_REALM_SETTINGS); } private void assertConsoleLocale(String expected) { assertCurrentUrlEquals(realmSettingsPage); - assertLocale(".//a[contains(@href,'/theme-settings')]", expected); // Themes + assertLocale(".//div[@class='nav-category'][1]/ul/li[1]//a", expected); // Realm Settings } private void assertAccountLocale(String expected) { assertCurrentUrlEquals(testRealmAccountPage); - assertLocale(".//div[contains(@class,'bs-sidebar')]/ul/li", expected); // Account + assertLocale(".//div[contains(@class,'content-area')]/div[@class='row']/div/h2", expected); // Edit Account } private void assertLocale(String xpathSelector, String expected) { diff --git a/testsuite/integration-arquillian/tests/other/pom.xml b/testsuite/integration-arquillian/tests/other/pom.xml index bb238bf620..6b0f83c7f1 100644 --- a/testsuite/integration-arquillian/tests/other/pom.xml +++ b/testsuite/integration-arquillian/tests/other/pom.xml @@ -59,7 +59,7 @@ integration-arquillian-tests-base ${project.version} test-jar - arquillian.xml,keycloak-add-user.json,kerberos/* + arquillian.xml,keycloak-add-user.json,test-constants.properties,kerberos/* diff --git a/testsuite/integration-arquillian/tests/pom.xml b/testsuite/integration-arquillian/tests/pom.xml index 6c250598a4..b2ee848b26 100755 --- a/testsuite/integration-arquillian/tests/pom.xml +++ b/testsuite/integration-arquillian/tests/pom.xml @@ -74,7 +74,7 @@ true true - test-constants.properties + ${project.build.directory}/dependency/test-constants.properties @@ -780,6 +780,14 @@ org.keycloak keycloak-adapter-spi + + org.keycloak + keycloak-saml-adapter-api-public + + + org.keycloak + keycloak-authz-client + diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/KeycloakServer.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/KeycloakServer.java index 3b8d68b9b2..8806514a70 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/KeycloakServer.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/KeycloakServer.java @@ -309,6 +309,8 @@ public class KeycloakServer { di.setDeploymentName("Keycloak"); di.setDefaultEncoding("UTF-8"); + di.addInitParameter("keycloak.embedded", "true"); + di.setDefaultServletConfig(new DefaultServletConfig(true)); ServletInfo restEasyDispatcher = Servlets.servlet("Keycloak REST Interface", HttpServlet30Dispatcher.class); 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 d3f87c98d1..b676b99c12 100755 --- a/testsuite/integration/src/test/resources/META-INF/keycloak-server.json +++ b/testsuite/integration/src/test/resources/META-INF/keycloak-server.json @@ -65,7 +65,8 @@ "driverDialect": "${keycloak.connectionsJpa.driverDialect:}", "user": "${keycloak.connectionsJpa.user:sa}", "password": "${keycloak.connectionsJpa.password:}", - "databaseSchema": "${keycloak.connectionsJpa.databaseSchema:update}", + "initializeEmpty": true, + "migrationStrategy": "update", "showSql": "${keycloak.connectionsJpa.showSql:false}", "formatSql": "${keycloak.connectionsJpa.formatSql:true}", "globalStatsInterval": "${keycloak.connectionsJpa.globalStatsInterval:-1}" diff --git a/themes/src/main/resources/theme/base/account/messages/messages_en.properties b/themes/src/main/resources/theme/base/account/messages/messages_en.properties index 0bb466db63..7e1715d6dd 100755 --- a/themes/src/main/resources/theme/base/account/messages/messages_en.properties +++ b/themes/src/main/resources/theme/base/account/messages/messages_en.properties @@ -157,6 +157,7 @@ locale_en=English locale_es=Espa\u00F1ol locale_fr=Fran\u00e7ais locale_it=Italian +locale_ja=\u65E5\u672C\u8A9E locale_lt=Lietuvi\u0173 locale_pt-BR=Portugu\u00EAs (Brasil) locale_ru=\u0420\u0443\u0441\u0441\u043A\u0438\u0439 \ No newline at end of file diff --git a/themes/src/main/resources/theme/base/account/messages/messages_ja.properties b/themes/src/main/resources/theme/base/account/messages/messages_ja.properties new file mode 100644 index 0000000000..82c9cb537b --- /dev/null +++ b/themes/src/main/resources/theme/base/account/messages/messages_ja.properties @@ -0,0 +1,153 @@ +# encoding: utf-8 +doSave=保存 +doCancel=キャンセル +doLogOutAllSessions=全セッションからログアウト +doRemove=削除 +doAdd=追加 +doSignOut=サインアウト + +editAccountHtmlTitle=アカウントの編集 +federatedIdentitiesHtmlTitle=Federated Identities +accountLogHtmlTitle=アカウントログ +changePasswordHtmlTitle=パスワード変更 +sessionsHtmlTitle=セッション +accountManagementTitle=Keycloak アカウント管理 +authenticatorTitle=Authenticator +applicationsHtmlTitle=アプリケーション + +authenticatorCode=ワンタイムコード +email=Eメール +firstName=名 +givenName=名 +fullName=氏名 +lastName=姓 +familyName=姓 +password=パスワード +passwordConfirm=新しいパスワード (確認) +passwordNew=新しいパスワード +username=ユーザー名 +address=住所 +street=番地 +locality=市区町村 +region=都道府県 +postal_code=郵便番号 +country=国 +emailVerified=確認済みEメール +gssDelegationCredential=GSS 代行クレデンシャル + +role_admin=管理者 +role_realm-admin=レルムの管理 +role_create-realm=レルムの作成 +role_view-realm=レルムの参照 +role_view-users=ユーザーの参照 +role_view-applications=アプリケーションの参照 +role_view-clients=クライアントの参照 +role_view-events=イベントの参照 +role_view-identity-providers=アイデンティティ プロバイダーの参照 +role_manage-realm=レルムの管理 +role_manage-users=ユーザーの管理 +role_manage-applications=アプリケーションの管理 +role_manage-identity-providers=アイデンティティ プロバイダーの管理 +role_manage-clients=クライアントの管理 +role_manage-events=イベントの管理 +role_view-profile=プロフィールの参照 +role_manage-account=アカウントの管理 +role_read-token=トークンの参照 +role_offline-access=オフラインアクセス +role_uma_authorization=アクセス権の取得 +client_account=アカウント +client_security-admin-console=セキュリティ管理コンソール +client_admin-cli=管理 CLI +client_realm-management=レルム管理 +client_broker=ブローカー + + +requiredFields=必須 +allFieldsRequired=全ての入力項目が必須 + +backToApplication=« アプリケーションに戻る +backTo={0} に戻る + +date=日付 +event=イベント +ip=IP +client=クライアント +clients=クライアント +details=詳細 +started=開始 +lastAccess=最終アクセス +expires=有効期限 +applications=アプリケーション + +account=アカウント +federatedIdentity=Federated Identity +authenticator=Authenticator +sessions=セッション +log=ログ + +application=アプリケーション +availablePermissions=使用可能なアクセス権 +grantedPermissions=許可されたアクセス権 +grantedPersonalInfo=許可された個人情報 +additionalGrants=追加の許可 +action=アクション +inResource=in +fullAccess=フルアクセス +offlineToken=オフライントークン +revoke=許可の取り消し + +configureAuthenticators=設定済みの Authenticator +mobile=モバイル +totpStep1=FreeOTP または Google Authenticator (Google認証システム) をご自身のデバイスにインストールしてください。これらのアプリケーションは Google Play と Apple App Store で入手できます。 +totpStep2=アプリケーションを開きバーコードをスキャンするかキーを入力してください。 +totpStep3=アプリケーションで提供されたワンタイムコードを入力して保存をクリックし、セットアップを完了してください。 + +missingUsernameMessage=ユーザー名を入力してください。 +missingFirstNameMessage=名を入力してください。 +invalidEmailMessage=無効なメールアドレスです。 +missingLastNameMessage=姓を入力してください。 +missingEmailMessage=Eメールを入力してください。 +missingPasswordMessage=パスワードを入力してください。 +notMatchPasswordMessage=パスワードが一致していません。 + +missingTotpMessage=Authenticator コードを入力してください。 +invalidPasswordExistingMessage=無効な既存のパスワードです。 +invalidPasswordConfirmMessage=新しいパスワード (確認) と一致していません。 +invalidTotpMessage=無効な Authenticator コードです。 + +usernameExistsMessage=既に存在するユーザー名です。 +emailExistsMessage=既に存在するEメールです。 + +readOnlyUserMessage=リードオンリーのためアカウントを更新することはできません。 +readOnlyPasswordMessage=リードオンリーのためパスワードを更新することはできません。 + +successTotpMessage=モバイル Authenticator が設定されました。 +successTotpRemovedMessage=モバイル Authenticator が削除されました。 + +successGrantRevokedMessage=許可が正常に取り消しされました。 + +accountUpdatedMessage=アカウントが更新されました。 +accountPasswordUpdatedMessage=パスワードが更新されました。 + +missingIdentityProviderMessage=アイデンティティ プロバイダーが指定されていません。 +invalidFederatedIdentityActionMessage=無効または存在しないアクションです。 +identityProviderNotFoundMessage=指定されたアイデンティティ プロバイダーが見つかりません。 +federatedIdentityLinkNotActiveMessage=このアイデンティティは有効ではありません。 +federatedIdentityRemovingLastProviderMessage=パスワードがないため最後の Federated Identity を削除できません。 +identityProviderRedirectErrorMessage=アイデンティティ プロバイダーへのリダイレクトに失敗しました。 +identityProviderRemovedMessage=アイデンティティ プロバイダーが正常に削除されました。 +identityProviderAlreadyLinkedMessage={0}から返された Federated Identity は既に他のユーザーに関連付けされています。 +staleCodeAccountMessage=有効期限切れです。再度お試しください。 +consentDenied=同意が拒否されました。 + +accountDisabledMessage=アカウントが無効です。管理者に連絡してください。 + +accountTemporarilyDisabledMessage=アカウントが一時的に無効です。管理者に連絡、またはしばらく時間をおいてから再度お試しください。 +invalidPasswordMinLengthMessage=無効なパスワード: 最小 {0} の長さが必要です。 +invalidPasswordMinLowerCaseCharsMessage=無効なパスワード: 少なくとも {0} 文字の小文字を含む必要があります。 +invalidPasswordMinDigitsMessage=無効なパスワード: 少なくとも {0} 文字の数字を含む必要があります。 +invalidPasswordMinUpperCaseCharsMessage=無効なパスワード: 少なくとも {0} 文字の大文字を含む必要があります。 +invalidPasswordMinSpecialCharsMessage=無効なパスワード: 少なくとも {0} 文字の特殊文字を含む必要があります。 +invalidPasswordNotUsernameMessage=無効なパスワード: ユーザー名と同じパスワードは禁止されています。 +invalidPasswordRegexPatternMessage=無効なパスワード: 正規表現パターンと一致しません。 +invalidPasswordHistoryMessage=無効なパスワード: 最近の {0} パスワードのいずれかと同じパスワードは禁止されています。 \ No newline at end of file diff --git a/themes/src/main/resources/theme/base/account/theme.properties b/themes/src/main/resources/theme/base/account/theme.properties index db32af801b..ff4018c4f5 100644 --- a/themes/src/main/resources/theme/base/account/theme.properties +++ b/themes/src/main/resources/theme/base/account/theme.properties @@ -1 +1 @@ -locales=ca,de,en,es,fr,it,lt,pt-BR,ru \ No newline at end of file +locales=ca,de,en,es,fr,it,ja,lt,pt-BR,ru \ No newline at end of file diff --git a/themes/src/main/resources/theme/base/admin/messages/admin-messages_ja.properties b/themes/src/main/resources/theme/base/admin/messages/admin-messages_ja.properties new file mode 100644 index 0000000000..f99e168ed9 --- /dev/null +++ b/themes/src/main/resources/theme/base/admin/messages/admin-messages_ja.properties @@ -0,0 +1,1121 @@ +# encoding: utf-8 +consoleTitle=Keycloak 管理コンソール + +# Common messages +enabled=有効 +name=名前 +displayName=表示名 +displayNameHtml=HTML 表示名 +save=保存 +cancel=キャンセル +onText=オン +offText=オフ +client=クライアント +clients=クライアント +clear=クリア +selectOne=1つ選択... + +true=はい +false=いいえ + +endpoints=エンドポイント + +# Realm settings +realm-detail.enabled.tooltip=有効の場合は、ユーザーとクライアントはこのレルムのみアクセス可能になります +realm-detail.oidc-endpoints.tooltip=OpenID Connect エンドポイントの設定を表示します。 +registrationAllowed=ユーザー登録 +registrationAllowed.tooltip=登録ページの有効/無効。ログインページに登録のリンクも表示されるようになります。 +registrationEmailAsUsername=Eメールをユーザー名とする +registrationEmailAsUsername.tooltip=有効の場合は、登録フォームにおいてユーザー名フィールドが非表示なり、Eメールが新規ユーザーのユーザー名として使われます。 +editUsernameAllowed=ユーザー名の編集 +editUsernameAllowed.tooltip=有効の場合はユーザー名フィールドが編集可能になり、そうでない場合は読み取り専用になります。 +resetPasswordAllowed=パスワード忘れ +resetPasswordAllowed.tooltip=クレデンシャルを忘れてしまった場合にクリックするリンクをログインページに表示します。 +rememberMe=ログイン状態の保存 +rememberMe.tooltip=セッションの有効期限が切れるまではブラウザーの再起動でもログイン状態を保存するチェックボックスをログインページに表示します。 +verifyEmail=Eメールの確認 +verifyEmail.tooltip=初回ログイン時にメールアドレスの確認をユーザーに要求します。 +sslRequired=SSL の要求 +sslRequired.option.all=全てのリクエスト +sslRequired.option.external=外部リクエスト +sslRequired.option.none=なし +sslRequired.tooltip=HTTPS は必須ですか? 「なし」 は HTTPS がどのクライアント IP アドレスにも要求されないことを意味します。 「外部リクエスト」 はローカルホストとプライベート IP アドレスは HTTPS なしでアクセスできることを意味します。 「すべてのリクエスト」 は HTTPS がすべての IP アドレスに要求されることを意味します。 +publicKey=公開鍵 +privateKey=秘密鍵 +gen-new-keys=新しい鍵を生成する +certificate=証明書 +host=ホスト +smtp-host=SMTP ホスト +port=ポート +smtp-port=SMTP ポート (デフォルトは25) +from=差出人 +sender-email-addr=送信者のメールアドレス +enable-ssl=SSL の有効 +enable-start-tls=StartTLS の有効 +enable-auth=認証の有効 +username=ユーザー名 +login-username=ログインユーザー名 +password=パスワード +login-password=ログインパスワード +login-theme=ログインテーマ +login-theme.tooltip=ログイン、TOTP、グラント、登録、パスワード忘れ画面のテーマを選択します。 +account-theme=アカウントテーマ +account-theme.tooltip=ユーザーアカウント管理画面のテーマを選択します。 +admin-console-theme=管理コンソールテーマ +select-theme-admin-console=管理コンソールのテーマを選択します。 +email-theme=Eメールテーマ +select-theme-email=サーバーから送信されるEメールのテーマを選択します。 +i18n-enabled=国際化の有効 +supported-locales=サポートされるロケール +supported-locales.placeholder=ロケールを入力し Enter キーを押してください +default-locale=デフォルトロケール +realm-cache-clear=レルムキャッシュ +realm-cache-clear.tooltip=レルムキャッシュからすべてのエントリをクリアする (すべてのレルムのエントリをクリアします) +user-cache-clear=ユーザーキャッシュ +user-cache-clear.tooltip=ユーザーキャッシュからすべてのエントリを削除します (すべてのレルムのエントリをクリアします) +revoke-refresh-token=リフレッシュトークンの取り消し +revoke-refresh-token.tooltip=有効の場合は、リフレッシュトークンは1回のみ使用可能になります。無効の場合は、リフレッシュトークンは使用されても取り消しされず、何度でも使用可能になります。 +sso-session-idle=SSO セッションアイドル +seconds=秒 +minutes=分 +hours=時 +days=日 +sso-session-max=SSO セッション最大 +sso-session-idle.tooltip=セッションの有効期限が切れるまでのアイドル時間です。セッションが有効期限切れの際はトークンとブラウザーセッションは無効化されます。 +sso-session-max.tooltip=セッションの有効期限が切れるまでの最大時間です。セッションが有効期限切れの際はトークンとブラウザーセッションは無効化されます。 +offline-session-idle=オフラインセッションアイドル +offline-session-idle.tooltip=セッションの有効期限が切れるまでのオフライン時間です。この期限内に少なくとも1回はオフライントークンを使用してリフレッシュしないと、オフラインセッションは有効期限切れとなります。 +access-token-lifespan=アクセストークン生存期間 +access-token-lifespan.tooltip=アクセストークンが有効期限切れとなる最大時間です。この値は SSO タイムアウトと比べて短くすることをお勧めします。 +access-token-lifespan-for-implicit-flow=Implicit Flow におけるアクセストークン生存期間 +access-token-lifespan-for-implicit-flow.tooltip=OpenID Connect Implicit Flow で発行されたアクセストークンが有効期限切れとなる最大時間です。この値は SSO タイムアウトより短くすることをお勧めします。 Implicit Flow ではトークンを更新することはありませんので、 「アクセストークン生存期間」 とは異なる別々のタイムアウト設定を設けています。 +client-login-timeout=クライアントのログインタイムアウト +client-login-timeout.tooltip=クライアントがアクセストークンプロトコルを完了するまでの最大時間です。これは通常は1分にすべきです。 +login-timeout=ログインタイムアウト +login-timeout.tooltip=ユーザーがログインを完了するまでの最大時間です。これは30分以上と比較的長くすることをお勧めします。 +login-action-timeout=ログインアクションタイムアウト +login-action-timeout.tooltip=ユーザーがパスワードの更新や TOTP の設定のようなログインに関係するアクションを完了するまでの最大時間です。これは5分以上と比較的長くすることをお勧めします。 +headers=ヘッダー +brute-force-detection=ブルートフォースの検出 +x-frame-options=X-Frame-Options +x-frame-options-tooltip=デフォルト値では別のオリジンの IFrame からの読み込みを防ぎます (詳細はラベルをクリックしてください) +content-sec-policy=Content-Security-Policy +content-sec-policy-tooltip=デフォルト値では別のオリジンの IFrame からの読み込みを防ぎます (詳細はラベルをクリックしてください) +content-type-options=X-Content-Type-Options +content-type-options-tooltip=デフォルト値では Internet Explorer と Google Chrome に対して、宣言された content-type を避けてレスポンスの MIME-sniffing を行うことを防ぎます (詳細はラベルをクリックしてください) +max-login-failures=最大ログイン失敗回数 +max-login-failures.tooltip=検出するまでの失敗回数です。 +wait-increment=連続失敗時の待機時間 +wait-increment.tooltip=失敗回数が閾値に達した場合、どれくらいの時間ユーザーはロックアウトされるか設定します。 +quick-login-check-millis=クイックログイン試行間のミリ秒数チェック +quick-login-check-millis.tooltip=クイックログイン失敗があまりにも頻繁に発生した場合は、ユーザーをロックアウトします。 +min-quick-login-wait=クイックログイン失敗時の最小待機時間 +min-quick-login-wait.tooltip=クイックログイン失敗後にどれくらいの時間待機するか設定します。 +max-wait=最大待機時間 +max-wait.tooltip=ユーザーがロックアウトされる最大待機時間を設定します。 +failure-reset-time=ログイン失敗回数のリセット時間 +failure-reset-time.tooltip=いつ失敗回数がリセットされるか設定します。 +realm-tab-login=ログイン +realm-tab-keys=鍵 +realm-tab-email=Eメール +realm-tab-themes=テーマ +realm-tab-cache=キャッシュ +realm-tab-tokens=トークン +realm-tab-client-initial-access=初期アクセストークン +realm-tab-security-defenses=セキュリティ防御 +realm-tab-general=一般 +add-realm=レルムの追加 + +#Session settings +realm-sessions=レルムセッション +revocation=取り消し +logout-all=すべてログアウトする +active-sessions=有効なセッション +sessions=セッション +not-before=この日時より前 +not-before.tooltip=この日時より前に発行されたトークンを取り消します。 +set-to-now=現在日時を設定 +push=プッシュ +push.tooltip=管理URLを持つすべてのクライアントに対して、新しい取り消しポリシーを通知します。 + +#Protocol Mapper +usermodel.prop.label=プロパティ +usermodel.prop.tooltip=UserModel インタフェースのプロパティメソッドの名前です。例えば、 「email」 の値は UserModel.getEmail() メソッドを参照しています。 +usermodel.attr.label=ユーザー属性 +usermodel.attr.tooltip=格納されるユーザー属性名、UserMode.attribute マップ内の属性名です。 +userSession.modelNote.label=ユーザーセッションノート +userSession.modelNote.tooltip=UserSessionModel.note マップ内のユーザーセッションノート名です。 +multivalued.label=マルチバリュー +multivalued.tooltip=属性がマルチバリューをサポートしているかどうかを示します。サポートしている場合は、この属性のすべての値リストがクレームとして設定されます。サポートしていない場合は、最初の値だけがクレームとして設定されます。 +selectRole.label=ロールの選択 +selectRole.tooltip=左側にあるテキストボックスにロールを入力するか、ブラウズして必要なロールを選択するためにこのボタンをクリックしてください。 +tokenClaimName.label=トークンクレーム名 +tokenClaimName.tooltip=トークン内に挿入するクレームの名前を設定します。 「address.street」 のように完全修飾名で設定します。この場合、ネストされた JSON オブジェクトが作成されます。 +jsonType.label=クレーム JSON タイプ +jsonType.tooltip=トークンへの JSON クレーム の追加で使用される JSON タイプを設定します。 long、int、boolean、String が有効な値です。 +includeInIdToken.label=ID トークンに追加 +includeInIdToken.tooltip=クレームを ID トークンに追加すべきかどうかを設定します。 +includeInAccessToken.label=アクセストークンに追加 +includeInAccessToken.tooltip=クレームをアクセストークンに追加すべきかどうかを設定します。 +includeInUserInfo.label=UserInfo に追加 +includeInUserInfo.tooltip=クレームを UserInfo に追加すべきかどうかを設定します。 +usermodel.clientRoleMapping.clientId.label=クライアント ID +usermodel.clientRoleMapping.clientId.tooltip=ロールマッピング用のクライアント ID +usermodel.clientRoleMapping.rolePrefix.label=クライアントロールのプレフィックス +usermodel.clientRoleMapping.rolePrefix.tooltip=各クライアントロールのプレフィックスを設定します (オプション)。 +usermodel.realmRoleMapping.rolePrefix.label=レルムロールのプレフィックス +usermodel.realmRoleMapping.rolePrefix.tooltip=各レルムロールのプレフィックスを設定します (オプション)。 + +# client details +clients.tooltip=クライアントとはレルム内の信頼されたブラウザーアプリケーションや Web サービスです。これらのクライアントはログインを要求することができます。また、クライアント固有のロールを定義することができます。 +search.placeholder=検索... +create=作成 +import=インポート +client-id=クライアント ID +base-url=ベース URL +actions=アクション +not-defined=未定義 +edit=編集 +delete=削除 +no-results=結果がありません +no-clients-available=使用可能なクライアントはありません +add-client=クライアントの追加 +select-file=ファイルを選択 +view-details=詳細を参照 +clear-import=インポートをクリア +client-id.tooltip=URI とトークンで参照される ID を指定します。例えば 「my-client」 です。 SAML においては 期待される AuthnRequest の Issuer の値になります。 +client.name.tooltip=クライアントの表示名を指定します。例えば、 「My Client」 です。ローカライズ用のキーもサポートしています。例\: ${my_client} +client.enabled.tooltip=無効なクライアントはログインを開始したり、アクセストークンを取得したりすることはできません。 +consent-required=同意が必要 +consent-required.tooltip=有効の場合は、ユーザーはクライアントアクセスに同意する必要があります。 +client-protocol=クライアントプロトコル +client-protocol.tooltip=「OpenID Connect」 は、認可サーバーによって実行される認証を元にして、エンドユーザーのアイデンティティを確認するクライアントです。 「SAML」 は、クロスドメインのシングルサインオン (SSO) を含むWeb ベースの認証と認可シナリオを可能にし、情報を渡すためにアサーションを含むセキュリティトークンを使用します。 +access-type=アクセスタイプ +access-type.tooltip=「Confidential」 クライアントはログインプロトコルの開始するためにシークレットを必要とします。 「Public」 クライアントはシークレットを必要としません。 「Bearer-only」 クライアントはログインを開始することはない Web サービスです。 +standard-flow-enabled=Standard Flow の有効 +standard-flow-enabled.tooltip=OpenID Connect の標準的な、認可コードによるリダイレクトベースの認証を有効にします。 OpenID Connect または OAuth2 の仕様における 「Authorization Code Flow」 のサポートを有効にします。 +implicit-flow-enabled=Implicit Flow の有効 +implicit-flow-enabled.tooltip=OpenID Connect の認可コードなしのリダイレクトベース認証のサポートを有効にします。OpenID Connect または OAuth2 の仕様における 「Implicit Flow」 のサポートを有効にします。 +direct-access-grants-enabled=ダイレクトアクセスグラントの有効 +direct-access-grants-enabled.tooltip=ダイレクトアクセスグラントのサポートを有効にします。これは、アクセストークンの取得のために Keycloak サーバーとユーザーのユーザー名/パスワードで直接アクセスを行います。OAuth2 の仕様における 「リソースオーナーパスワードクレデンシャルグラント」 のサポートを有効にします。 +service-accounts-enabled=サービスアカウントの有効 +service-accounts-enabled.tooltip=Keycloak にこのクライアントを認証し、このクライアント専用のアクセストークンの取得ができるようになります。OAuth2 の仕様における 「クライアントクレデンシャルグラント」 のサポートを有効にします。 +include-authnstatement=AuthnStatement を含める +include-authnstatement.tooltip=認証方式とタイムスタンプを含めたステートメントをログインレスポンスに含めるべきか設定します。 +sign-documents=ドキュメントを署名する +sign-documents.tooltip=SAML ドキュメントをレルムで署名すべきか設定します。 +sign-assertions=アサーションを署名する +sign-assertions.tooltip=SAML ドキュメント内のアサーションを署名すべきか設定します。もしドキュメントが既に署名済みの場合は、この設定は不要です。 +signature-algorithm=署名アルゴリズム +signature-algorithm.tooltip=ドキュメントの署名に使用する署名アルゴリズムです。 +canonicalization-method=正規化方式 +canonicalization-method.tooltip=XML署名の正規化方式 (Canonicalization Method) を設定します。 +encrypt-assertions=アサーションを暗号化する +encrypt-assertions.tooltip=SAML アサーションをクライアントの公開鍵で AES を使い暗号化すべきか設定します。 +client-signature-required=クライアント署名が必須 +client-signature-required.tooltip=クライアントは SAML リクエストとレスポンスを署名しますか? それらを検証すべきか設定します。 +force-post-binding=POST Binding を強制 +force-post-binding.tooltip=レスポンスに常に POST Binding を使用します。 +front-channel-logout=フロントチャンネルログアウト +front-channel-logout.tooltip=有効の場合は、ログアウトはクライアントへのブラウザーリダイレクトが必要になります。無効の場合は、サーバーはログアウトのバックグラウンド呼び出しを行います。 +force-name-id-format=Name ID フォーマットを強制 +force-name-id-format.tooltip=要求された NameID サブジェクトフォーマットを無視し、管理コンソールで設定された物を使用します。 +name-id-format=Name ID フォーマット +name-id-format.tooltip=サブジェクトに使用する Name ID フォーマットを設定します。 +root-url=ルート URL +root-url.tooltip=相対 URL に追加するルート URL を設定します。 +valid-redirect-uris=有効なリダイレクト URI +valid-redirect-uris.tooltip=ログインまたはログインの成功後にブラウザーがリダイレクト可能とする、有効な URI パターンを設定します。単純なワイルドカード、つまり 「http://example.com/*」 が使用可能です。相対パス、つまり 「/my/relative/path/*」 も指定可能です。相対パスはクライアントのルート URL を基準とします。または、未指定の場合は認証サーバーのルート URL が使用されます。SAML では、ログインリクエストに埋め込まれたコンシューマサービスの URL に依存している場合は、有効な URI パターンを設定する必要があります。 +base-url.tooltip=認証サーバーがクライアントへのリダイレクトまたは戻るリンクを必要とする際に使用するデフォルト URL を設定します。 +admin-url=管理 URL +admin-url.tooltip=クライアントの管理インタフェースの URL を設定します。クライアントがアダプターの REST API をサポートしている場合に設定してください。この REST API により、認証サーバーは取り消しポリシーや他の管理タスクをプッシュすることができます。通常、クライアントのベース URL を設定します。 +master-saml-processing-url=SAML を処理するマスター URL +master-saml-processing-url.tooltip=設定された場合は、この URL が SP のアサーションコンシューマおよびシングルログアウトサービスの両方の Binding に使われます。これは、SAML エンドポイントの詳細設定にある各 Binding やサービスの設定にて個別に上書きすることができます。 +idp-sso-url-ref=IDP Initiated SSO の URL Name +idp-sso-url-ref.tooltip=IDP Initiated SSO を行う際にクライアントを参照するための URL フラグメント名を設定します。空にすると IDP Initiated SSO は無効になります。ブラウザーから参照する URL は 「{server-root}/realms/{realm}/protocol/saml/clients/{client-url-name}」 になります。 +idp-sso-relay-state=IDP Initiated SSO の RelayState +idp-sso-relay-state.tooltip=IDP Initiated SSO を行う際の SAML リクエストで送信したい RelayState を設定します。 +web-origins=Web オリジン +web-origins.tooltip=許可される CORS オリジンを設定します。有効なリダイレクト URI のすべてのオリジンを許可するには 「+」 を追加してください。すべてのオリジンを許可するには 「*」 を追加してください。 +fine-saml-endpoint-conf=SAML エンドポイントの詳細設定 +fine-saml-endpoint-conf.tooltip=アサーションコンシューマおよびシングルログアウトサービスの正確な URL を設定するにはこのセクションを開きます。 +assertion-consumer-post-binding-url=アサーションコンシューマサービスの POST Binding URL +assertion-consumer-post-binding-url.tooltip=アサーションコンシューマサービス (ログインレスポンス) の SAML POST Binding URL を設定します。この Binding のための URL がない場合は空でよいです。 +assertion-consumer-redirect-binding-url=アサーションコンシューマサービスの Redirect Binding URL +assertion-consumer-redirect-binding-url.tooltip=アサーションコンシューマサービス (ログインレスポンス) の SAML Redirect Binding URL を設定します。この Binding のための URL がない場合は空でよいです。 +logout-service-post-binding-url=ログアウトサービスの POST Binding URL +logout-service-post-binding-url.tooltip=シングルログアウトサービスの SAML POST Binding URLを設定します。異なる Binding を使用している場合は空でよいです。 +logout-service-redir-binding-url=ログアウトサービスの Redirect Binding URL +logout-service-redir-binding-url.tooltip=シングルログアウトサービスの SAML Redirect Binding URLを設定します。異なる Binding を使用している場合は空でよいです。 + +# client import +import-client=クライアントのインポート +format-option=フォーマットオプション +select-format=フォーマットを選択 +import-file=ファイルをインポート + +# client tabs +settings=設定 +credentials=クレデンシャル +saml-keys=SAML 鍵 +roles=ロール +mappers=マッパー +mappers.tooltip=プロトコルマッパーはトークンやドキュメントの変換を行います。ユーザーデータをプロトコルのクレームにマッピングしたり、クライアントと認証サーバー間の任意のリクエストを単に変換したりすることができます。 +scope=スコープ +scope.tooltip=スコープマッピングはクライアントに要求されたアクセストークン内に含まれるユーザーのロールマッピングを制限することができます。 +sessions.tooltip=このクライアントの有効なセッションを参照します。どのユーザーがアクティブでいつログインしたかを見ることができます。 +offline-access=オフラインアクセス +offline-access.tooltip=このクライアントのオフラインセッションを参照します。オフライントークンをどのユーザーがいつ取得したかを見ることができます。このクライアントのすべてのトークンを取り消すには、取り消しタブを開き、 「この日時より前」 に現在日時を設定してください。 +clustering=クラスタリング +installation=インストール +installation.tooltip=様々なクライアントアダプターの設定フォーマットを生成するヘルパーユーティリティです。生成したものをダウンロードまたはカットアンドペーストしてクライアントに設定することができます。 +service-account-roles=サービスアカウントロール +service-account-roles.tooltip=このクライアント専用のサービスアカウントのロールマッピングを認証できるようにします。 + +# client credentials +client-authenticator=クライアント認証 +client-authenticator.tooltip=Keycloak サーバーに対してこのクライアントの認証に使用するクライアント認証方式を設定します。 +certificate.tooltip=クライアントで発行され、キーストアの秘密鍵で署名された JWT を検証するためのクライアント証明書です。 +publicKey.tooltip=クライアントで発行され、秘密鍵で署名された JWT を検証するための公開鍵です。 +no-client-certificate-configured=クライアント証明書が設定されていません +gen-new-keys-and-cert=新しい鍵と証明書を生成 +import-certificate=証明書をインポート +gen-client-private-key=クライアントの秘密鍵の生成 +generate-private-key=秘密鍵の生成 +archive-format=アーカイブ形式 +archive-format.tooltip=Java キーストアまたは PKCS12 アーカイブ形式 +key-alias=キーエイリアス +key-alias.tooltip=秘密鍵と証明書のアーカイブエイリアスです。 +key-password=鍵のパスワード +key-password.tooltip=アーカイブ内の秘密鍵にアクセスするためのパスワード +store-password=ストアのパスワード +store-password.tooltip=アーカイブ自身にアクセスするためのパスワード +generate-and-download=生成 & ダウンロード +client-certificate-import=クライアント証明書のインポート +import-client-certificate=クライアント証明書のインポート +jwt-import.key-alias.tooltip=証明書のアーカイブエイリアスです。 +secret=シークレット +regenerate-secret=シークレットの再生成 +registrationAccessToken=登録用アクセストークン +registrationAccessToken.regenerate=登録用アクセストークンの再生成 +registrationAccessToken.tooltip=登録用アクセストークンにより、クライアントはクライアント登録サービスにアクセスできます。 +add-role=ロールの追加 +role-name=ロール名 +composite=複合 +description=説明 +no-client-roles-available=使用可能なクライアントロールはありません。 +scope-param-required=スコープパラメータが必要 +scope-param-required.tooltip=認可/トークンリクエストの間にロール名のスコープパラメータが使用された場合のみ与えられます。 +composite-roles=複合ロール +composite-roles.tooltip=このロールがユーザーにアサイン(アサイン解除)された際に、関連するロールが暗黙的にアサイン(アサイン解除)されます。 +realm-roles=レルムロール +available-roles=使用可能なロール +add-selected=選択したものを追加 +associated-roles=関連ロール +composite.associated-realm-roles.tooltip=この複合ロールに関連付けされているレルムレベルのロールです。 +composite.available-realm-roles.tooltip=この複合ロールに関連付け可能なレルムレベルのロールです。 +remove-selected=選択されたものを削除 +client-roles=クライアントロール +select-client-to-view-roles=ロールを参照するにはクライアントを選択してください +available-roles.tooltip=この複合ロール関連付け可能なこのクライアントのロールです。 +client.associated-roles.tooltip=この複合ロールに関連付けされているクライアントロールです。 +add-builtin=ビルトインを追加 +category=カテゴリ +type=タイプ +no-mappers-available=使用可能なマッパーはありません +add-builtin-protocol-mappers=ビルトインプロトコルマッパーを追加 +add-builtin-protocol-mapper=ビルトインプロトコルマッパーを追加 +scope-mappings=スコープマッピング +full-scope-allowed=フルスコープを許可 +full-scope-allowed.tooltip=全ての制限の無効を許可します。 +scope.available-roles.tooltip=スコープにアサイン可能なレルムレベルのロールです。 +assigned-roles=アサイン済みロール +assigned-roles.tooltip=スコープにアサイン済みのレルムレベルのロールです。 +effective-roles=有効なロール +realm.effective-roles.tooltip=複合ロールの継承も含めたアサイン済みのレルムレベルのロールです。 +select-client-roles.tooltip=ロールを参照するにはクライアントを選択してください +assign.available-roles.tooltip=アサイン可能なクライアントロールです。 +client.assigned-roles.tooltip=アサイン済みクライアントロールです。 +client.effective-roles.tooltip=複合ロールより引き継いでいるロールも含めたアサイン済みのクライアントロールです。 +basic-configuration=基本設定 +node-reregistration-timeout=ノード再登録のタイムアウト +node-reregistration-timeout.tooltip=登録されたクライアントをクラスターノードへ再登録する際の最大時間間隔を設定します。クラスターノードがこの時間内に Keycloak に再登録リクエストを送信しない場合は、Keycloak から登録解除されます。 +registered-cluster-nodes=登録済みクラスターノード +register-node-manually=ノードを手動で登録 +test-cluster-availability=クラスターの可用性をテスト +last-registration=最終登録 +node-host=ノードホスト +no-registered-cluster-nodes=使用可能な登録済みクラスターノードがありません +cluster-nodes=クラスターノード +add-node=ノードを追加 +active-sessions.tooltip=このクライントの有効なユーザーセッションの合計数です。 +show-sessions=セッションを表示 +show-sessions.tooltip=有効なセッション数に応じて高負荷なオペレーションになる恐れがありますので注意してください。 +user=ユーザー +from-ip=From IP +session-start=セッション開始 +first-page=最初のページ +previous-page=前のページ +next-page=次のページ +client-revoke.not-before.tooltip=この日時より前に発行されたこのクライアント用のトークンを取り消します。 +client-revoke.push.tooltip=管理 URL がこのクライアントに設定されている場合は、クライアントにポリシーをプッシュします。 +select-a-format=フォーマットを選択 +download=ダウンロード +offline-tokens=オフライントークン +offline-tokens.tooltip=このクライアントのオフライントークンの合計数です。 +show-offline-tokens=オフライントークンを表示 +show-offline-tokens.tooltip=オフライントークン数に応じて高負荷なオペレーションになる恐れがありますので注意してください。 +token-issued=発行済みトークン +last-access=最終アクセス +last-refresh=最終リフレッシュ +key-export=鍵をエクスポート +key-import=鍵をインポート +export-saml-key=SAML 鍵をエクスポート +import-saml-key=SAML 鍵をインポート +realm-certificate-alias=レルム証明書エイリアス +realm-certificate-alias.tooltip=レルム証明書もアーカイブに格納されます。これはそのエイリアスとなります。 +signing-key=署名鍵 +saml-signing-key=SAML 署名鍵です。 +private-key=秘密鍵 +generate-new-keys=新しい鍵を生成 +export=エクスポート +encryption-key=暗号化鍵 +saml-encryption-key.tooltip=SAML 暗号化鍵です。 +service-accounts=サービスアカウント +service-account.available-roles.tooltip=サービスアカウントにアサイン可能なレルムレベルのロールです。 +service-account.assigned-roles.tooltip=サービスアカウントにアサイン済みのレルムレベルのロールです。 +service-account-is-not-enabled-for={{client}} のサービスアカウントは有効ではありません +create-protocol-mappers=プロトコルマッパーを作成 +create-protocol-mapper=プロトコルマッパーを作成 +protocol=プロトコル +protocol.tooltip=プロトコルです。 +id=ID +mapper.name.tooltip=マッパーの名前です。 +mapper.consent-required.tooltip=一時的なアクセスを許可する際に、クライアントへの提供データにユーザーの同意を必要とするか設定します。 +consent-text=同意のテキスト +consent-text.tooltip=同意ページに表示するテキストです。 +mapper-type=マッパータイプ +mapper-type.tooltip=マッパーのタイプです。 +# realm identity providers +identity-providers=アイデンティティ プロバイダー +table-of-identity-providers=アイデンティティ プロバイダーの一覧表 +add-provider.placeholder=プロバイダーを追加... +provider=プロバイダー +gui-order=GUI 順序 +first-broker-login-flow=初回ログインフロー +post-broker-login-flow=ログイン後のフロー +redirect-uri=リダイレクト URI +redirect-uri.tooltip=アイデンティティ プロバイダーの設定で使用するリダイレクト URIです。 +alias=エイリアス +identity-provider.alias.tooltip=エイリアスは一意にアイデンティティ プロバイダーを識別するもので、リダイレクト URI の構築にも使用されます。 +identity-provider.enabled.tooltip=このアイデンティティ プロバイダーの有効/無効を設定します。 +authenticate-by-default=デフォルトで認証 +identity-provider.authenticate-by-default.tooltip=ログイン画面の表示前に、このプロバイダーでデフォルトで認証試行すべきかどうかを示しています。 +store-tokens=トークンの格納 +identity-provider.store-tokens.tooltip=ユーザー認証後のトークン格納の有効/無効を設定します。 +stored-tokens-readable=読み取り可能なトークンを格納 +identity-provider.stored-tokens-readable.tooltip=新しいユーザーが格納されたトークンを読み取り可能かどうかの有効/無効設定です。broker.read-token ロールをアサインします。 +update-profile-on-first-login=初回ログイン時にプロフィールを更新 +on=オン +on-missing-info=情報不足の場合 +off=オフ +update-profile-on-first-login.tooltip=初回ログイン時にどのユーザーがプロフィールの更新を必要とするか条件を定義します。 +trust-email=Eメールを信頼 +trust-email.tooltip=有効とした場合は、このレルムでEメールの確認が有効となっている場合でも、このプロバイダーが提供するEメールは確認されなくなります。 +gui-order.tooltip=GUI (例. ログインページ上) でのプロバイダーの表示順序を決める番号を設定します。 +first-broker-login-flow.tooltip=このアイデンティティ プロバイダーでの初回ログイン後に起動させる認証フローのエイリアスです。 「初回ログイン」 という用語は、認証したアイデンティティ プロバイダーアカウントに関連付けられた Keycloak アカウントがまだ存在しない状態であることを意味します。 +post-broker-login-flow.tooltip=このアイデンティティ プロバイダーでの各ログイン後に起動させる認証フローのエイリアスです。このアイデンティティ プロバイダーで認証された各ユーザーの追加の確認 (例えば OTP) を行いたい場合に便利です。このアイデンティティ プロバイダーによるログイン後に追加の Authenticator の起動を行いたくない場合は、空のままとしてください。また、Authenticator の実装では、ClientSession にはアイデンティティ プロバイダーによりユーザーが既に設定されていることに注意してください。 +openid-connect-config=OpenID Connect の設定 +openid-connect-config.tooltip=OIDC SP と 外部 IDP の設定です。 +authorization-url=認可 URL +authorization-url.tooltip=認可 URL を設定します。 +token-url=トークン URL +token-url.tooltip=トークン URL を設定します。 +logout-url=ログアウト URL +identity-provider.logout-url.tooltip=外部 IDP からユーザーのログアウトに使用するセッション終了エンドポイントを設定します。 +backchannel-logout=バックチャンネルログアウト +backchannel-logout.tooltip=外部 IDP がバックチャンネルログアウトをサポートするどうかを設定します。 +user-info-url=UserInfo URL +user-info-url.tooltip=UserInfo の URL を設定します。これはオプションです。 +identity-provider.client-id.tooltip=アイデンティティ プロバイダーで登録されているクライアントまたはクライアント識別子を設定します。 +client-secret=クライアントシークレット +show-secret=シークレットを表示する +hide-secret=シークレットを隠す +client-secret.tooltip=アイデンティティ プロバイダーで登録されているクライアントまたはクライアントシークレットを設定します。 +issuer=発行者 (Issuer) +issuer.tooltip=レスポンス内の発行者の識別子 (Issuer Identifier) を設定します。未設定の場合は、検証は実行されません。 +default-scopes=デフォルトスコープ +identity-provider.default-scopes.tooltip=認可要求の際に送信されるスコープです。スペース区切りでスコープのリストを設定します。デフォルトは 「openid」 です。 +prompt=プロンプト (prompt) +unspecified.option=未定義 +none.option=none +consent.option=consent +login.option=login +select-account.option=select_account +prompt.tooltip=認証サーバーは再認証や同意をエンドユーザーに促すかどうかを指定します。 +validate-signatures=署名検証 +identity-provider.validate-signatures.tooltip=外部 IDP の署名検証の有効/無効を設定します。 +validating-public-key=検証用の公開鍵 +identity-provider.validating-public-key.tooltip=外部 IDP の署名検証に使用する PEM 形式の公開鍵を設定します。 +import-external-idp-config=外部 IDP 設定のインポート +import-external-idp-config.tooltip=外部 IDP メタデータを設定ファイルよりロード、または URL よりダウンロードして設定します。 +import-from-url=URL よりインポート +identity-provider.import-from-url.tooltip=リモート IDP ディスカバリディスクリプタよりメタデータをインポートします。 +import-from-file=ファイルよりインポート +identity-provider.import-from-file.tooltip=ダウンロードした IDP ディスカバリディスクリプタよりメタデータをインポートします。 +saml-config=SAML 設定 +identity-provider.saml-config.tooltip=SAML SP と 外部 IDP の設定です。 +single-signon-service-url=シングルサインオンサービスの URL +saml.single-signon-service-url.tooltip=認証リクエスト (SAML AuthnRequest) の送信に使用する URL を設定します。 +single-logout-service-url=シングルログアウトサービスの URL +saml.single-logout-service-url.tooltip=ログアウトリクエストの送信に使用する URL を設定します。 +nameid-policy-format=Name ID ポリシーフォーマット +nameid-policy-format.tooltip=Name ID フォーマットに対応する URI リファレンスを指定します。デフォルトは urn:oasis:names:tc:SAML:2.0:nameid-format:persistent になります。 +http-post-binding-response=HTTP-POST Binding レスポンス +http-post-binding-response.tooltip=HTTP-POST Binding を使用してリクエストに応答するかどうかを設定します。オフの場合は、HTTP-REDIRECT Binding が使用されます。 +http-post-binding-for-authn-request=AuthnRequest の HTTP-POST Binding +http-post-binding-for-authn-request.tooltip=HTTP-POST Binding を使用して AuthnRequest を送信するかどうかを設定します。オフの場合は、HTTP-REDIRECT Binding が使用されます。 +want-authn-requests-signed=AuthnRequest の署名が必要 +want-authn-requests-signed.tooltip=アイデンティティ プロバイダーが署名された AuthnRequest を必要とするかどうかを設定します。 +force-authentication=認証を強制 +identity-provider.force-authentication.tooltip=アイデンティティ プロバイダーが以前のセキュリティコンテキストに頼るのではなく、プレゼンターを直接認証すべきかどうかを設定します。 +validate-signature=署名検証 +saml.validate-signature.tooltip=SAML レスポンスの署名検証の有効/無効を設定します。 +validating-x509-certificate=検証用の X509 証明書 +validating-x509-certificate.tooltip=署名の確認に使用する PEM 形式の証明書を設定します。 +saml.import-from-url.tooltip=リモート IDP の SAML エンティティディスクリプタからメタデータをインポートします。 +social.client-id.tooltip=アイデンティティ プロバイダーで登録されているクライアント識別子を設定します。 +social.client-secret.tooltip=アイデンティティ プロバイダーで登録されているクライアントシークレットを設定します。 +social.default-scopes.tooltip=認可要求の際に送信されるスコープを設定します。設定可能な値、区切り文字、デフォルト値はドキュメントを参照してください。 +key=Key +stackoverflow.key.tooltip=Stack Overflow のクライアント登録で取得した Key を設定します。 + +# User federation +sync-ldap-roles-to-keycloak=LDAP ロールを Keycloak に同期 +sync-keycloak-roles-to-ldap=Keycloak ロールを LDAP に同期 +sync-ldap-groups-to-keycloak=LDAP グループを Keycloak に同期 +sync-keycloak-groups-to-ldap=Keycloak グループを LDAP に同期 + +realms=レルム +realm=レルム + +identity-provider-mappers=アイデンティティ プロバイダー マッパー +create-identity-provider-mapper=アイデンティティ プロバイダー マッパーを作成 +add-identity-provider-mapper=アイデンティティ プロバイダー マッパーを追加 +client.description.tooltip=クライアントの説明を指定します。例えば 「タイムシート用のクライアント」 です。ローカライズ用のキーもサポートしています。例\: ${my_client_description} + +expires=有効期限 +expiration=有効期限 +expiration.tooltip=トークンの有効期間を指定します。 +count=カウント +count.tooltip=このトークンを利用してクライアントをいくつ作成可能か指定します。 +remainingCount=残りのカウント +created=作成日時 +back=戻る +initial-access-tokens=初期アクセストークン +initial-access-tokens.tooltip=動的クライアント登録用の初期アクセストークンです。これらのトークン付きのリクエストは任意のホストから送信することができます。 +add-initial-access-tokens=初期アクセストークンを追加 +initial-access-token=初期アクセストークン +initial-access.copyPaste.tooltip=後からは取得することはできませんので、このページから離れる前に初期アクセストークンをコピー/ペーストしてください。 +continue=続ける +initial-access-token.confirm.title=初期アクセストークンのコピー +initial-access-token.confirm.text=後からは取得することはできませんので、初期アクセストークンのコピー & ペーストを行ってください +no-initial-access-available=使用可能な初期アクセストークンはありません + +trusted-hosts-legend=クライアント登録用の信頼されたホスト +trusted-hosts-legend.tooltip=クライアント登録用に信頼されたホストです。これらのホストからのクライアント登録のリクエストは、初期アクセストークンなしで送信することができます。特定ホストからのクライアント登録数は指定された数に制限することができます。 +no-client-trusted-hosts-available=使用可能な信頼されたホストはありません +add-client-reg-trusted-host=信頼されたホストの追加 +hostname=ホスト名 +client-reg-hostname.tooltip=完全修飾のホスト名またはIPアドレスです。このホスト/アドレスからのクライアント登録のリクエストは信頼され、新しいクライアント登録が許可されます。 +client-reg-count.tooltip=特定ホストからのクライアント登録リクエストを許可する数です。このリミットに達した場合はリスタートする必要があります。 +client-reg-remainingCount.tooltip=このホストからのクライアント登録リクエストの残りのカウントです。このリミットに達した場合はリスタートする必要があります。 +reset-remaining-count=残りのカウントをリセット + + +client-templates=クライアントテンプレート +client-templates.tooltip=クライアントテンプレートでは、複数のクライアントで共有する共通設定を定義することができます。 + +groups=グループ + +group.add-selected.tooltip=グループにアサイン可能なレルムロールです。 +group.assigned-roles.tooltip=グループにマッピングされたレルムロールです。 +group.effective-roles.tooltip=マッピングされているすべてのレルムロールです。複合ロールより引き継いでいるロールも含みます。 +group.available-roles.tooltip=このクライアントよりアサイン可能なロールです。 +group.assigned-roles-client.tooltip=マッピングされたこのクライアントのロールです。 +group.effective-roles-client.tooltip=マッピングされたこのクライアントのロールです。複合ロールより引き継いでいるロールも含みます。 + +default-roles=デフォルトロール +no-realm-roles-available=使用可能なレルムロールはありません + +users=ユーザー +user.add-selected.tooltip=ユーザーにアサイン可能なレルムロールです。 +user.assigned-roles.tooltip=ユーザーにマッピングされたレルムロールです。 +user.effective-roles.tooltip=マッピングされているすべてのレルムロールです。複合ロールより引き継いでいるロールも含みます。 +user.available-roles.tooltip=このクライアントよりアサイン可能なロールです。 +user.assigned-roles-client.tooltip=マッピングされたこのクライアントのロールです。 +user.effective-roles-client.tooltip=マッピングされたこのクライアントのロールです。複合ロールより引き継いでいるロールも含みます。 +default.available-roles.tooltip=アサイン可能なレルムレベルのロールです。 +realm-default-roles=レルムのデフォルトロール +realm-default-roles.tooltip=ユーザーにアサインされたレルムレベルのロールです。 +default.available-roles-client.tooltip=デフォルトでアサイン可能なこのクライアントのロールです。 +client-default-roles=クライアントのデフォルトロール +client-default-roles.tooltip=デフォルトロールとしてアサインされたこのクライアントのロールです。 +composite.available-roles.tooltip=この複合ロールに関連付け可能なレルムレベルのロールです。 +composite.associated-roles.tooltip=この複合ロールに関連付けされているレルムレベルのロールです。 +composite.available-roles-client.tooltip=この複合ロールに関連付け可能なこのクライアントのロールです。 +composite.associated-roles-client.tooltip=この複合ロールに関連付けされているクライアントロールです。 +partial-import=部分インポート +partial-import.tooltip=部分インポートでは、以前にエクスポートした JSON ファイルよりユーザー、クライアント、およびその他のリソースをインポートすることができます。 + +file=ファイル +exported-json-file=エクスポートされた JSON ファイル +import-from-realm=レルムからインポート +import-users=ユーザーをインポート +import-groups=グループをインポート +import-clients=クライアントをインポート +import-identity-providers=アイデンティティ プロバイダーをインポート +import-realm-roles=レルムロールをインポート +import-client-roles=クライアントロールをインポート +if-resource-exists=リソースが存在する場合 +fail=失敗 +skip=スキップ +overwrite=上書き +if-resource-exists.tooltip=既に存在するリソースをインポートしようとした場合にどうすべきかを指定します。 + +action=アクション +role-selector=ロールの選択 +realm-roles.tooltip=選択可能なレルムロールです。 + +select-a-role=ロールを選択してください +select-realm-role=レルムロールを選択 +client-roles.tooltip=選択可能なクライアントロールです。 +select-client-role=クライアントロールを選択 + +client-template=クライアントテンプレート +client-template.tooltip=設定を引き継ぐクライアントテンプレートを選択します。 +client-saml-endpoint=クライアント SAML エンドポイント +add-client-template=クライアントテンプレートを追加 + +manage=管理 +authentication=認証 +user-federation=ユーザーフェデレーション +user-storage=ユーザーストレージ +events=イベント +realm-settings=レルムの設定 +configure=設定 +select-realm=レルムの選択 +add=追加 + +client-template.name.tooltip=クライアントテンプレートの名前です。レルム内でユニークにする必要があります。 +client-template.description.tooltip=クライアントテンプレートの説明です。 +client-template.protocol.tooltip=このクライアントテンプレートにより、どの SSO プロトコルが提供されるか設定します。 + +add-user-federation-provider=ユーザー フェデレーション プロバイダーの追加 +add-user-storage-provider=ユーザー ストレージ プロバイダーの追加 +required-settings=必要な設定 +provider-id=プロバイダー ID +console-display-name=コンソール表示名 +console-display-name.tooltip=管理コンソール内でのリンク表示名を設定します。 +priority=優先度 +priority.tooltip=ユーザーを検索する際のプロバイダーの優先度を設定します。低い順となります。 +sync-settings=同期の設定 +periodic-full-sync=定期的なフル同期 +periodic-full-sync.tooltip=プロバイダーユーザーの Keycloak への定期的なフル同期を有効または無効とすべきかを設定します。 +full-sync-period=フル同期の周期 +full-sync-period.tooltip=フル同期の周期を秒で設定します。 +periodic-changed-users-sync=定期的な変更ユーザーの同期 +periodic-changed-users-sync.tooltip=変更または新規作成されたプロバイダーユーザーの Keycloak への定期的な同期を有効または無効とすべきか設定します。 +changed-users-sync-period=変更ユーザーの同期周期 +changed-users-sync-period.tooltip=変更または新規作成されたプロバイダーユーザーの同期周期を秒で設定します。 +synchronize-changed-users=変更ユーザーを同期 +synchronize-all-users=すべてのユーザーを同期 +kerberos-realm=Kerberos レルム +kerberos-realm.tooltip=Kerberos レルムの名前を設定します。例えば、 FOO.ORG です。 +server-principal=サーバープリンシパル +server-principal.tooltip=サーバー、ドメイン名を含む HTTP サービスのサービスプリンシパルのフルネームを設定します。例えば、 HTTP/host.foo.org@FOO.ORG です。 +keytab=KeyTab +keytab.tooltip=サーバープリンシパルのクレデンシャルを含む Kerberos の KeyTab ファイルを設定します。例えば、/etc/krb5.keytab です。 +debug=デバッグ +debug.tooltip=Krb5LoginModule の標準出力へのデバッグロギングの有効/無効を設定します。 +allow-password-authentication=パスワード認証を許可 +allow-password-authentication.tooltip=Kerberos データベースに対するユーザー名/パスワード認証の有効/無効を設定します。 +edit-mode=編集モード +edit-mode.tooltip=READ_ONLY は、パスワード更新は許可されずユーザーは常に Kerberos のパスワードで認証することを意味します。UNSYNCED は、ユーザーは Keycloak データベース内の自分のパスワードを変更可能であり、Kerberos のパスワードの代わりに使用されることを意味します。 +ldap.edit-mode.tooltip=READ_ONLY は、読み取り専用の LDAP ストアです。WRITABLE は、データがオンデマンドで LDAP に書き戻しされることを意味します。UNSYNCEDは、ユーザーデータはインポートされますが、LDAP に書き戻しはされないことを意味します。 +update-profile-first-login=初回ログイン時にプロフィールを更新 +update-profile-first-login.tooltip=初回ログイン時のプロフィール更新の有効/無効を設定します。 +sync-registrations=登録の同期 +ldap.sync-registrations.tooltip=LDAP ストア内に新規作成ユーザーを作成すべきかどうかを設定します。どのプロバイダーが新しいユーザーの同期先に選択されるかは、優先度が影響します。 +vendor=ベンダー +ldap.vendor.tooltip=LDAP ベンダー (プロバイダー) +username-ldap-attribute=ユーザー名の LDAP 属性 +ldap-attribute-name-for-username=ユーザー名の LDAP 属性名 +username-ldap-attribute.tooltip=Keycloak ユーザー名にマッピングされる LDAP 属性名を設定します。多くの LDAP サーバーベンダーでは 「uid」 となります。Active Directory では 「sAMAccountName」 または 「cn」 となります。LDAP から Keycloak にインポートするすべての LDAP ユーザーのレコードで、属性は入力されているはずです。 +rdn-ldap-attribute=RDN LDAP 属性 +ldap-attribute-name-for-user-rdn=ユーザー RDN の LDAP 属性名 +rdn-ldap-attribute.tooltip=一般的なユーザー DN の RDN (top 属性) として使用される LDAP 属性名を設定します。通常は、ユーザー名の LDAP 属性 と同じですが、必須ではありません。例えば Active Directory では、ユーザー名が 「sAMAccountName」 だと RDN 属性として 「cn」 を使用するのが一般的です。 +uuid-ldap-attribute=UUID LDAP 属性 +ldap-attribute-name-for-uuid=UUID の LDAP 属性名 +uuid-ldap-attribute.tooltip=LDAP 内でオブジェクトのユニークなオブジェクト識別子 (UUID) として使用される LDAP 属性名を設定します。多くの LDAP サーバーベンダーでは 「entryUUID」 となりますが、異なる場合もあります。例えば Active Directory では、 「objectGUID」 となります。お使いの LDAP サーバーが UUID をサポートしていない場合は、ツリー内の LDAP ユーザーの中でユニークとなる他の属性を使用することができます。例えば、 「uid」 や 「entryDN」 です。 +user-object-classes=ユーザーオブジェクトクラス +ldap-user-object-classes.placeholder=LDAP のユーザーオブジェクトクラス (カンマ区切り) + +ldap-connection-url=LDAP 接続 URL +ldap-users-dn=LDAP ユーザー DN +ldap-bind-dn=LDAP Bind DN +ldap-bind-credentials=LDAP Bind のクレデンシャル +ldap-filter=LDAP フィルター +ldap.user-object-classes.tooltip=LDAP ユーザー用の すべての LDAP オブジェクトクラスをカンマ区切りで設定します。例: 「inetOrgPerson, organizationalPerson」 。新規作成された Keycloak ユーザーは、これらすべてのオブジェクトクラスを使用して LDAP に書き込まれます。また、既存の LDAP ユーザーのレコードは、これらすべてのオブジェクトクラスを含む場合だけ発見されます。 + +connection-url=接続 URL +ldap.connection-url.tooltip=お使いの LDAP サーバーへの接続 URL +test-connection=接続テスト +users-dn=ユーザー DN +ldap.users-dn.tooltip=ユーザーがいる LDAP ツリーの完全 DN を設定します。この DN は LDAP ユーザーの親になります。例えば、典型的なユーザーは 「uid=john,ou=users,dc=example,dc=com」 「ou=users,dc=example,dc=com」 のような DN となりますが、この場合は 「ou=users,dc=example,dc=com」 となります。 +authentication-type=認証タイプ +ldap.authentication-type.tooltip=LDAP 認証タイプを設定します。今のところ、 「none」 (Anonymous LDAP 認証) または 「simple」 (クレデンシャルによる Bind + パスワード認証による Bind) メカニズムが用意されています。 +bind-dn=Bind DN +ldap.bind-dn.tooltip=Keycloak が LDAP サーバーにアクセスするために使用する LDAP 管理者の DN を設定します。 +bind-credential=Bind のクレデンシャル +ldap.bind-credential.tooltip=LDAP 管理者のパスワードを設定します。 +test-authentication=認証テスト +custom-user-ldap-filter=カスタムユーザー LDAP フィルター +ldap.custom-user-ldap-filter.tooltip=ユーザー検索のフィルタリングを行う LDAP フィルターを設定します。追加のフィルターが必要ない場合は空のままにしてください。設定は、 「(」 から始まり 「)」 で終わることを確認してください。 +search-scope=検索スコープ +ldap.search-scope.tooltip=One Level では、ユーザー DN で指定された DN 内のユーザーのみを検索します。subtree では、サブツリー全体を検索します。より詳細については LDAP のドキュメントを参照してください。 +use-truststore-spi=トラストストア SPI を使用 +ldap.use-truststore-spi.tooltip=LDAP 接続で、keycloak-server.json で設定されたトラストストアの トラストストア SPI を使用するかどうかを指定します。 「Always」 は常に使用することを意味します。 「Never」 は使用しないことを意味します。 「Only for ldaps」 は、接続 URL が ldaps の場合に使用することを意味します。keycloak-server.json で設定されていない場合でも、デフォルトの Java CA 証明書 (cacerts) や 「javax.net.ssl.trustStore」 プロパティで指定された証明書が使用される点に注意してください。 +connection-pooling=接続プーリング +ldap.connection-pooling.tooltip=Keycloak は LDAP サーバーへのアクセスで接続プールを使用するかどうかを設定します。 +ldap.pagination.tooltip=LDAP サーバーはページネーションをサポートするかどうかを設定します。 +kerberos-integration=Kerberos と統合 +allow-kerberos-authentication=Kerberos 認証を許可 +ldap.allow-kerberos-authentication.tooltip=SPNEGO/Kerberos のトークンを持つユーザーの HTTP 認証を有効/無効にします。認証されたユーザーに関するデータはこの LDAP サーバーよりプロビジョニングされます。 +use-kerberos-for-password-authentication=パスワード認証に Kerberos を使用 +ldap.use-kerberos-for-password-authentication.tooltip=LDAP サーバーに対してディレクトリサービスの API で認証する代わりに、Kerberos に対して ユーザー名/パスワード認証する Kerberos ログインモジュールを使用します。 +batch-size=バッチサイズ +ldap.batch-size.tooltip=1トランザクションで LDAP から Keycloak にインポートされる LDAP ユーザー数を設定します。 +ldap.periodic-full-sync.tooltip=Keycloak への LDAP ユーザーの定期的なフル同期を有効/無効にします。 +ldap.periodic-changed-users-sync.tooltip=Keycloak への 変更または新規作成された LDAP ユーザーの定期的な同期を有効/無効にします。 +ldap.changed-users-sync-period.tooltip=変更または新規作成された LDAP ユーザーの同期周期を秒で設定します。 +user-federation-mappers=ユーザーフェデレーションのマッパー +create-user-federation-mapper=ユーザーフェデレーション マッパーの作成 +add-user-federation-mapper=ユーザーフェデレーション マッパーの追加 +provider-name=プロバイダー名 +no-user-federation-providers-configured=設定されているユーザーフェデレーション プロバイダーはありません +no-user-storage-providers-configured=設定されているユーザーストレージ プロバイダーはありません +add-identity-provider=アイデンティティ プロバイダーの登録 +add-identity-provider-link=アイデンティティ プロバイダーのリンク登録 +identity-provider=アイデンティティ プロバイダー +identity-provider-user-id=アイデンティティ プロバイダーのユーザー ID +identity-provider-user-id.tooltip=アイデンティティ プロバイダー側のユーザーのユニーク ID です。 +identity-provider-username=アイデンティティ プロバイダーのユーザー名 +identity-provider-username.tooltip=アイデンティティ プロバイダー側のユーザー名です。 +pagination=ページネーション + +browser-flow=ブラウザーフロー +browser-flow.tooltip=ブラウザー認証で使用したいフローを選択してください。 +registration-flow=登録フロー +registration-flow.tooltip=登録で使用したいフローを選択してください。 +direct-grant-flow=ダイレクトグラントフロー +direct-grant-flow.tooltip=ダイレクトグラント認証で使用したいフローを選択してください。 +reset-credentials=クレデンシャルのリセット +reset-credentials.tooltip=ユーザーがクレデンシャルを忘れた際に使用したいフローを選択してください。 +client-authentication=クライアント認証 +client-authentication.tooltip=クライアント認証で使用したいフローを選択してください。 +new=新規作成 +copy=コピー +add-execution=Execution を追加 +add-flow=フローを追加 +auth-type=認証タイプ +requirement=必要条件 +config=設定 +no-executions-available=使用可能な Execution がありません +authentication-flows=認証フロー +create-authenticator-config=認証設定の作成 +authenticator.alias.tooltip=この設定の名前を設定します。 +otp-type=OTP タイプ +time-based=タイムベース +counter-based=カウンターベース +otp-type.tooltip=TOTP はタイムベースのワンタイムパスワードです。 「hotp」 は、サーバーでハッシュに対してカウンターを保持するカウンターベースのワンタイムパスワードです。 +otp-hash-algorithm=OTP ハッシュアルゴリズム +otp-hash-algorithm.tooltip=OTP を生成するのにどのハッシュアルゴリズムを使用するか設定します。 +number-of-digits=桁数 +otp.number-of-digits.tooltip=OTP の桁数を設定します。 +look-ahead-window=先読みウィンドウ +otp.look-ahead-window.tooltip=トークンジェネレーターとサーバーが時刻同期またはカウンター同期していないことを考慮してどれくらい先読みを行うか設定します。 +initial-counter=初期カウンター +otp.initial-counter.tooltip=初期カウンターの値は何とするか設定します。 +otp-token-period=OTP トークンの期間 +otp-token-period.tooltip=OTP トークンは何秒有効とするか設定します。デフォルトは30秒です。 +table-of-password-policies=パスワードポリシーの一覧表 +add-policy.placeholder=ポリシーを追加... +policy-type=ポリシーのタイプ +policy-value=ポリシーの値 +admin-events=管理イベント +admin-events.tooltip=保存されたレルムの管理イベントを表示します。管理イベントは、アカウント管理に関連したイベント、例えばレルムの作成などです。イベントの保存を有効にするには設定へ移動してください。 +login-events=ログインイベント +filter=フィルター +update=更新 +reset=リセット +operation-types=操作タイプ +resource-types=リソースタイプ +select-operations.placeholder=操作を選択... +select-resource-types.placeholder=リソースタイプを選択... +resource-path=リソースパス +resource-path.tooltip=リソースパスでフィルターします。ワイルドカード 「*」 はパスの単一部分と一致し、 「**」 は複数部分と一致します。例えば、 「realms/*/clients/asbc」 はすべてのレルム内の ID asbc を持つクライアントとマッチし、 「realms/master/**」 は master レルム内のすべてにマッチします。 +date-(from)=日付 (From) +date-(to)=日付 (To) +authentication-details=認証の詳細 +ip-address=IP アドレス +time=日時 +operation-type=操作タイプ +resource-type=リソースタイプ +auth=認証 +representation=Representation +register=登録 +required-action=Required アクション +default-action=デフォルトアクション +auth.default-action.tooltip=有効の場合は、新規ユーザーにはこの Required アクションがアサインされます。 +no-required-actions-configured=設定された Required アクションはありません +defaults-to-id=ID がデフォルトになります +flows=フロー +bindings=バインディング +required-actions=Required アクション +password-policy=パスワードポリシー +otp-policy=OTP ポリシー +user-groups=ユーザーグループ +default-groups=デフォルトグループ +groups.default-groups.tooltip=新規ユーザーが自動的に参加するグループのセットを設定します。 +cut=カット +paste=ペースト + +create-group=グループの作成 +create-authenticator-execution=認証 Execution の作成 +create-form-action-execution=フォームアクション Execution の作成 +create-top-level-form=トップレベルフォームの作成 +flow.alias.tooltip=フローの表示名を指定します。 +top-level-flow-type=トップレベルフロータイプ +flow.generic=generic +flow.client=client +top-level-flow-type.tooltip=どの種類のトップレベルフローを作成しますか? 「client」 タイプは、クライアント (アプリケーション) の認証で使用します。 「generic」 はユーザーと他のすべてで使用します。 +create-execution-flow=Execution フローの作成 +flow-type=フロータイプ +flow.form.type=form +flow.generic.type=generic +flow-type.tooltip=どの種類のフォームかを設定します。 +form-provider=フォームプロバイダー +default-groups.tooltip=新規作成または登録されたユーザーは自動的にこれらのグループに追加されます。 +select-a-type.placeholder=タイプを選択してください +available-groups=使用可能なグループ +available-groups.tooltip=デフォルトとして追加したいグループを選択してください。 +value=値 +table-of-group-members=グループメンバーの一覧表 +last-name=姓 +first-name=名 +email=Eメール +toggle-navigation=ナビゲーションの切り替え +manage-account=アカウントの管理 +sign-out=サインアウト +server-info=サーバー情報 +resource-not-found=リソースが見つかりません... +resource-not-found.instruction=お探しのリソースが見つかりませんでした。入力された URL が正しいかご確認ください。 +go-to-the-home-page=ホームページへ移動 » +page-not-found=ページが見つかりません... +page-not-found.instruction=お探しのページが見つかりませんでした。入力された URL が正しいかご確認ください。 +events.tooltip=保存されたレルムのイベントを表示します。イベントは、ユーザーアカウントに関連したイベント、例えばログインなどです。イベントの保存を有効にするには設定へ移動してください。 +select-event-types.placeholder=イベントタイプを選択... +events-config.tooltip=ユーザーと管理イベントの保存を有効にする設定オプションを表示します。 +select-an-action.placeholder=アクションを選択... +event-listeners.tooltip=どのリスナーがレルムのイベントを受け取るか設定します。 +login.save-events.tooltip=有効の場合は、ログインイベントがデータベースに保存され、管理コンソールとアカウント管理で使用することができます。 +clear-events.tooltip=データベース内のすべてのイベントを削除します。 +events.expiration.tooltip=イベントの有効期限を設定します。期限切れのイベントはデータベースから定期的に削除されます。 +admin-events-settings=管理イベントの設定 +save-events=イベントの保存 +admin.save-events.tooltip=有効の場合は、管理イベントがデータベースに保存され、管理コンソールで使用可能になります。 +saved-types.tooltip=どのイベントタイプが保存されるかを設定します。 +include-representation=Representation を含める +include-representation.tooltip=作成または更新リクエストの JSON Representation を含めるかどうかを設定します。 +clear-admin-events.tooltip=データベース内のすべての管理イベントを削除します。 +server-version=サーバーのバージョン +info=情報 +providers=プロバイダー +server-time=サーバーの時刻 +server-uptime=サーバーの稼働時間 +memory=メモリー +total-memory=メモリーの総容量 +free-memory=空きメモリー +used-memory=使用メモリー +system=システム +current-working-directory=現在の作業ディレクトリ +java-version=Java バージョン +java-vendor=Java ベンダー +java-runtime=Java ランタイム +java-vm=Java VM +java-vm-version=Java VM バージョン +java-home=Java ホーム +user-name=ユーザー名 +user-timezone=ユーザータイムゾーン +user-locale=ユーザーロケール +system-encoding=システムエンコーディング +operating-system=オペレーションシステム +os-architecture=OS アーキテクチャ +spi=SPI +granted-roles=許可されたロール +granted-protocol-mappers=許可されたプロトコルマッパー +additional-grants=追加の許可 +revoke=取り消し +new-password=新しいパスワード +password-confirmation=新しいパスワード (確認) +reset-password=パスワードをリセット +credentials.temporary.tooltip=有効の場合は、ユーザーは次のログイン時にパスワードの変更が必要となります。 +remove-totp=TOTP の削除 +credentials.remove-totp.tooltip=ユーザーのワンタイムパスワードジェネレーターを削除します。 +reset-actions=リセットアクション +credentials.reset-actions.tooltip=ユーザーにリセットアクションEメールを送信する際に実行するアクションを設定します。 「Verify email」 は、メールアドレスの確認のためにユーザーにEメールを送信します。 「Update profile」 は、新しい個人情報の入力を必要とします。 「Update password」 は、新しいパスワードの入力を必要とします。 「Configure TOTP」 は、モバイルのパスワードジェネレーターのセットアップを必要とします。 +reset-actions-email=リセットアクションEメール +send-email=Eメールを送信 +credentials.reset-actions-email.tooltip=リンクを記載したEメールをユーザーに送信します。リンクをクリックすることで、ユーザーはリセットアクションを実行できます。ユーザーはリセットの前にログインする必要はありません。例えば、 「Update Password」 アクションを設定してこのボタンをクリックすると、ユーザーはログインなしにパスワードの変更が可能になります。 +add-user=ユーザーの追加 +created-at=作成日 +user-enabled=ユーザーの有効 +user-enabled.tooltip=無効ユーザーはログインすることができません。 +user-temporarily-locked=ユーザーの一時的なロック +user-temporarily-locked.tooltip=ユーザーは何度もログインに失敗してロックされている可能性があります。 +unlock-user=ユーザーをアンロック +federation-link=フェデレーション リンク +email-verified=確認済みのEメール +email-verified.tooltip=ユーザーのEメールは確認済みかどうか設定します。 +required-user-actions=必要なユーザーアクション +required-user-actions.tooltip=ユーザーがログイン時に必要となるアクションを設定します。 「Verify email」 は、メールアドレスの確認のためにユーザーにEメールを送信します。 「Update profile」 は、新しい個人情報の入力を必要とします。 「Update password」 は、新しいパスワードの入力を必要とします。 「Configure TOTP」 は、モバイルのパスワードジェネレーターのセットアップを必要とします。 +locale=ロケール +select-one.placeholder=1つ選択... +impersonate=代理ログイン +impersonate-user=ユーザーの代理 +impersonate-user.tooltip=このユーザーとしてログインします。同じレルム内のユーザーの場合は、このユーザーでログインする前に、現在のログインセッションがログアウトされます。 +identity-provider-alias=アイデンティティ プロバイダーのエイリアス +provider-user-id=プロバイダーのユーザー ID +provider-username=プロバイダーのユーザー名 +no-identity-provider-links-available=使用可能なアイデンティティ プロバイダーのリンクはありません +group-membership=グループメンバーシップ +leave=外す +group-membership.tooltip=メンバーであるグループです。グループから外すには、リストのグループを選択して 「外す」 ボタンをクリックしてください。 +membership.available-groups.tooltip=ユーザーが参加可能なグループです。グループを選択して 「参加」 ボタンをクリックしてください。 +table-of-realm-users=レルムユーザーの一覧表 +view-all-users=すべてのユーザーを参照 +unlock-users=ユーザーのアンロック +no-users-available=使用可能なユーザーはおりません +users.instruction=検索を入力するか、 「すべてのユーザーを参照」 をクリックしてください +consents=同意 +started=開始 +logout-all-sessions=すべてのセッションをログアウト +logout=ログアウト +new-name=新しい名前 +ok=OK +attributes=属性 +role-mappings=ロールマッピング +members=メンバー +details=詳細 +identity-provider-links=アイデンティティ プロバイダーのリンク +register-required-action=Required アクションの登録 +gender=性別 +address=住所 +phone=電話番号 +profile-url=プロフィール URL +picture-url=画像 URL +website=Web サイト +import-keys-and-cert=鍵と証明書をインポート +import-keys-and-cert.tooltip=クライアントの鍵ペアと証明書をアップロードします。 +upload-keys=鍵をアップロード +download-keys-and-cert=鍵と証明書をダウンロード +no-value-assigned.placeholder=アサイン済みの値はありません +remove=削除 +no-group-members=グループメンバーはおりません +temporary=一時的 +join=参加 +event-type=イベントタイプ +events-config=イベント設定 +event-listeners=イベントリスナー +login-events-settings=ログインイベントの設定 +clear-events=イベントのクリア +saved-types=保存タイプ +clear-admin-events=管理イベントのクリア +clear-changes=変更をクリア +error=エラー + +# Authz +# Authz Common +authz-authorization=認可 +authz-owner=オーナー +authz-uri=URI +authz-scopes=スコープ +authz-resource=リソース +authz-resource-type=リソースタイプ +authz-resources=リソース +authz-scope=スコープ +authz-authz-scopes=認可スコープ +authz-policies=ポリシー +authz-permissions=アクセス権 +authz-evaluate=評価 +authz-icon-uri=アイコン URI +authz-icon-uri.tooltip=アイコンを指す URI を設定します。 +authz-select-scope=スコープを選択 +authz-select-resource=リソースを選択 +authz-associated-policies=関連ポリシー +authz-any-resource=任意のリソース +authz-any-scope=任意のスコープ +authz-any-role=任意のロール +authz-policy-evaluation=ポリシー評価 +authz-select-client=クライアントを選択 +authz-select-user=ユーザーを選択 +authz-entitlements=エンタイトルメント +authz-no-resources=リソースはありません +authz-result=結果 +authz-authorization-services-enabled=認可の有効 +authz-authorization-services-enabled.tooltip=きめ細かい認可のサポートを有効/無効にします。 +authz-required=必須 + +# Authz Settings +authz-import-config.tooltip=リソースサーバーの認可設定を含む JSON ファイルをインポートします。 + +authz-policy-enforcement-mode=ポリシー施行モード +authz-policy-enforcement-mode.tooltip=ポリシー施行モードは、認可リクエストを評価する際に適用される方法を決定します。 「Enforcing」 は、与えられたリソースに関連するポリシーが存在しない場合でも、リクエストはデフォルトで拒否されることを意味します。 「Permissive」 は、与えられたリソースに関連するポリシーが存在しない場合でも、リクエストは許可されることを意味します。 「Disabled」 は、完全にポリシーの評価を無効にし、任意のリソースへのアクセスを許可します。 +authz-policy-enforcement-mode-enforcing=Enforcing +authz-policy-enforcement-mode-permissive=Permissive +authz-policy-enforcement-mode-disabled=Disabled + +authz-remote-resource-management=リモートリソース管理 +authz-remote-resource-management.tooltip=リソースは、リソースサーバーによりリモートで管理すべきかどうかを設定します。 オフの場合は、リソースはこの管理コンソールだけで管理されます。 + +authz-export-settings=エクスポート設定 +authz-export-settings.tooltip=このリソースサーバーのすべての認可設定をエクスポートしダウンロードします。 + +# Authz Resource List +authz-no-resources-available=使用可能なリソースはありません。 +authz-no-scopes-assigned=アサイン済みのスコープはありません。 +authz-no-type-defined=定義されたタイプはありません。 +authz-no-permission-assigned=アサイン済みのアクセス権はありません。 +authz-no-policy-assigned=アサイン済みのポリシーはありません。 +authz-create-permission=アクセス権を作成 + +# Authz Resource Detail +authz-add-resource=リソースの追加 +authz-resource-name.tooltip=このリソースのユニークな名前を設定します。名前はリソースの一意な識別に使用され、特定のリソースを照会する際に使用することができます。 +authz-resource-owner.tooltip=このリソースのオーナーです。 +authz-resource-type.tooltip=このリソースのタイプを設定します。異なるリソースインスタンスを同じタイプにグルーピングすることができます。 +authz-resource-uri.tooltip=このリソースを一意に識別する URI を設定します。 +authz-resource-scopes.tooltip=このリソースに関連付けるスコープを設定します。 + +# Authz Scope List +authz-add-scope=スコープの追加 +authz-no-scopes-available=使用可能なスコープはありません。 + +# Authz Scope Detail +authz-scope-name.tooltip=このスコープのユニークな名前を設定します。名前はスコープの一意な識別に使用され、特定のスコープを照会する際に使用することができます。 + +# Authz Policy List +authz-all-types=すべてのタイプ +authz-create-policy=ポリシーを作成 +authz-no-policies-available=使用可能なポリシーはありません。 + +# Authz Policy Detail +authz-policy-name.tooltip=このポリシーの名前を設定します。 +authz-policy-description.tooltip=このポリシーの説明を設定します。 +authz-policy-logic=ロジック +authz-policy-logic-positive=Positive +authz-policy-logic-negative=Negative +authz-policy-logic.tooltip=ロジックは、ポリシーの判定方法を決定します。 「Positive」 の場合は、このポリシーの評価中に得られた結果 (許可または拒否) が判定の実行に使用されます。 「Negative」 の場合は、結果は反転されます。言い換えれば、許可は拒否とその反対になります。 +authz-policy-apply-policy=ポリシーの適用 +authz-policy-apply-policy.tooltip=このポリシーやアクセス権で定義されたスコープに適用するすべてのポリシーを設定します。 +authz-policy-decision-strategy=判定戦略 +authz-policy-decision-strategy.tooltip=判定戦略は、ポリシーの評価方法と最終的な判定方法を決定します。 「Affirmative」 は、全体の判定が positive となるためには、少なくとも1つのポリシーが positive と評価する必要がある、ということを意味します。 「Unanimous」 は、全体の判定が positive となるためには、すべてのポリシーが positive と評価する必要がある、ということを意味します。 「Consensus」 は、positive の数が nagative の数より多くなければならないことを意味します。positive と negative の数が同じ場合は、最終的な判定は negative になります。 +authz-policy-decision-strategy-affirmative=Affirmative +authz-policy-decision-strategy-unanimous=Unanimous +authz-policy-decision-strategy-consensus=Consensus +authz-select-a-policy=ポリシーを選択 + +# Authz Role Policy Detail +authz-add-role-policy=ロールポリシーの追加 +authz-no-roles-assigned=アサイン済みのロールはありません。 +authz-policy-role-realm-roles.tooltip=このポリシーで許可されるレルムロールを指定してください。 +authz-policy-role-clients.tooltip=このポリシーに適用されるクライアントロールをフィルタリングするために、クライアントを選択してください。 +authz-policy-role-client-roles.tooltip=このポリシーで許可されるクライアントロールを指定してください。 + +# Authz User Policy Detail +authz-add-user-policy=ユーザーポリシーの追加 +authz-no-users-assigned=アサイン済みのユーザーはおりません。 +authz-policy-user-users.tooltip=どのユーザーがこのポリシーで許可されるか指定してください。 + +# Authz Time Policy Detail +authz-add-time-policy=タイムポリシーの追加 +authz-policy-time-not-before.tooltip=ポリシーを許可しない日時を定義します。現在日時がこの値より後か、等しい場合にのみ許可されます。 +authz-policy-time-not-on-after=この日時より後 +authz-policy-time-not-on-after.tooltip=ポリシーを許可しない日時を定義します。現在日時がこの値より前か、等しい場合にのみ許可されます。 + +# Authz Drools Policy Detail +authz-add-drools-policy=Drools ポリシーの追加 +authz-policy-drools-maven-artifact-resolve=解決 +authz-policy-drools-maven-artifact=ポリシー Maven アーティファクト +authz-policy-drools-maven-artifact.tooltip=ルールの読み込む先となるアーティファクトを示す Maven GAV を設定します。GAV を提供し 「解決」 をクリックすることで、 「モジュール」 と 「セッション」 フィールドを読み込みます。 +authz-policy-drools-module=モジュール +authz-policy-drools-module.tooltip=このポリシーで使用されるモジュールです。ルールの読み込み先から特定のセッションを選択するにはモジュールを提供する必要があります。 +authz-policy-drools-session=セッション +authz-policy-drools-session.tooltip=このポリシーで使用されるセッションです。セッションは、ポリシーを処理する際に評価するすべてのルールを提供します。 +authz-policy-drools-update-period=更新周期 +authz-policy-drools-update-period.tooltip=アーティファクトの更新をスキャンする間隔を指定します。 + +# Authz JS Policy Detail +authz-add-js-policy=JavaScript ポリシーの追加 +authz-policy-js-code=コード +authz-policy-js-code.tooltip=このポリシーの条件を提供する JavaScript コードを設定します。 + + +# Authz Aggregated Policy Detail +authz-aggregated=集約 +authz-add-aggregated-policy=集約ポリシーの追加 + +# Authz Permission List +authz-no-permissions-available=使用可能なアクセス権はありません。 + +# Authz Permission Detail +authz-permission-name.tooltip=このアクセス権の名前を設定します。 +authz-permission-description.tooltip=このアクセス権の説明を設定します。 + +# Authz Resource Permission Detail +authz-add-resource-permission=リソースアクセス権の追加 +authz-permission-resource-apply-to-resource-type=リソースタイプに適用 +authz-permission-resource-apply-to-resource-type.tooltip=このアクセス権が、特定タイプの全リソースに適用されるかどうかを指定します。この場合、アクセス権は特定リソースタイプの全インスタンスに対して評価されます。 +authz-permission-resource-resource.tooltip=このアクセス権が適用されるリソースインスタンスを指定します。 +authz-permission-resource-type.tooltip=このアクセス権が適用されるリソースタイプを指定します。 + +# Authz Scope Permission Detail +authz-add-scope-permission=スコープアクセス権の追加 +authz-permission-scope-resource.tooltip=選択されたリソースに関連するスコープに制限します。選択されていない場合は、すべてのスコープが使用可能になります。 +authz-permission-scope-scope.tooltip=このアクセス権は1つまたは複数のスコープに適用されるように指定してください。 + +# Authz Evaluation +authz-evaluation-identity-information=アイデンティティ情報 +authz-evaluation-identity-information.tooltip=ポリシーの評価の際に使用されるアイデンティティ情報の設定オプションです。 +authz-evaluation-client.tooltip=認可リクエストを作成するクライアントを選択してください。提供されない場合は、認可リクエストは今いるページのクライアントで行われることになります。 +authz-evaluation-user.tooltip=アクセス権を照会するために使用するユーザーのアイデンティティを選択してください。 +authz-evaluation-role.tooltip=選択されたユーザーに関連付けたいロールを選択してください。 +authz-evaluation-new=新規に評価 +authz-evaluation-re-evaluate=再評価 +authz-evaluation-previous=前の評価 +authz-evaluation-contextual-info=コンテキスト情報 +authz-evaluation-contextual-info.tooltip=ポリシーの評価の際に使用されるコンテキスト情報の設定オプションです。 +authz-evaluation-contextual-attributes=コンテキスト属性 +authz-evaluation-contextual-attributes.tooltip=実行環境や実行コンテキストによって提供される任意の属性を設定します。 +authz-evaluation-permissions.tooltip=ポリシーが適用されるようにアクセス権を設定するオプションです。 +authz-evaluation-evaluate=評価 +authz-evaluation-any-resource-with-scopes=スコープを持つ任意のリソース +authz-evaluation-no-result=認可リクエストから結果を得ることができませんでした。提供されたリソースまたはスコープが、ポリシーと関連付けられているかどうかを確認してください。 +authz-evaluation-no-policies-resource=このリソースのポリシーが見つかりませんでした。 +authz-evaluation-result.tooltip=このアクセス権のリクエストの全体的な結果です。 +authz-evaluation-scopes.tooltip=許可されたスコープリストです。 +authz-evaluation-policies.tooltip=どのポリシーが評価され判定されたか詳細を表示しています。 +authz-evaluation-authorization-data=レスポンス +authz-evaluation-authorization-data.tooltip=認可リクエストの処理の結果として送信された認可データのトークンを表示します。これは、許可を求めたクライアントに対して Keycloak が発行する基本的なものです。現在の認可リクエストで付与されたアクセス権については 「authorization」 クレームを確認してください。 +authz-show-authorization-data=認可データを表示 diff --git a/themes/src/main/resources/theme/base/admin/messages/messages_ja.properties b/themes/src/main/resources/theme/base/admin/messages/messages_ja.properties new file mode 100644 index 0000000000..9a8d26db97 --- /dev/null +++ b/themes/src/main/resources/theme/base/admin/messages/messages_ja.properties @@ -0,0 +1,15 @@ +# encoding: utf-8 +invalidPasswordMinLengthMessage=無効なパスワード: 最小 {0} の長さが必要です。 +invalidPasswordMinLowerCaseCharsMessage=無効なパスワード: 少なくとも {0} 文字の小文字を含む必要があります。 +invalidPasswordMinDigitsMessage=無効なパスワード: 少なくとも {0} 文字の数字を含む必要があります。 +invalidPasswordMinUpperCaseCharsMessage=無効なパスワード: 少なくとも {0} 文字の大文字を含む必要があります。 +invalidPasswordMinSpecialCharsMessage=無効なパスワード: 少なくとも {0} 文字の特殊文字を含む必要があります。 +invalidPasswordNotUsernameMessage=無効なパスワード: ユーザー名と同じパスワードは禁止されています。 +invalidPasswordRegexPatternMessage=無効なパスワード: 正規表現パターンと一致しません。 +invalidPasswordHistoryMessage=無効なパスワード: 最近の {0} パスワードのいずれかと同じパスワードは禁止されています。 + +ldapErrorInvalidCustomFilter=LDAP フィルターのカスタム設定が、 「(」 から開始または 「)」 で終了となっていません。 +ldapErrorMissingClientId=レルムロールマッピングを使用しない場合は、クライアント ID は設定内で提供される必要があります。 +ldapErrorCantPreserveGroupInheritanceWithUIDMembershipType=グループ継承と UID メンバーシップタイプを一緒に保存することはできません。 +ldapErrorCantWriteOnlyForReadOnlyLdap=LDAP プロバイダーモードが WRITABLE ではない場合は、write only を設定することはできません。 +ldapErrorCantWriteOnlyAndReadOnly=write-only と read-only を一緒に設定することはできません。 \ No newline at end of file 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 9c67740e61..72b6c43c06 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 @@ -1090,7 +1090,36 @@ module.controller('ResourceServerPolicyTimeDetailCtrl', function($scope, $route, }, onInitUpdate : function(policy) { - + if (policy.config.dayMonth) { + policy.config.dayMonth = parseInt(policy.config.dayMonth); + } + if (policy.config.dayMonthEnd) { + policy.config.dayMonthEnd = parseInt(policy.config.dayMonthEnd); + } + if (policy.config.month) { + policy.config.month = parseInt(policy.config.month); + } + if (policy.config.monthEnd) { + policy.config.monthEnd = parseInt(policy.config.monthEnd); + } + if (policy.config.year) { + policy.config.year = parseInt(policy.config.year); + } + if (policy.config.yearEnd) { + policy.config.yearEnd = parseInt(policy.config.yearEnd); + } + if (policy.config.hour) { + policy.config.hour = parseInt(policy.config.hour); + } + if (policy.config.hourEnd) { + policy.config.hourEnd = parseInt(policy.config.hourEnd); + } + if (policy.config.minute) { + policy.config.minute = parseInt(policy.config.minute); + } + if (policy.config.minuteEnd) { + policy.config.minuteEnd = parseInt(policy.config.minuteEnd); + } }, onUpdate : function() { @@ -1106,6 +1135,19 @@ module.controller('ResourceServerPolicyTimeDetailCtrl', function($scope, $route, } }, realm, client, $scope); + + $scope.isRequired = function () { + var policy = $scope.policy; + if (policy.config.noa || policy.config.nbf + || policy.config.dayMonth + || policy.config.month + || policy.config.year + || policy.config.hour + || policy.config.minute) { + return false; + } + return true; + } }); module.controller('ResourceServerPolicyAggregateDetailCtrl', function($scope, $route, $location, realm, PolicyController, ResourceServerPolicy, client) { 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 d1041c7780..3bbd471758 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 @@ -365,11 +365,12 @@ module.controller('ClientCertificateImportCtrl', function($scope, $location, $ht ]; if (callingContext == 'jwt-credentials') { - $scope.keyFormats.push('JSON Web Key Set (JWK)'); + $scope.keyFormats.push('Public Key PEM'); + $scope.keyFormats.push('JSON Web Key Set'); } $scope.hideKeystoreSettings = function() { - return $scope.uploadKeyFormat == 'Certificate PEM' || $scope.uploadKeyFormat == 'JSON Web Key Set (JWK)'; + return $scope.uploadKeyFormat == 'Certificate PEM' || $scope.uploadKeyFormat == 'Public Key PEM' || $scope.uploadKeyFormat == 'JSON Web Key Set'; } $scope.uploadKeyFormat = $scope.keyFormats[0]; @@ -791,6 +792,17 @@ module.controller('ClientDetailCtrl', function($scope, realm, client, templates, {name: "INCLUSIVE_WITH_COMMENTS", value: "http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments"} ]; + $scope.oidcSignatureAlgorithms = [ + "unsigned", + "RS256" + ]; + + $scope.requestObjectSignatureAlgorithms = [ + "any", + "none", + "RS256" + ]; + $scope.realm = realm; $scope.samlAuthnStatement = false; $scope.samlMultiValuedRoles = false; @@ -891,6 +903,12 @@ module.controller('ClientDetailCtrl', function($scope, realm, client, templates, $scope.samlForcePostBinding = false; } } + + var attrVal1 = $scope.client.attributes['user.info.response.signature.alg']; + $scope.userInfoSignedResponseAlg = attrVal1==null ? 'unsigned' : attrVal1; + + var attrVal2 = $scope.client.attributes['request.object.signature.alg']; + $scope.requestObjectSignatureAlg = attrVal2==null ? 'any' : attrVal2; } if (!$scope.create) { @@ -955,6 +973,22 @@ module.controller('ClientDetailCtrl', function($scope, realm, client, templates, $scope.client.attributes['saml_name_id_format'] = $scope.nameIdFormat; }; + $scope.changeUserInfoSignedResponseAlg = function() { + if ($scope.userInfoSignedResponseAlg === 'unsigned') { + $scope.client.attributes['user.info.response.signature.alg'] = null; + } else { + $scope.client.attributes['user.info.response.signature.alg'] = $scope.userInfoSignedResponseAlg; + } + }; + + $scope.changeRequestObjectSignatureAlg = function() { + if ($scope.requestObjectSignatureAlg === 'any') { + $scope.client.attributes['request.object.signature.alg'] = null; + } else { + $scope.client.attributes['request.object.signature.alg'] = $scope.requestObjectSignatureAlg; + } + }; + $scope.$watch(function() { return $location.path(); }, function() { @@ -1084,6 +1118,12 @@ module.controller('ClientDetailCtrl', function($scope, realm, client, templates, }, $scope.client, function() { $route.reload(); Notifications.success("Your changes have been saved to the client."); + }, function(error) { + if (error.status == 400 && error.data.error_description) { + Notifications.error(error.data.error_description); + } else { + Notifications.error('Unexpected error when updating client'); + } }); } }; @@ -1198,6 +1238,12 @@ module.controller('CreateClientCtrl', function($scope, realm, client, templates, var id = l.substring(l.lastIndexOf("/") + 1); $location.url("/realms/" + realm.realm + "/clients/" + id); Notifications.success("The client has been created."); + }, function(error) { + if (error.status == 400 && error.data.error_description) { + Notifications.error(error.data.error_description); + } else { + Notifications.error('Unexpected error when creating client'); + } }); }; diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/authz/policy/provider/resource-server-policy-time-detail.html b/themes/src/main/resources/theme/base/admin/resources/partials/authz/policy/provider/resource-server-policy-time-detail.html index fc4af74f25..517773470f 100644 --- a/themes/src/main/resources/theme/base/admin/resources/partials/authz/policy/provider/resource-server-policy-time-detail.html +++ b/themes/src/main/resources/theme/base/admin/resources/partials/authz/policy/provider/resource-server-policy-time-detail.html @@ -37,7 +37,7 @@
- +
{{:: 'authz-policy-time-not-before.tooltip' | translate}} @@ -45,10 +45,50 @@
- +
{{:: 'authz-policy-time-not-on-after.tooltip' | translate}} +
+ + +
+   to   +
+ {{:: 'authz-policy-time-day-month.tooltip' | translate}} +
+
+ + +
+   to   +
+ {{:: 'authz-policy-time-month.tooltip' | translate}} +
+
+ + +
+   to   +
+ {{:: 'authz-policy-time-year.tooltip' | translate}} +
+
+ + +
+   to   +
+ {{:: 'authz-policy-time-hour.tooltip' | translate}} +
+
+ + +
+   to   +
+ {{:: 'authz-policy-time-minute.tooltip' | translate}} +
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 1378f787e4..d8f0d246a1 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 @@ -333,6 +333,36 @@
+
+ {{:: 'fine-oidc-endpoint-conf' | translate}} {{:: 'fine-oidc-endpoint-conf.tooltip' | translate}} +
+ +
+
+ +
+
+ {{:: 'user-info-signed-response-alg.tooltip' | translate}} +
+
+ +
+
+ +
+
+ {{:: 'request-object-signature-alg.tooltip' | translate}} +
+
+
diff --git a/themes/src/main/resources/theme/base/admin/theme.properties b/themes/src/main/resources/theme/base/admin/theme.properties index 65bc856908..b292e393f1 100644 --- a/themes/src/main/resources/theme/base/admin/theme.properties +++ b/themes/src/main/resources/theme/base/admin/theme.properties @@ -1,2 +1,2 @@ import=common/keycloak -locales=ca,de,en,es,fr,it,lt,pt-BR,ru \ No newline at end of file +locales=ca,de,en,es,fr,it,ja,lt,pt-BR,ru \ No newline at end of file diff --git a/themes/src/main/resources/theme/base/email/messages/messages_ja.properties b/themes/src/main/resources/theme/base/email/messages/messages_ja.properties new file mode 100644 index 0000000000..087170a10c --- /dev/null +++ b/themes/src/main/resources/theme/base/email/messages/messages_ja.properties @@ -0,0 +1,25 @@ +# encoding: utf-8 +emailVerificationSubject=Eメールの確認 +emailVerificationBody=このメールアドレスで {2} アカウントが作成されたました。以下のリンクをクリックしてメールアドレスの確認を完了してください。\n\n{0}\n\nこのリンクは {1} 分間だけ有効です。\n\nもしこのアカウントの作成に心当たりがない場合は、このメールを無視してください。 +emailVerificationBodyHtml=

このメールアドレスで {2} アカウントが作成されました。以下のリンクをクリックしてメールアドレスの確認を完了してください。

{0}

このリンクは {1} 分間だけ有効です。

もしこのアカウントの作成に心当たりがない場合は、このメールを無視してください。

+identityProviderLinkSubject=リンク {0} +identityProviderLinkBody=あなたの "{1}" アカウントと {2} ユーザーの "{0}" アカウントのリンクが要求されました。以下のリンクをクリックしてアカウントのリンクを行ってください。\n\n{3}\n\nこのリンクは {4} 分間だけ有効です。\n\nもしアカウントのリンクを行わない場合は、このメッセージを無視してください。アカウントのリンクを行うことで、{0} 経由で {1} にログインすることができるようになります。 +identityProviderLinkBodyHtml=

あなたの {1} アカウントと {2} ユーザーの {0} アカウントのリンクが要求されました。以下のリンクをクリックしてアカウントのリンクを行ってください。

{3}

このリンクは {4} 分間だけ有効です。

もしアカウントのリンクを行わない場合は、このメッセージを無視してください。アカウントのリンクを行うことで、{0} 経由で {1} にログインすることができるようになります。

+passwordResetSubject=パスワードのリセット +passwordResetBody=あなたの {2} アカウントのクレデンシャルの変更が要求されています。以下のリンクをクリックしてクレデンシャルのリセットを行ってください。\n\n{0}\n\nこのリンクとコードは {1} 分間だけ有効です。\n\nもしクレデンシャルのリセットを行わない場合は、このメッセージを無視してください。何も変更されません。 +passwordResetBodyHtml=

あなたの {2} アカウントのクレデンシャルの変更が要求されています。以下のリンクをクリックしてクレデンシャルのリセットを行ってください。

{0}

このリンクとコードは {1} 分間だけ有効です。

もしクレデンシャルのリセットを行わない場合は、このメッセージを無視してください。何も変更されません。

+executeActionsSubject=アカウントの更新 +executeActionsBody=管理者よりあなたの {2} アカウントの更新が要求されています。以下のリンクをクリックしてこのプロセスを開始してください。\n\n{0}\n\nこのリンクは {1} 分間だけ有効です。\n\n管理者からのこの変更要求についてご存知ない場合は、このメッセージを無視してください。何も変更されません。 +executeActionsBodyHtml=

管理者よりあなたの {2} アカウントの更新が要求されています。以下のリンクをクリックしてこのプロセスを開始してください。

{0}

このリンクは {1} 分間だけ有効です。

管理者からのこの変更要求についてご存知ない場合は、このメッセージを無視してください。何も変更されません。

+eventLoginErrorSubject=ログインエラー +eventLoginErrorBody={0} に {1} からのログイン失敗があなたのアカウントで検出されました。心当たりがない場合は、管理者に連絡してください。 +eventLoginErrorBodyHtml=

{0} に {1} からのログイン失敗があなたのアカウントで検出されました。心当たりがない場合は管理者に連絡してください。

+eventRemoveTotpSubject=TOTPの削除 +eventRemoveTotpBody={0} に {1} からの操作で TOTP が削除されました。心当たりがない場合は、管理者に連絡してください。 +eventRemoveTotpBodyHtml=

{0} に {1} からの操作で TOTP が削除されました。心当たりがない場合は、管理者に連絡してください。

+eventUpdatePasswordSubject=パスワードの更新 +eventUpdatePasswordBody={0} に {1} からの操作であなたのパスワードが変更されました。心当たりがない場合は、管理者に連絡してください。 +eventUpdatePasswordBodyHtml=

{0} に {1} からの操作であなたのパスワードが変更されました。心当たりがない場合は、管理者に連絡してください。

+eventUpdateTotpSubject=TOTPの更新 +eventUpdateTotpBody={0} に {1} からの操作で TOTP が更新されました。心当たりがない場合は、管理者に連絡してください。 +eventUpdateTotpBodyHtml=

{0} に {1} からの操作で TOTP が更新されました。心当たりがない場合は、管理者に連絡してください。

diff --git a/themes/src/main/resources/theme/base/email/theme.properties b/themes/src/main/resources/theme/base/email/theme.properties index db32af801b..ff4018c4f5 100644 --- a/themes/src/main/resources/theme/base/email/theme.properties +++ b/themes/src/main/resources/theme/base/email/theme.properties @@ -1 +1 @@ -locales=ca,de,en,es,fr,it,lt,pt-BR,ru \ No newline at end of file +locales=ca,de,en,es,fr,it,ja,lt,pt-BR,ru \ No newline at end of file diff --git a/themes/src/main/resources/theme/base/login/messages/messages_en.properties b/themes/src/main/resources/theme/base/login/messages/messages_en.properties index 45305c7177..17e1b740af 100755 --- a/themes/src/main/resources/theme/base/login/messages/messages_en.properties +++ b/themes/src/main/resources/theme/base/login/messages/messages_en.properties @@ -214,6 +214,7 @@ locale_en=English locale_es=Espa\u00F1ol locale_fr=Fran\u00e7ais locale_it=Italian +locale_ja=\u65E5\u672C\u8A9E locale_pt_BR=Portugu\u00EAs (Brasil) locale_pt-BR=Portugu\u00EAs (Brasil) locale_ru=\u0420\u0443\u0441\u0441\u043A\u0438\u0439 diff --git a/themes/src/main/resources/theme/base/login/messages/messages_ja.properties b/themes/src/main/resources/theme/base/login/messages/messages_ja.properties new file mode 100644 index 0000000000..e0d1fd87d1 --- /dev/null +++ b/themes/src/main/resources/theme/base/login/messages/messages_ja.properties @@ -0,0 +1,218 @@ +# encoding: utf-8 +doLogIn=ログイン +doRegister=登録 +doCancel=キャンセル +doSubmit=送信 +doYes=はい +doNo=いいえ +doContinue=続ける +doAccept=承諾 +doDecline=却下 +doForgotPassword=パスワードをお忘れですか? +doClickHere=クリックしてください +doImpersonate=代理ログイン +kerberosNotConfigured=Kerberos は設定されていません +kerberosNotConfiguredTitle=Kerberos は設定されていません +bypassKerberosDetail=Kerberos 経由でログインしていないか、ブラウザーで Kerberos ログインの設定がされていません。他の手段でログインするには 「続ける」 をクリックしてください。 +kerberosNotSetUp=Kerberos が設定されていません。ログインできません。 +registerWithTitle={0} で登録 +registerWithTitleHtml={0} +loginTitle={0} にログイン +loginTitleHtml={0} +impersonateTitle={0} ユーザーの代理 +impersonateTitleHtml={0} ユーザーの代理 +realmChoice=レルム +unknownUser=不明なユーザー +loginTotpTitle=モバイル Authenticator セットアップ +loginProfileTitle=アカウント情報の更新 +loginTimeout=ログインに時間がかかりすぎています。最初からログイン処理を開始します。 +oauthGrantTitle=Grant アクセス +oauthGrantTitleHtml={0} +errorTitle=申し訳ございません +errorTitleHtml=申し訳ございません +emailVerifyTitle=Eメール確認 +emailForgotTitle=パスワードをお忘れですか? +updatePasswordTitle=パスワード更新 +codeSuccessTitle=成功コード +codeErrorTitle=エラーコード\: {0} + +termsTitle=利用規約 +termsTitleHtml=利用規約 +termsText=

利用規約はここで設定する必要があります

+ +recaptchaFailed=無効な reCAPTCHA +recaptchaNotConfigured=reCAPTCHA が必須ですが、設定されていません +consentDenied=同意は拒否されました。 + +noAccount=新規ユーザーですか? +username=ユーザー名 +usernameOrEmail=ユーザー名 または メールアドレス +firstName=名 +givenName=名 +fullName=氏名 +lastName=姓 +familyName=姓 +email=Eメール +password=パスワード +passwordConfirm=パスワード (確認) +passwordNew=新しいパスワード +passwordNewConfirm=新しいパスワード (確認) +rememberMe=ログイン状態の保存 +authenticatorCode=ワンタイムコード +address=住所 +street=番地 +locality=市区町村 +region=都道府県 +postal_code=郵便番号 +country=国 +emailVerified=確認済みEメール +gssDelegationCredential=GSS 代行クレデンシャル + +loginTotpStep1=FreeOTP または Google Authenticator (Google認証システム) をご自身のデバイスにインストールしてください。これらのアプリケーションは Google Play と Apple App Store で入手できます。 +loginTotpStep2=アプリケーションを開きバーコードをスキャンするかキーを入力してください。 +loginTotpStep3=アプリケーションで提供されたワンタイムコードを入力して送信をクリックし、セットアップを完了してください。 +loginTotpOneTime=ワンタイムコード + +oauthGrantRequest=アクセス権を許可してよろしいでしょうか? +inResource=in + +emailVerifyInstruction1=メールアドレスを確認する手順を記載したEメールを送信しました。 +emailVerifyInstruction2=Eメールで確認コードを受け取っていませんか? +emailVerifyInstruction3=Eメールを再送信します。 + +emailLinkIdpTitle=リンク {0} +emailLinkIdp1={0} の {1} アカウントを あなたの {2} アカウントとリンクするための手順を記載したEメールを送信しました。 +emailLinkIdp2=Eメールで確認コードを受け取っていませんか? +emailLinkIdp3=Eメールを再送信します。 + +backToLogin=« ログインに戻る + +emailInstruction=ユーザー名またメールアドレスを入力してください。新しいパスワードの設定方法をご案内いたします。 + +copyCodeInstruction=このコードをコピーし、あなたのアプリケーションにペーストしてください: + +personalInfo=個人情報: +role_admin=管理者 +role_realm-admin=レルム管理者 +role_create-realm=レルムの作成 +role_create-client=クライアントの作成 +role_view-realm=レルムの参照 +role_view-users=ユーザーの参照 +role_view-applications=アプリケーションの参照 +role_view-clients=クライアントの参照 +role_view-events=イベントの参照 +role_view-identity-providers=アイデンティティ プロバイダーの参照 +role_manage-realm=レルムの管理 +role_manage-users=ユーザーの管理 +role_manage-applications=アプリケーションの管理 +role_manage-identity-providers=アイデンティティ プロバイダーの管理 +role_manage-clients=クライアントの管理 +role_manage-events=イベントの管理 +role_view-profile=プロフィールの参照 +role_manage-account=アカウントの管理 +role_read-token=トークンの読み取り +role_offline-access=オフラインアクセス +client_account=アカウント +client_security-admin-console=セキュリティ管理コンソール +client_admin-cli=管理 CLI +client_realm-management=レルム管理 +client_broker=ブローカー + +invalidUserMessage=無効なユーザー名またはパスワードです。 +invalidEmailMessage=無効なメールアドレスです。 +accountDisabledMessage=アカウントは無効です。管理者に連絡してください。 +accountTemporarilyDisabledMessage=アカウントは一時的に無効です。管理者に連絡、またはしばらく時間をおいてから再度お試しください。 +expiredCodeMessage=ログインタイムアウトが発生しました。再度ログインしてください。 + +missingFirstNameMessage=名を指定してください。 +missingLastNameMessage=姓を指定してください。 +missingEmailMessage=Eメールを指定してください。 +missingUsernameMessage=ユーザー名を指定してください。 +missingPasswordMessage=パスワードを指定してください。 +missingTotpMessage=Authenticator コードを指定してください。 +notMatchPasswordMessage=パスワードが一致していません。 + +invalidPasswordExistingMessage=間違った既存のパスワードです。 +invalidPasswordConfirmMessage=パスワード確認が一致していません。 +invalidTotpMessage=間違った Authenticator コードです。 + +usernameExistsMessage=既に存在するユーザー名です。 +emailExistsMessage=既に存在するEメールです。 + +federatedIdentityExistsMessage={0} {1} のユーザーは既に存在します。そのアカウントををリンクするにはアカウント管理にログインしてください。 + +confirmLinkIdpTitle=既に存在するアカウントです。 +federatedIdentityConfirmLinkMessage={0} {1} のユーザーは既に存在します。継続しますか? +federatedIdentityConfirmReauthenticateMessage={1} でアカウントをリンクするために {0} として認証します +confirmLinkIdpReviewProfile=プロフィールの確認 +confirmLinkIdpContinue=既存のアカウントに追加する + +configureTotpMessage=アカウントを有効にするにはモバイル Authenticator のセットアップが必要です。 +updateProfileMessage=アカウントを有効にするにはユーザープロフィールの更新が必要です。 +updatePasswordMessage=アカウントを有効にするにはパスワードの更新が必要です。 +verifyEmailMessage=アカウントを有効にするにはメールアドレスの確認が必要です。 +linkIdpMessage=アカウントを {0} とリンクするにはメールアドレスの確認が必要です。 + +emailSentMessage=詳細な手順を記載したEメールをすぐに受信してください。 +emailSendErrorMessage=Eメールの送信に失敗しました。しばらく時間をおいてから再度お試しください。 + +accountUpdatedMessage=アカウントが更新されました。 +accountPasswordUpdatedMessage=パスワードが更新されました。 + +noAccessMessage=アクセスがありません + +invalidPasswordMinLengthMessage=無効なパスワード: 最小 {0} の長さが必要です。 +invalidPasswordMinDigitsMessage=無効なパスワード: 少なくとも {0} 文字の数字を含む必要があります。 +invalidPasswordMinLowerCaseCharsMessage=無効なパスワード: 少なくとも {0} 文字の小文字を含む必要があります。 +invalidPasswordMinUpperCaseCharsMessage=無効なパスワード: 少なくとも {0} 文字の大文字を含む必要があります。 +invalidPasswordMinSpecialCharsMessage=無効なパスワード: 少なくとも {0} 文字の特殊文字を含む必要があります。 +invalidPasswordNotUsernameMessage=無効なパスワード: ユーザー名と同じパスワードは禁止されています。 +invalidPasswordRegexPatternMessage=無効なパスワード: 正規表現パターンと一致しません。 +invalidPasswordHistoryMessage=無効なパスワード: 最近の {0} パスワードのいずれかと同じパスワードは禁止されています。 + +failedToProcessResponseMessage=応答を処理できませんでした +httpsRequiredMessage=HTTPS が必須です +realmNotEnabledMessage=レルムが有効ではありません +invalidRequestMessage=無効なリクエストです +failedLogout=ログアウトに失敗しました +unknownLoginRequesterMessage=不明なログイン要求元です +loginRequesterNotEnabledMessage=ログイン要求元は有効ではありません +bearerOnlyMessage=bearer-only のアプリケーションはブラウザーログインを開始することが許可されていません +standardFlowDisabledMessage=与えられた response_type でクライアントはブラウザーログインを開始することが許可されていません。 Standard Flow は無効です。 +implicitFlowDisabledMessage=与えられた response_type でクライアントはブラウザーログインを開始することが許可されていません。 Implicit Flow は無効です。 +invalidRedirectUriMessage=無効なリダイレクト URI です +unsupportedNameIdFormatMessage=サポートされていない NameID Format です +invalidRequesterMessage=無効な要求元です +registrationNotAllowedMessage=登録は許可されていません +resetCredentialNotAllowedMessage=クレデンシャルのリセットは許可されていません + +permissionNotApprovedMessage=アクセス権は承認されていません。 +noRelayStateInResponseMessage=アイデンティティ プロバイダーからの応答に RelayState がありません。 +insufficientPermissionMessage=アイデンティティにリンクするには不十分なアクセス権です。 +couldNotProceedWithAuthenticationRequestMessage=アイデンティティ プロバイダーに認証要求を続行できませんでした。 +couldNotObtainTokenMessage=アイデンティティ プロバイダーからトークンを取得できませんでした。 +unexpectedErrorRetrievingTokenMessage=アイデンティティ プロバイダーからのトークン取得で予期せぬエラーが発生しました。 +unexpectedErrorHandlingResponseMessage=アイデンティティ プロバイダーからの応答を処理する際に予期せぬエラーが発生しました。 +identityProviderAuthenticationFailedMessage=認証に失敗しました。アイデンティティ プロバイダーを使用して認証できませんでした。 +identityProviderDifferentUserMessage={1} として認証されることを期待していましたが、{0} として認証されました +couldNotSendAuthenticationRequestMessage=アイデンティティ プロバイダーに認証要求を送信することができませんでした。 +unexpectedErrorHandlingRequestMessage=アイデンティティ プロバイダーへの認証要求を処理する際に予期せぬエラーが発生しました。 +invalidAccessCodeMessage=無効なアクセスコードです。 +sessionNotActiveMessage=セッションが有効ではありません。 +invalidCodeMessage=エラーが発生しました。アプリケーションを介して再度ログインしてください。 +identityProviderUnexpectedErrorMessage=アイデンティティ プロバイダーによる認証の再に予期せぬエラーが発生しました +identityProviderNotFoundMessage=該当の識別子を持つアイデンティティ プロバイダーが見つかりませんでした。 +identityProviderLinkSuccess=アカウントが正常に {0} の {1} アカウントにリンクされました。 +staleCodeMessage=このページはもはや有効ではありませんので、アプリケーションに戻り再度ログインしてください +realmSupportsNoCredentialsMessage=レルムはクレデンシャルタイプをサポートしていません。 +identityProviderNotUniqueMessage=レルムは複数のアイデンティティ プロバイダーをサポートしています。どのアイデンティティ プロバイダーが認証に使用されるべきか判断できませんでした。 +emailVerifiedMessage=メールアドレスが確認できました。 +staleEmailVerificationLink=クリックされたリンクは古いリンクでありもはや有効ではありません。おそらくEメールは既に確認済みではないでしょうか? + +backToApplication=« アプリケーションに戻る +missingParameterMessage=不足パラメータ\: {0} +clientNotFoundMessage=クライアントが見つかりません。 +clientDisabledMessage=クライアントが無効になっています。 +invalidParameterMessage=無効なパラメータ\: {0} +alreadyLoggedIn=既にログインしています。 + diff --git a/themes/src/main/resources/theme/base/login/theme.properties b/themes/src/main/resources/theme/base/login/theme.properties index db32af801b..ff4018c4f5 100644 --- a/themes/src/main/resources/theme/base/login/theme.properties +++ b/themes/src/main/resources/theme/base/login/theme.properties @@ -1 +1 @@ -locales=ca,de,en,es,fr,it,lt,pt-BR,ru \ No newline at end of file +locales=ca,de,en,es,fr,it,ja,lt,pt-BR,ru \ No newline at end of file diff --git a/wildfly/server-subsystem/pom.xml b/wildfly/server-subsystem/pom.xml index 5601739bdc..05b0fc0c0b 100755 --- a/wildfly/server-subsystem/pom.xml +++ b/wildfly/server-subsystem/pom.xml @@ -47,11 +47,34 @@ **/*TestCase.java - + + + src/main/config/default-server-subsys-config.properties + + + + src/main/resources + true + + + + + src/test/resources + true + + + + com.fasterxml.jackson.core + jackson-core + + + com.fasterxml.jackson.core + jackson-databind + org.wildfly.core wildfly-controller diff --git a/wildfly/server-subsystem/src/main/config/default-server-subsys-config.properties b/wildfly/server-subsystem/src/main/config/default-server-subsys-config.properties new file mode 100644 index 0000000000..2355ab0e7e --- /dev/null +++ b/wildfly/server-subsystem/src/main/config/default-server-subsys-config.properties @@ -0,0 +1,73 @@ +# IMPORTANT: If you change this file you should also make equivalent changes +# to src/main/resources/cli/default-keycloak-subsys-config.cli +# The CLI file is packaged with the subsystem and extracted by the overlay distribution. + +keycloak.server.subsys.default.config=\ +\ + auth\ + \ + classpath:${jboss.home.dir}/providers/*\ + \ + master\ + 900\ + \ + 2592000\ + true\ + true\ + ${jboss.home.dir}/themes\ + \ + \ + jpa\ + \ + \ + \ + \ + \ + \ + \ + jpa\ + \ + \ + jpa\ + \ + \ + \ + \ + \ + jpa\ + \ + \ + jpa\ + \ + \ + basic\ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + default\ + \ + \ + \ + \ + \ + \ + \ + ${keycloak.jta.lookup.provider:jboss}\ + \ + \ +\ \ No newline at end of file diff --git a/wildfly/server-subsystem/src/main/java/org/keycloak/subsystem/server/attributes/ModulesListAttributeBuilder.java b/wildfly/server-subsystem/src/main/java/org/keycloak/subsystem/server/attributes/ModulesListAttributeBuilder.java new file mode 100644 index 0000000000..d47ea1086b --- /dev/null +++ b/wildfly/server-subsystem/src/main/java/org/keycloak/subsystem/server/attributes/ModulesListAttributeBuilder.java @@ -0,0 +1,32 @@ +/* + * 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.subsystem.server.attributes; + +import org.jboss.as.controller.StringListAttributeDefinition; + +/** + * + * @author Stan Silvert ssilvert@redhat.com (C) 2016 Red Hat Inc. + */ +public class ModulesListAttributeBuilder extends StringListAttributeDefinition.Builder { + public ModulesListAttributeBuilder() { + super("modules"); + setAllowExpression(true); + setAllowNull(true); + } +} diff --git a/wildfly/server-subsystem/src/main/java/org/keycloak/subsystem/server/attributes/ProvidersListAttributeBuilder.java b/wildfly/server-subsystem/src/main/java/org/keycloak/subsystem/server/attributes/ProvidersListAttributeBuilder.java new file mode 100644 index 0000000000..2b4952e1fc --- /dev/null +++ b/wildfly/server-subsystem/src/main/java/org/keycloak/subsystem/server/attributes/ProvidersListAttributeBuilder.java @@ -0,0 +1,37 @@ +/* + * 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.subsystem.server.attributes; + +import org.jboss.as.controller.StringListAttributeDefinition; +import org.jboss.dmr.ModelNode; + +/** + * + * @author Stan Silvert ssilvert@redhat.com (C) 2016 Red Hat Inc. + */ +public class ProvidersListAttributeBuilder extends StringListAttributeDefinition.Builder { + public ProvidersListAttributeBuilder() { + super("providers"); + ModelNode provider = new ModelNode(); + provider.add("classpath:${jboss.home.dir}/providers/*"); + this.defaultValue = provider; + setAllowExpression(true); + setAllowNull(true); + } + +} diff --git a/wildfly/server-subsystem/src/main/java/org/keycloak/subsystem/server/extension/JsonConfigConverter.java b/wildfly/server-subsystem/src/main/java/org/keycloak/subsystem/server/extension/JsonConfigConverter.java new file mode 100644 index 0000000000..aa4d3f578d --- /dev/null +++ b/wildfly/server-subsystem/src/main/java/org/keycloak/subsystem/server/extension/JsonConfigConverter.java @@ -0,0 +1,262 @@ +/* + * 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.subsystem.server.extension; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import org.jboss.as.controller.PathAddress; +import org.jboss.as.controller.operations.common.Util; +import org.jboss.dmr.ModelNode; + +import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.WRITE_ATTRIBUTE_OPERATION; + +import static org.keycloak.subsystem.server.extension.KeycloakSubsystemDefinition.PROVIDERS; +import static org.keycloak.subsystem.server.extension.KeycloakSubsystemDefinition.MASTER_REALM_NAME; +import static org.keycloak.subsystem.server.extension.KeycloakSubsystemDefinition.SCHEDULED_TASK_INTERVAL; +import static org.keycloak.subsystem.server.extension.ThemeResourceDefinition.CACHE_TEMPLATES; +import static org.keycloak.subsystem.server.extension.ThemeResourceDefinition.CACHE_THEMES; +import static org.keycloak.subsystem.server.extension.ThemeResourceDefinition.DEFAULT; +import static org.keycloak.subsystem.server.extension.ThemeResourceDefinition.DIR; +import static org.keycloak.subsystem.server.extension.ThemeResourceDefinition.MODULES; +import static org.keycloak.subsystem.server.extension.ThemeResourceDefinition.STATIC_MAX_AGE; +import static org.keycloak.subsystem.server.extension.ThemeResourceDefinition.WELCOME_THEME; + +/** + * Converts json representation of Keycloak config to DMR operations. + * + * @author Stan Silvert ssilvert@redhat.com (C) 2016 Red Hat Inc. + */ +public class JsonConfigConverter { + + private static final List NON_SPI_LIST = new ArrayList<>(); + + static { + NON_SPI_LIST.add("providers"); + NON_SPI_LIST.add("admin"); + NON_SPI_LIST.add("theme"); + NON_SPI_LIST.add("scheduled"); + } + + /** + * Convert keycloak-server.json to DMR operations that write to standalone.xml + * or domain.xml. + * + * @param json The json representation of the config. + * @param subsysAddress The management model address of the keycloak-server subsystem. + * @return A list of DMR operations. + * @throws IOException If the json can not be parsed. + */ + public static List convertJsonConfig(String json, PathAddress subsysAddress) throws IOException { + List list = new ArrayList<>(); + + JsonNode root = new ObjectMapper().readTree(json); + + list.add(masterRealmName(root, subsysAddress)); + list.add(scheduledTaskInterval(root, subsysAddress)); + list.add(providers(root, subsysAddress)); + list.add(theme(root, subsysAddress.append(ThemeResourceDefinition.TAG_NAME, + ThemeResourceDefinition.RESOURCE_NAME))); + list.addAll(spis(root, subsysAddress)); + + return list; + } + + private static ModelNode masterRealmName(JsonNode root, PathAddress addr) { + JsonNode targetNode = getNode(root, "admin", "realm"); + String value = MASTER_REALM_NAME.getDefaultValue().asString(); + if (targetNode != null) value = targetNode.asText(value); + + ModelNode op = Util.createOperation(WRITE_ATTRIBUTE_OPERATION, addr); + op.get("name").set(MASTER_REALM_NAME.getName()); + op.get("value").set(value); + return op; + } + + private static ModelNode scheduledTaskInterval(JsonNode root, PathAddress addr) { + JsonNode targetNode = getNode(root, "scheduled", "interval"); + Long value = SCHEDULED_TASK_INTERVAL.getDefaultValue().asLong(); + if (targetNode != null) value = targetNode.asLong(value); + + ModelNode op = Util.createOperation(WRITE_ATTRIBUTE_OPERATION, addr); + op.get("name").set(SCHEDULED_TASK_INTERVAL.getName()); + op.get("value").set(value); + return op; + } + + private static ModelNode providers(JsonNode root, PathAddress addr) { + JsonNode targetNode = getNode(root, "providers"); + ModelNode value = PROVIDERS.getDefaultValue(); + if (targetNode != null && targetNode.isArray()) { + value = new ModelNode(); + for (JsonNode node : targetNode) { + value.add(node.asText()); + } + } + + ModelNode op = Util.createOperation(WRITE_ATTRIBUTE_OPERATION, addr); + op.get("name").set(PROVIDERS.getName()); + op.get("value").set(value); + return op; + } + + private static ModelNode theme(JsonNode root, PathAddress addr) { + JsonNode themeNode = getNode(root, "theme"); + ModelNode op = Util.createAddOperation(addr); + + JsonNode targetNode = getNode(themeNode, "staticMaxAge"); + Long lValue = STATIC_MAX_AGE.getDefaultValue().asLong(); + if (targetNode != null) lValue = targetNode.asLong(lValue); + op.get(STATIC_MAX_AGE.getName()).set(lValue); + + targetNode = getNode(themeNode, "cacheTemplates"); + Boolean bValue = CACHE_TEMPLATES.getDefaultValue().asBoolean(); + if (targetNode != null) bValue = targetNode.asBoolean(bValue); + op.get(CACHE_TEMPLATES.getName()).set(bValue); + + targetNode = getNode(themeNode, "cacheThemes"); + bValue = CACHE_THEMES.getDefaultValue().asBoolean(); + if (targetNode != null) bValue = targetNode.asBoolean(bValue); + op.get(CACHE_THEMES.getName()).set(bValue); + + targetNode = getNode(themeNode, "folder", "dir"); + String sValue = DIR.getDefaultValue().asString(); + if (targetNode != null) sValue = targetNode.asText(sValue); + op.get(DIR.getName()).set(sValue); + + targetNode = getNode(themeNode, "welcomeTheme"); + if (targetNode != null) op.get(WELCOME_THEME.getName()).set(targetNode.asText()); + + targetNode = getNode(themeNode, "default"); + if (targetNode != null) op.get(DEFAULT.getName()).set(targetNode.asText()); + + targetNode = getNode(themeNode, "module", "modules"); + if (targetNode != null && targetNode.isArray()) { + op.get(MODULES.getName()).set(themeModules(targetNode)); + } + + return op; + } + + private static ModelNode themeModules(JsonNode modulesNode) { + ModelNode modules = new ModelNode(); + for (JsonNode node : modulesNode) { + modules.add(node.asText()); + } + return modules; + } + + private static Collection spis(JsonNode root, PathAddress addr) { + List spis = new ArrayList<>(); + + Iterator spiIterator = root.fieldNames(); + while (spiIterator.hasNext()) { + String spiName = spiIterator.next(); + if (NON_SPI_LIST.contains(spiName)) continue; + + PathAddress spiAddr = addr.append("spi", spiName); + spis.addAll(spi(root, spiAddr, spiName)); + } + + return spis; + } + + private static List spi(JsonNode root, PathAddress spiAddr, String spiName) { + List spiAndProviders = new ArrayList<>(); + ModelNode op = Util.createAddOperation(spiAddr); + spiAndProviders.add(op); + + Iterator providerIterator = root.get(spiName).fieldNames(); + while (providerIterator.hasNext()) { + String providerName = providerIterator.next(); + if ("provider".equals(providerName)) { + op.get(SpiResourceDefinition.DEFAULT_PROVIDER.getName()).set(getNode(root, spiName, "provider").asText()); + } else { + PathAddress providerAddr = spiAddr.append("provider", providerName); + spiAndProviders.add(spiProvider(getNode(root, spiName, providerName), providerAddr)); + } + } + + return spiAndProviders; + } + + private static ModelNode spiProvider(JsonNode providerNode, PathAddress providerAddr) { + ModelNode op = Util.createAddOperation(providerAddr); + + ModelNode properties = new ModelNode(); + + Iterator propNames = providerNode.fieldNames(); + while (propNames.hasNext()) { + String propName = propNames.next(); + + if ("enabled".equals(propName)) { + op.get(ProviderResourceDefinition.ENABLED.getName()).set(providerNode.get(propName).asBoolean()); + } else { + if (providerNode.get(propName).isArray()) { + properties.get(propName).set(makeArrayText(providerNode.get(propName))); + } else { + properties.get(propName).set(providerNode.get(propName).asText()); + } + } + } + + if (properties.isDefined() && !properties.asPropertyList().isEmpty()) { + op.get("properties").set(properties); + } + + if (!op.hasDefined(ProviderResourceDefinition.ENABLED.getName())) { + op.get(ProviderResourceDefinition.ENABLED.getName()).set(ProviderResourceDefinition.ENABLED.getDefaultValue()); + } + + return op; + } + + private static String makeArrayText(JsonNode arrayNode) { + StringBuilder builder = new StringBuilder("["); + + Iterator nodes = arrayNode.iterator(); + while (nodes.hasNext()) { + JsonNode node = nodes.next(); + builder.append("\""); + builder.append(node.asText()); + builder.append("\""); + if (nodes.hasNext()) builder.append(","); + } + builder.append("]"); + + return builder.toString(); + } + + private static JsonNode getNode(JsonNode root, String... path) { + if (root == null) { + return null; + } + JsonNode n = root; + for (String p : path) { + n = n.get(p); + if (n == null) { + return null; + } + } + return n; + } + +} diff --git a/wildfly/server-subsystem/src/main/java/org/keycloak/subsystem/server/extension/KeycloakAdapterConfigService.java b/wildfly/server-subsystem/src/main/java/org/keycloak/subsystem/server/extension/KeycloakAdapterConfigService.java index 03a2fe2251..0eec2eed0a 100755 --- a/wildfly/server-subsystem/src/main/java/org/keycloak/subsystem/server/extension/KeycloakAdapterConfigService.java +++ b/wildfly/server-subsystem/src/main/java/org/keycloak/subsystem/server/extension/KeycloakAdapterConfigService.java @@ -16,9 +16,15 @@ */ package org.keycloak.subsystem.server.extension; +import org.jboss.as.controller.PathAddress; +import org.jboss.as.controller.PathElement; +import org.jboss.dmr.ModelNode; +import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.ADDRESS; +import org.jboss.dmr.Property; + /** * This service keeps track of the entire Keycloak management model so as to provide - * adapter configuration to each deployment at deploy time. + * configuration to the Keycloak Server. * * @author Stan Silvert ssilvert@redhat.com (C) 2013 Red Hat Inc. */ @@ -27,6 +33,8 @@ public final class KeycloakAdapterConfigService { static final KeycloakAdapterConfigService INSTANCE = new KeycloakAdapterConfigService(); static final String DEPLOYMENT_NAME = "keycloak-server.war"; + + static ModelNode fullConfig = new ModelNode(); private String webContext; @@ -34,6 +42,127 @@ public final class KeycloakAdapterConfigService { private KeycloakAdapterConfigService() { } + void updateConfig(ModelNode operation, ModelNode config) { + PathAddress address = PathAddress.pathAddress(operation.get(ADDRESS)); + address = address.subAddress(1); // remove root (subsystem=keycloak-server) + + ModelNode newConfig = fullConfig.clone(); + ModelNode subNode = newConfig; + for (PathElement pathElement : address) { + subNode = subNode.get(pathElement.getKey(), pathElement.getValue()); + } + + subNode.set(config.clone()); + + // remove undefined properties + for (Property prop : subNode.asPropertyList()) { + if (!prop.getValue().isDefined()) { + subNode.remove(prop.getName()); + } + } + + fullConfig = newConfig; + } + + ModelNode getConfig() { + ModelNode copy = fullConfig.clone(); + //System.out.println("******** BEFORE *************"); + //System.out.println(copy); + //System.out.println("*****************************"); + copy.remove("web-context"); + massageScheduledTaskInterval(copy); + massageMasterRealm(copy); + massageTheme(copy); + massageSpis(copy); + //System.out.println("******** JSON *************"); + //System.out.println(copy.resolve().toJSONString(false)); + //System.out.println("**********************"); + return copy; + } + + // The "massage" methods rearrange the model so that everything will + // be where the Keycloak server's Config interface expects it to be. + + private void massageScheduledTaskInterval(ModelNode copy) { + if (!copy.hasDefined("scheduled-task-intervale")) return; + ModelNode taskInterval = copy.remove("scheduled-task-interval"); + copy.get("scheduled", "interval").set(taskInterval); + } + + private void massageMasterRealm(ModelNode copy) { + if (!copy.hasDefined("master-realm-name")) return; + ModelNode master = copy.remove("master-realm-name"); + copy.get("admin", "realm").set(master); + } + + private void massageTheme(ModelNode copy) { + if (!copy.hasDefined("theme")) return; + if (!copy.get("theme").hasDefined("defaults")) return; + + ModelNode themeDefaults = copy.get("theme", "defaults"); + copy.get("theme").set(themeDefaults); + + if (copy.has("theme", "dir")) { + ModelNode dir = copy.get("theme", "dir"); + copy.get("theme", "folder", "dir").set(dir); + copy.get("theme").remove("dir"); + } + + if (copy.has("theme", "modules")) { + ModelNode modules = copy.get("theme").remove("modules"); + copy.get("theme", "module", "modules").set(modules); + } + } + + private void massageSpis(ModelNode copy) { + if (!copy.hasDefined("spi")) return; + ModelNode spis = copy.remove("spi"); + + for (Property prop : spis.asPropertyList()) { + ModelNode spi = prop.getValue(); + + if (spi.has("provider")) { + massageProviders(spi); + } + + if (spi.has("default-provider")) { + ModelNode defaultProvider = spi.remove("default-provider"); + spi.get("provider").set(defaultProvider); + } + + copy.get(prop.getName()).set(spi); + } + } + + private void massageProviders(ModelNode spi) { + if (!spi.hasDefined("provider")) return; + ModelNode providers = spi.remove("provider"); + for (Property prop : providers.asPropertyList()) { + ModelNode provider = prop.getValue(); + if (provider.has("properties")) { + massageProviderProps(provider); + } + spi.get(prop.getName()).set(provider); + } + } + + private void massageProviderProps(ModelNode provider) { + if (!provider.hasDefined("properties")) return; + ModelNode providerProps = provider.remove("properties"); + for (Property prop : providerProps.asPropertyList()) { + ModelNode value = prop.getValue(); + if (isArray(value.asString().trim())) { + provider.get(prop.getName()).set(ModelNode.fromString(value.asString()).asList()); + } else { + provider.get(prop.getName()).set(value); + } + } + } + + private boolean isArray(String value) { + return value.startsWith("[") && value.endsWith("]"); + } + void setWebContext(String webContext) { this.webContext = webContext; } diff --git a/wildfly/server-subsystem/src/main/java/org/keycloak/subsystem/server/extension/KeycloakExtension.java b/wildfly/server-subsystem/src/main/java/org/keycloak/subsystem/server/extension/KeycloakExtension.java index c6a3cdb03a..bdfe47c8e7 100755 --- a/wildfly/server-subsystem/src/main/java/org/keycloak/subsystem/server/extension/KeycloakExtension.java +++ b/wildfly/server-subsystem/src/main/java/org/keycloak/subsystem/server/extension/KeycloakExtension.java @@ -26,6 +26,7 @@ import org.jboss.as.controller.descriptions.StandardResourceDescriptionResolver; import org.jboss.as.controller.parsing.ExtensionParsingContext; import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.SUBSYSTEM; +import org.jboss.as.controller.registry.ManagementResourceRegistration; import static org.keycloak.subsystem.server.logging.KeycloakLogger.ROOT_LOGGER; @@ -44,6 +45,10 @@ public class KeycloakExtension implements Extension { private static final ResourceDefinition KEYCLOAK_SUBSYSTEM_RESOURCE = new KeycloakSubsystemDefinition(); private static final KeycloakSubsystemParser PARSER = new KeycloakSubsystemParser(); private static final ModelVersion MGMT_API_VERSION = ModelVersion.create(1,1,0); + + static final ThemeResourceDefinition THEME_DEFINITION = new ThemeResourceDefinition(); + static final SpiResourceDefinition SPI_DEFINITION = new SpiResourceDefinition(); + static final ProviderResourceDefinition PROVIDER_DEFINITION = new ProviderResourceDefinition(); static StandardResourceDescriptionResolver getResourceDescriptionResolver(final String... keyPrefix) { StringBuilder prefix = new StringBuilder(SUBSYSTEM_NAME); @@ -69,7 +74,11 @@ public class KeycloakExtension implements Extension { ROOT_LOGGER.debug("Activating Keycloak Extension"); final SubsystemRegistration subsystem = context.registerSubsystem(SUBSYSTEM_NAME, MGMT_API_VERSION); - subsystem.registerSubsystemModel(KEYCLOAK_SUBSYSTEM_RESOURCE); + ManagementResourceRegistration subsystemRegistration = subsystem.registerSubsystemModel(KEYCLOAK_SUBSYSTEM_RESOURCE); + subsystemRegistration.registerSubModel(THEME_DEFINITION); + ManagementResourceRegistration spiRegistration = subsystemRegistration.registerSubModel(SPI_DEFINITION); + spiRegistration.registerSubModel(PROVIDER_DEFINITION); + subsystem.registerXMLElementWriter(PARSER); } } diff --git a/wildfly/server-subsystem/src/main/java/org/keycloak/subsystem/server/extension/KeycloakServerDeploymentProcessor.java b/wildfly/server-subsystem/src/main/java/org/keycloak/subsystem/server/extension/KeycloakServerDeploymentProcessor.java index f99cab3cd7..1bc2211e24 100755 --- a/wildfly/server-subsystem/src/main/java/org/keycloak/subsystem/server/extension/KeycloakServerDeploymentProcessor.java +++ b/wildfly/server-subsystem/src/main/java/org/keycloak/subsystem/server/extension/KeycloakServerDeploymentProcessor.java @@ -16,41 +16,78 @@ */ package org.keycloak.subsystem.server.extension; +import java.util.ArrayList; +import java.util.List; import org.jboss.as.ee.component.EEModuleDescription; import org.jboss.as.server.deployment.DeploymentPhaseContext; import org.jboss.as.server.deployment.DeploymentUnit; import org.jboss.as.server.deployment.DeploymentUnitProcessingException; import org.jboss.as.server.deployment.DeploymentUnitProcessor; +import org.jboss.as.web.common.WarMetaData; +import org.jboss.metadata.javaee.spec.ParamValueMetaData; +import org.jboss.metadata.web.jboss.JBossWebMetaData; import org.jboss.msc.service.ServiceName; import org.jboss.msc.service.ServiceTarget; /** - * DUP responsible for setting the web context of a Keycloak auth server. + * DUP responsible for setting the web context of a Keycloak auth server and + * passing the Keycloak configuration to the Keycloak server. * * @author Stan Silvert ssilvert@redhat.com (C) 2014 Red Hat Inc. */ public class KeycloakServerDeploymentProcessor implements DeploymentUnitProcessor { + // This param name is defined again in Keycloak Services class + // org.keycloak.services.resources.KeycloakApplication. We have this value in + // two places to avoid dependency between Keycloak Subsystem and Keyclaok Services module. + public static final String KEYCLOAK_CONFIG_PARAM_NAME = "org.keycloak.server-subsystem.Config"; + private static final ServiceName cacheContainerService = ServiceName.of("jboss", "infinispan", "keycloak"); - + @Override public void deploy(DeploymentPhaseContext phaseContext) throws DeploymentUnitProcessingException { DeploymentUnit deploymentUnit = phaseContext.getDeploymentUnit(); - KeycloakAdapterConfigService config = KeycloakAdapterConfigService.INSTANCE; + KeycloakAdapterConfigService configService = KeycloakAdapterConfigService.INSTANCE; String deploymentName = deploymentUnit.getName(); - if (!config.isKeycloakServerDeployment(deploymentName)) { + if (!configService.isKeycloakServerDeployment(deploymentName)) { return; } final EEModuleDescription description = deploymentUnit.getAttachment(org.jboss.as.ee.component.Attachments.EE_MODULE_DESCRIPTION); - String webContext = config.getWebContext(); + String webContext = configService.getWebContext(); if (webContext == null) { throw new DeploymentUnitProcessingException("Can't determine web context/module for Keycloak Server"); } description.setModuleName(webContext); addInfinispanCaches(phaseContext); + addConfiguration(deploymentUnit, configService); + } + + private void addConfiguration(DeploymentUnit deploymentUnit, KeycloakAdapterConfigService configService) throws DeploymentUnitProcessingException { + WarMetaData warMetaData = deploymentUnit.getAttachment(WarMetaData.ATTACHMENT_KEY); + if (warMetaData == null) { + throw new DeploymentUnitProcessingException("WarMetaData not found for KeycloakServer."); + } + + JBossWebMetaData webMetaData = warMetaData.getMergedJBossWebMetaData(); + if (webMetaData == null) { + webMetaData = new JBossWebMetaData(); + warMetaData.setMergedJBossWebMetaData(webMetaData); + } + + List contextParams = webMetaData.getContextParams(); + if (contextParams == null) { + contextParams = new ArrayList(); + } + + ParamValueMetaData param = new ParamValueMetaData(); + param.setParamName(KEYCLOAK_CONFIG_PARAM_NAME); + param.setParamValue(configService.getConfig().toString()); + contextParams.add(param); + + webMetaData.setContextParams(contextParams); } private void addInfinispanCaches(DeploymentPhaseContext context) { diff --git a/wildfly/server-subsystem/src/main/java/org/keycloak/subsystem/server/extension/KeycloakSubsystemAdd.java b/wildfly/server-subsystem/src/main/java/org/keycloak/subsystem/server/extension/KeycloakSubsystemAdd.java index 3c6dc34c80..6d75e28af6 100755 --- a/wildfly/server-subsystem/src/main/java/org/keycloak/subsystem/server/extension/KeycloakSubsystemAdd.java +++ b/wildfly/server-subsystem/src/main/java/org/keycloak/subsystem/server/extension/KeycloakSubsystemAdd.java @@ -62,9 +62,10 @@ class KeycloakSubsystemAdd extends AbstractBoottimeAddStepHandler { }, OperationContext.Stage.RUNTIME); } - protected void populateModel(final OperationContext context, final ModelNode operation, final Resource resource) throws OperationFailedException { + @Override + protected void populateModel(final OperationContext context, final ModelNode operation, final Resource resource) throws OperationFailedException { ModelNode model = resource.getModel(); - + // set attribute values from parsed model for (AttributeDefinition attrDef : ALL_ATTRIBUTES) { attrDef.validateAndSet(operation, model); @@ -89,5 +90,7 @@ class KeycloakSubsystemAdd extends AbstractBoottimeAddStepHandler { ServerUtil serverUtil = new ServerUtil(operation); serverUtil.addStepToUploadServerWar(context); KeycloakAdapterConfigService.INSTANCE.setWebContext(webContext); + + KeycloakAdapterConfigService.INSTANCE.updateConfig(operation, model); } } diff --git a/wildfly/server-subsystem/src/main/java/org/keycloak/subsystem/server/extension/KeycloakSubsystemDefinition.java b/wildfly/server-subsystem/src/main/java/org/keycloak/subsystem/server/extension/KeycloakSubsystemDefinition.java index 6fcee7b08a..3dc4917324 100644 --- a/wildfly/server-subsystem/src/main/java/org/keycloak/subsystem/server/extension/KeycloakSubsystemDefinition.java +++ b/wildfly/server-subsystem/src/main/java/org/keycloak/subsystem/server/extension/KeycloakSubsystemDefinition.java @@ -29,6 +29,8 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; +import org.jboss.as.controller.StringListAttributeDefinition; +import org.keycloak.subsystem.server.attributes.ProvidersListAttributeBuilder; /** * Definition of subsystem=keycloak-server. @@ -44,15 +46,34 @@ public class KeycloakSubsystemDefinition extends SimpleResourceDefinition { .setRestartAllServices() .build(); - static final List ALL_ATTRIBUTES = new ArrayList(); + static final StringListAttributeDefinition PROVIDERS = new ProvidersListAttributeBuilder().build(); + + static final SimpleAttributeDefinition MASTER_REALM_NAME = + new SimpleAttributeDefinitionBuilder("master-realm-name", ModelType.STRING, true) + .setAllowExpression(true) + .setDefaultValue(new ModelNode("master")) + .setRestartAllServices() + .build(); + + static final SimpleAttributeDefinition SCHEDULED_TASK_INTERVAL = + new SimpleAttributeDefinitionBuilder("scheduled-task-interval", ModelType.LONG, true) + .setAllowExpression(true) + .setDefaultValue(new ModelNode("900")) + .setRestartAllServices() + .build(); + + static final List ALL_ATTRIBUTES = new ArrayList(); static { ALL_ATTRIBUTES.add(WEB_CONTEXT); + ALL_ATTRIBUTES.add(PROVIDERS); + ALL_ATTRIBUTES.add(MASTER_REALM_NAME); + ALL_ATTRIBUTES.add(SCHEDULED_TASK_INTERVAL); } - private static final Map DEFINITION_LOOKUP = new HashMap(); + private static final Map DEFINITION_LOOKUP = new HashMap(); static { - for (SimpleAttributeDefinition def : ALL_ATTRIBUTES) { + for (AttributeDefinition def : ALL_ATTRIBUTES) { DEFINITION_LOOKUP.put(def.getXmlName(), def); } } @@ -71,6 +92,7 @@ public class KeycloakSubsystemDefinition extends SimpleResourceDefinition { public void registerOperations(ManagementResourceRegistration resourceRegistration) { super.registerOperations(resourceRegistration); resourceRegistration.registerOperationHandler(GenericSubsystemDescribeHandler.DEFINITION, GenericSubsystemDescribeHandler.INSTANCE); + resourceRegistration.registerOperationHandler(MigrateJsonOperation.DEFINITION, new MigrateJsonOperation()); } @Override @@ -81,7 +103,7 @@ public class KeycloakSubsystemDefinition extends SimpleResourceDefinition { } } - public static SimpleAttributeDefinition lookup(String name) { + public static AttributeDefinition lookup(String name) { return DEFINITION_LOOKUP.get(name); } } diff --git a/wildfly/server-subsystem/src/main/java/org/keycloak/subsystem/server/extension/KeycloakSubsystemParser.java b/wildfly/server-subsystem/src/main/java/org/keycloak/subsystem/server/extension/KeycloakSubsystemParser.java index 6db6e5701e..0f4ea8c0aa 100755 --- a/wildfly/server-subsystem/src/main/java/org/keycloak/subsystem/server/extension/KeycloakSubsystemParser.java +++ b/wildfly/server-subsystem/src/main/java/org/keycloak/subsystem/server/extension/KeycloakSubsystemParser.java @@ -29,9 +29,26 @@ import org.jboss.staxmapper.XMLExtendedStreamWriter; import javax.xml.stream.XMLStreamConstants; import javax.xml.stream.XMLStreamException; import java.util.List; +import static javax.xml.stream.XMLStreamConstants.END_ELEMENT; +import org.jboss.as.controller.AttributeDefinition; +import org.jboss.as.controller.PathElement; +import org.jboss.as.controller.PropertiesAttributeDefinition; +import org.jboss.as.controller.SimpleAttributeDefinition; +import org.jboss.as.controller.descriptions.ModelDescriptionConstants; +import org.jboss.dmr.Property; import static org.keycloak.subsystem.server.extension.KeycloakExtension.PATH_SUBSYSTEM; import static org.keycloak.subsystem.server.extension.KeycloakSubsystemDefinition.WEB_CONTEXT; +import static org.keycloak.subsystem.server.extension.KeycloakSubsystemDefinition.PROVIDERS; +import static org.keycloak.subsystem.server.extension.KeycloakSubsystemDefinition.MASTER_REALM_NAME; +import static org.keycloak.subsystem.server.extension.KeycloakSubsystemDefinition.SCHEDULED_TASK_INTERVAL; + +import static org.keycloak.subsystem.server.extension.ThemeResourceDefinition.MODULES; + +import static org.keycloak.subsystem.server.extension.SpiResourceDefinition.DEFAULT_PROVIDER; + +import static org.keycloak.subsystem.server.extension.ProviderResourceDefinition.ENABLED; +import static org.keycloak.subsystem.server.extension.ProviderResourceDefinition.PROPERTIES; /** * The subsystem parser, which uses stax to read and write to and from xml @@ -51,12 +68,116 @@ class KeycloakSubsystemParser implements XMLStreamConstants, XMLElementReader
  • list, final XMLExtendedStreamReader reader) throws XMLStreamException { + ModelNode addThemeDefaults = new ModelNode(); + addThemeDefaults.get(ModelDescriptionConstants.OP).set(ModelDescriptionConstants.ADD); + PathAddress addr = PathAddress.pathAddress(PathElement.pathElement(ModelDescriptionConstants.SUBSYSTEM, KeycloakExtension.SUBSYSTEM_NAME), + PathElement.pathElement(ThemeResourceDefinition.TAG_NAME, ThemeResourceDefinition.RESOURCE_NAME)); + addThemeDefaults.get(ModelDescriptionConstants.OP_ADDR).set(addr.toModelNode()); + list.add(addThemeDefaults); + + while (reader.hasNext() && nextTag(reader) != END_ELEMENT) { + String tagName = reader.getLocalName(); + if (MODULES.getName().equals(tagName)) { + readModules(reader, addThemeDefaults); + continue; + } + + SimpleAttributeDefinition def = KeycloakExtension.THEME_DEFINITION.lookup(tagName); + if (def == null) throw new XMLStreamException("Unknown theme tag " + tagName); + def.parseAndSetParameter(reader.getElementText(), addThemeDefaults, reader); + } + } + + private void readModules(final XMLExtendedStreamReader reader, ModelNode addThemeDefaults) throws XMLStreamException { + while (reader.hasNext() && nextTag(reader) != END_ELEMENT) { + MODULES.parseAndAddParameterElement(reader.getElementText(),addThemeDefaults, reader); + } + } + + private void readSpi(final List list, final XMLExtendedStreamReader reader) throws XMLStreamException { + String spiName = ParseUtils.requireAttributes(reader, "name")[0]; + ModelNode addSpi = new ModelNode(); + addSpi.get(ModelDescriptionConstants.OP).set(ModelDescriptionConstants.ADD); + PathAddress addr = PathAddress.pathAddress(PathElement.pathElement(ModelDescriptionConstants.SUBSYSTEM, KeycloakExtension.SUBSYSTEM_NAME), + PathElement.pathElement(SpiResourceDefinition.TAG_NAME, spiName)); + addSpi.get(ModelDescriptionConstants.OP_ADDR).set(addr.toModelNode()); + list.add(addSpi); + + while (reader.hasNext() && nextTag(reader) != END_ELEMENT) { + if (reader.getLocalName().equals(DEFAULT_PROVIDER.getXmlName())) { + DEFAULT_PROVIDER.parseAndSetParameter(reader.getElementText(), addSpi, reader); + } else if (reader.getLocalName().equals(ProviderResourceDefinition.TAG_NAME)) { + readProvider(list, spiName, reader); + } + } + } + + private void readProvider(final List list, String spiName, final XMLExtendedStreamReader reader) throws XMLStreamException { + String[] attributes = ParseUtils.requireAttributes(reader, "name", ENABLED.getXmlName()); + String providerName = attributes[0]; + String enabled = attributes[1]; + + ModelNode addProvider = new ModelNode(); + addProvider.get(ModelDescriptionConstants.OP).set(ModelDescriptionConstants.ADD); + PathAddress addr = PathAddress.pathAddress(PathElement.pathElement(ModelDescriptionConstants.SUBSYSTEM, KeycloakExtension.SUBSYSTEM_NAME), + PathElement.pathElement(SpiResourceDefinition.TAG_NAME, spiName), + PathElement.pathElement(ProviderResourceDefinition.TAG_NAME, providerName)); + addProvider.get(ModelDescriptionConstants.OP_ADDR).set(addr.toModelNode()); + addProvider.get(ENABLED.getName()).set(Boolean.valueOf(enabled)); + list.add(addProvider); + + while (nextTag(reader) != END_ELEMENT) { + if (reader.getLocalName().equals(PROPERTIES.getXmlName())) { + readProperties(PROPERTIES, addProvider, reader); + } + } + } + + private void readProperties(final PropertiesAttributeDefinition attrDef, ModelNode addOp, final XMLExtendedStreamReader reader) throws XMLStreamException { + while (nextTag(reader) != END_ELEMENT) { + int attrCount = reader.getAttributeCount(); + if (attrCount != 2) throw new XMLStreamException("Property must have only two attributes"); + String name = ""; + String value = ""; + for (int i=0 ; i < 2; i++) { + String attrName = reader.getAttributeLocalName(i); + String attrValue = reader.getAttributeValue(i); + if (attrName.equals("name")) { + name = attrValue; + } else if (attrName.equals("value")) { + value = attrValue; + } else { + throw new XMLStreamException("Property can only have attributes named 'name' and 'value'"); + } + } + attrDef.parseAndAddParameterElement(name, value, addOp, reader); + nextTag(reader); + } + } + // used for debugging private int nextTag(XMLExtendedStreamReader reader) throws XMLStreamException { return reader.nextTag(); @@ -69,9 +190,64 @@ class KeycloakSubsystemParser implements XMLStreamConstants, XMLElementReader
  • definitions) { + public KeycloakSubsystemWriteAttributeHandler(List definitions) { this(definitions.toArray(new AttributeDefinition[definitions.size()])); } @@ -59,7 +58,7 @@ public class KeycloakSubsystemWriteAttributeHandler extends ModelOnlyWriteAttrib } private boolean attribNotChanging(String attributeName, ModelNode newValue, ModelNode oldValue) { - SimpleAttributeDefinition attribDef = KeycloakSubsystemDefinition.lookup(attributeName); + AttributeDefinition attribDef = KeycloakSubsystemDefinition.lookup(attributeName); if (!oldValue.isDefined()) { oldValue = attribDef.getDefaultValue(); } diff --git a/wildfly/server-subsystem/src/main/java/org/keycloak/subsystem/server/extension/MigrateJsonOperation.java b/wildfly/server-subsystem/src/main/java/org/keycloak/subsystem/server/extension/MigrateJsonOperation.java new file mode 100644 index 0000000000..92c229ddc5 --- /dev/null +++ b/wildfly/server-subsystem/src/main/java/org/keycloak/subsystem/server/extension/MigrateJsonOperation.java @@ -0,0 +1,97 @@ +/* + * 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.subsystem.server.extension; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import org.jboss.as.controller.AttributeDefinition; +import org.jboss.as.controller.OperationContext; +import org.jboss.as.controller.OperationDefinition; +import org.jboss.as.controller.OperationFailedException; +import org.jboss.as.controller.OperationStepHandler; +import org.jboss.as.controller.PathAddress; +import org.jboss.as.controller.SimpleAttributeDefinitionBuilder; +import org.jboss.as.controller.SimpleOperationDefinitionBuilder; +import org.jboss.dmr.ModelNode; +import org.jboss.dmr.ModelType; + +import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.ADDRESS; +import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.OP; + +/** + * This operation provides a migration path from keycloak-server.json to + * standalone.xml or domain.xml. + * + * @author Stan Silvert ssilvert@redhat.com (C) 2016 Red Hat Inc. + */ +public class MigrateJsonOperation implements OperationStepHandler { + public static final String OPERATION_NAME = "migrate-json"; + + private static final String CONFIG_DIR = System.getProperty("jboss.server.config.dir"); + private static final Path DEFAULT_CONFIG_FILE = Paths.get(CONFIG_DIR, "keycloak-server.json"); + + private static final AttributeDefinition FILE_ATTRIBUTE = SimpleAttributeDefinitionBuilder.create("file", ModelType.BYTES, true).build(); + public static final OperationDefinition DEFINITION = new SimpleOperationDefinitionBuilder(OPERATION_NAME, KeycloakExtension.getResourceDescriptionResolver()) + .setRuntimeOnly() + .setReadOnly() + .setReplyType(ModelType.STRING) + .setParameters(FILE_ATTRIBUTE) + .build(); + + private String localConfig() throws IOException { + if (Files.notExists(DEFAULT_CONFIG_FILE)) return null; + return new String(Files.readAllBytes(DEFAULT_CONFIG_FILE)); + } + + private String readConfig(ModelNode operation) throws IOException { + ModelNode file = operation.get(FILE_ATTRIBUTE.getName()); + if (file.isDefined() && file.asBytes().length > 0) { + return new String(file.asBytes()); + } + + String localConfig = localConfig(); + if (localConfig != null) return localConfig; + + throw new IOException("Can not find json file to migrate"); + } + + @Override + public void execute(OperationContext context, ModelNode operation) throws OperationFailedException { + List ops = null; + try { + PathAddress currentAddr = context.getCurrentAddress(); + ops = JsonConfigConverter.convertJsonConfig(readConfig(operation), currentAddr); + } catch (IOException ioe) { + throw new OperationFailedException(ioe); + } + + for (ModelNode op : ops) { + PathAddress addr = PathAddress.pathAddress(op.get(ADDRESS)); + String opName = op.get(OP).asString(); + context.addStep(op, + context.getRootResourceRegistration().getOperationHandler(addr, opName), + OperationContext.Stage.MODEL); + } + + context.completeStep(OperationContext.RollbackHandler.NOOP_ROLLBACK_HANDLER); + } + +} diff --git a/wildfly/server-subsystem/src/main/java/org/keycloak/subsystem/server/extension/ProviderResourceAddHandler.java b/wildfly/server-subsystem/src/main/java/org/keycloak/subsystem/server/extension/ProviderResourceAddHandler.java new file mode 100644 index 0000000000..7db883c053 --- /dev/null +++ b/wildfly/server-subsystem/src/main/java/org/keycloak/subsystem/server/extension/ProviderResourceAddHandler.java @@ -0,0 +1,48 @@ +/* + * 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.subsystem.server.extension; + +import org.jboss.as.controller.AbstractAddStepHandler; +import org.jboss.as.controller.OperationFailedException; +import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.ADD; +import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.OP; +import org.jboss.dmr.ModelNode; + +/** + * + * @author Stan Silvert ssilvert@redhat.com (C) 2016 Red Hat Inc. + */ +public class ProviderResourceAddHandler extends AbstractAddStepHandler { + + public static ProviderResourceAddHandler INSTANCE = new ProviderResourceAddHandler(); + + private ProviderResourceAddHandler() { + } + + @Override + protected void populateModel(ModelNode operation, ModelNode model) throws OperationFailedException { + // TODO: localize exception. get id number + if (!operation.get(OP).asString().equals(ADD)) { + throw new OperationFailedException("Unexpected operation for add SPI. operation=" + operation.toString()); + } + + ProviderResourceDefinition.ENABLED.validateAndSet(operation, model); + ProviderResourceDefinition.PROPERTIES.validateAndSet(operation, model); + + KeycloakAdapterConfigService.INSTANCE.updateConfig(operation, model); + } +} diff --git a/wildfly/server-subsystem/src/main/java/org/keycloak/subsystem/server/extension/ProviderResourceDefinition.java b/wildfly/server-subsystem/src/main/java/org/keycloak/subsystem/server/extension/ProviderResourceDefinition.java new file mode 100644 index 0000000000..68ba9bb327 --- /dev/null +++ b/wildfly/server-subsystem/src/main/java/org/keycloak/subsystem/server/extension/ProviderResourceDefinition.java @@ -0,0 +1,70 @@ +/* + * 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.subsystem.server.extension; + +import org.jboss.as.controller.PathElement; +import org.jboss.as.controller.PropertiesAttributeDefinition; +import org.jboss.as.controller.ReloadRequiredWriteAttributeHandler; +import org.jboss.as.controller.SimpleAttributeDefinition; +import org.jboss.as.controller.SimpleAttributeDefinitionBuilder; +import org.jboss.as.controller.SimpleResourceDefinition; +import org.jboss.as.controller.registry.ManagementResourceRegistration; +import org.jboss.dmr.ModelNode; +import org.jboss.dmr.ModelType; + +/** + * + * @author Stan Silvert ssilvert@redhat.com (C) 2016 Red Hat Inc. + */ +public class ProviderResourceDefinition extends SimpleResourceDefinition { + + public static final String TAG_NAME = "provider"; + + protected static final SimpleAttributeDefinition ENABLED = + new SimpleAttributeDefinitionBuilder("enabled", ModelType.BOOLEAN, true) + .setAllowExpression(true) + .setDefaultValue(new ModelNode(true)) + .setAllowNull(false) + .setRestartAllServices() + .build(); + + static final PropertiesAttributeDefinition PROPERTIES = + new PropertiesAttributeDefinition.Builder("properties", true) + .setRestartAllServices() + .setAllowExpression(true) + .build(); + + protected static final ReloadRequiredWriteAttributeHandler WRITE_ATTR_HANDLER = new ReloadRequiredWriteAttributeHandler(ENABLED); + + protected ProviderResourceDefinition() { + super(PathElement.pathElement(TAG_NAME), + KeycloakExtension.getResourceDescriptionResolver(TAG_NAME), + ProviderResourceAddHandler.INSTANCE, + ProviderResourceRemoveHandler.INSTANCE + ); + } + + @Override + public void registerAttributes(ManagementResourceRegistration resourceRegistration) { + super.registerAttributes(resourceRegistration); + resourceRegistration.registerReadWriteAttribute(ENABLED, null, WRITE_ATTR_HANDLER); + resourceRegistration.registerReadWriteAttribute(PROPERTIES, null, WRITE_ATTR_HANDLER); + } + + +} diff --git a/wildfly/server-subsystem/src/main/java/org/keycloak/subsystem/server/extension/ProviderResourceRemoveHandler.java b/wildfly/server-subsystem/src/main/java/org/keycloak/subsystem/server/extension/ProviderResourceRemoveHandler.java new file mode 100644 index 0000000000..21d8217410 --- /dev/null +++ b/wildfly/server-subsystem/src/main/java/org/keycloak/subsystem/server/extension/ProviderResourceRemoveHandler.java @@ -0,0 +1,32 @@ +/* + * 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.subsystem.server.extension; + +import org.jboss.as.controller.AbstractRemoveStepHandler; + +/** + * + * @author Stan Silvert ssilvert@redhat.com (C) 2016 Red Hat Inc. + */ +public class ProviderResourceRemoveHandler extends AbstractRemoveStepHandler { + + public static ProviderResourceRemoveHandler INSTANCE = new ProviderResourceRemoveHandler(); + + private ProviderResourceRemoveHandler() {} + +} diff --git a/wildfly/server-subsystem/src/main/java/org/keycloak/subsystem/server/extension/SpiResourceAddHandler.java b/wildfly/server-subsystem/src/main/java/org/keycloak/subsystem/server/extension/SpiResourceAddHandler.java new file mode 100644 index 0000000000..3d417917e8 --- /dev/null +++ b/wildfly/server-subsystem/src/main/java/org/keycloak/subsystem/server/extension/SpiResourceAddHandler.java @@ -0,0 +1,47 @@ +/* + * 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.subsystem.server.extension; + +import org.jboss.as.controller.AbstractAddStepHandler; +import org.jboss.as.controller.OperationFailedException; +import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.ADD; +import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.OP; +import org.jboss.dmr.ModelNode; + +/** + * + * @author Stan Silvert ssilvert@redhat.com (C) 2016 Red Hat Inc. + */ +public class SpiResourceAddHandler extends AbstractAddStepHandler { + + public static SpiResourceAddHandler INSTANCE = new SpiResourceAddHandler(); + + private SpiResourceAddHandler() {} + + @Override + protected void populateModel(ModelNode operation, ModelNode model) throws OperationFailedException { + // TODO: localize exception. get id number + if (!operation.get(OP).asString().equals(ADD)) { + throw new OperationFailedException("Unexpected operation for add SPI. operation=" + operation.toString()); + } + + SpiResourceDefinition.DEFAULT_PROVIDER.validateAndSet(operation, model); + + KeycloakAdapterConfigService.INSTANCE.updateConfig(operation, model); + } +} diff --git a/wildfly/server-subsystem/src/main/java/org/keycloak/subsystem/server/extension/SpiResourceDefinition.java b/wildfly/server-subsystem/src/main/java/org/keycloak/subsystem/server/extension/SpiResourceDefinition.java new file mode 100644 index 0000000000..db15ba4911 --- /dev/null +++ b/wildfly/server-subsystem/src/main/java/org/keycloak/subsystem/server/extension/SpiResourceDefinition.java @@ -0,0 +1,59 @@ +/* + * 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.subsystem.server.extension; + +import org.jboss.as.controller.PathElement; +import org.jboss.as.controller.ReloadRequiredWriteAttributeHandler; +import org.jboss.as.controller.SimpleAttributeDefinition; +import org.jboss.as.controller.SimpleAttributeDefinitionBuilder; +import org.jboss.as.controller.SimpleResourceDefinition; +import org.jboss.as.controller.registry.ManagementResourceRegistration; +import org.jboss.dmr.ModelType; + +/** + * + * @author Stan Silvert ssilvert@redhat.com (C) 2016 Red Hat Inc. + */ +public class SpiResourceDefinition extends SimpleResourceDefinition { + + public static final String TAG_NAME = "spi"; + + protected static final SimpleAttributeDefinition DEFAULT_PROVIDER = + new SimpleAttributeDefinitionBuilder("default-provider", ModelType.STRING, true) + .setAllowExpression(true) + .setRestartAllServices() + .build(); + + protected static final ReloadRequiredWriteAttributeHandler WRITE_ATTR_HANDLER = new ReloadRequiredWriteAttributeHandler(DEFAULT_PROVIDER); + + protected SpiResourceDefinition() { + super(PathElement.pathElement(TAG_NAME), + KeycloakExtension.getResourceDescriptionResolver(TAG_NAME), + SpiResourceAddHandler.INSTANCE, + SpiResourceRemoveHandler.INSTANCE + ); + } + + @Override + public void registerAttributes(ManagementResourceRegistration resourceRegistration) { + super.registerAttributes(resourceRegistration); + resourceRegistration.registerReadWriteAttribute(DEFAULT_PROVIDER, null, WRITE_ATTR_HANDLER); + } + + +} diff --git a/wildfly/server-subsystem/src/main/java/org/keycloak/subsystem/server/extension/SpiResourceRemoveHandler.java b/wildfly/server-subsystem/src/main/java/org/keycloak/subsystem/server/extension/SpiResourceRemoveHandler.java new file mode 100644 index 0000000000..468a40a6a6 --- /dev/null +++ b/wildfly/server-subsystem/src/main/java/org/keycloak/subsystem/server/extension/SpiResourceRemoveHandler.java @@ -0,0 +1,32 @@ +/* + * 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.subsystem.server.extension; + +import org.jboss.as.controller.AbstractRemoveStepHandler; + +/** + * + * @author Stan Silvert ssilvert@redhat.com (C) 2016 Red Hat Inc. + */ +public class SpiResourceRemoveHandler extends AbstractRemoveStepHandler { + + public static SpiResourceRemoveHandler INSTANCE = new SpiResourceRemoveHandler(); + + private SpiResourceRemoveHandler() {} + +} diff --git a/wildfly/server-subsystem/src/main/java/org/keycloak/subsystem/server/extension/ThemeResourceAddHandler.java b/wildfly/server-subsystem/src/main/java/org/keycloak/subsystem/server/extension/ThemeResourceAddHandler.java new file mode 100644 index 0000000000..b955101b47 --- /dev/null +++ b/wildfly/server-subsystem/src/main/java/org/keycloak/subsystem/server/extension/ThemeResourceAddHandler.java @@ -0,0 +1,60 @@ +/* + * 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.subsystem.server.extension; + +import org.jboss.as.controller.AbstractAddStepHandler; +import org.jboss.as.controller.AttributeDefinition; +import org.jboss.as.controller.OperationFailedException; +import org.jboss.as.controller.PathAddress; +import org.jboss.as.controller.PathElement; +import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.ADD; +import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.ADDRESS; +import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.OP; +import org.jboss.dmr.ModelNode; +import static org.keycloak.subsystem.server.extension.ThemeResourceDefinition.ALL_ATTRIBUTES; + +/** + * + * @author Stan Silvert ssilvert@redhat.com (C) 2016 Red Hat Inc. + */ +public class ThemeResourceAddHandler extends AbstractAddStepHandler { + + public static ThemeResourceAddHandler INSTANCE = new ThemeResourceAddHandler(); + + private ThemeResourceAddHandler() {} + + @Override + protected void populateModel(ModelNode operation, ModelNode model) throws OperationFailedException { + // TODO: localize exception. get id number + if (!operation.get(OP).asString().equals(ADD)) { + throw new OperationFailedException("Unexpected operation for add Theme. operation=" + operation.toString()); + } + + PathAddress address = PathAddress.pathAddress(operation.get(ADDRESS)); + PathElement last = address.getLastElement(); + if (!last.getValue().equals(ThemeResourceDefinition.RESOURCE_NAME)) { + throw new OperationFailedException("Theme resource with name " + last.getValue() + " not allowed."); + } + + for (AttributeDefinition def : ALL_ATTRIBUTES) { + def.validateAndSet(operation, model); + } + + KeycloakAdapterConfigService.INSTANCE.updateConfig(operation, model); + } +} diff --git a/wildfly/server-subsystem/src/main/java/org/keycloak/subsystem/server/extension/ThemeResourceDefinition.java b/wildfly/server-subsystem/src/main/java/org/keycloak/subsystem/server/extension/ThemeResourceDefinition.java new file mode 100644 index 0000000000..94dca67eee --- /dev/null +++ b/wildfly/server-subsystem/src/main/java/org/keycloak/subsystem/server/extension/ThemeResourceDefinition.java @@ -0,0 +1,135 @@ +/* + * 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.subsystem.server.extension; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.jboss.as.controller.AttributeDefinition; +import org.jboss.as.controller.PathElement; +import org.jboss.as.controller.ReloadRequiredWriteAttributeHandler; +import org.jboss.as.controller.SimpleAttributeDefinition; +import org.jboss.as.controller.SimpleAttributeDefinitionBuilder; +import org.jboss.as.controller.SimpleResourceDefinition; +import org.jboss.as.controller.StringListAttributeDefinition; +import org.jboss.as.controller.registry.ManagementResourceRegistration; +import org.jboss.dmr.ModelNode; +import org.jboss.dmr.ModelType; +import org.keycloak.subsystem.server.attributes.ModulesListAttributeBuilder; + +/** + * + * @author Stan Silvert ssilvert@redhat.com (C) 2016 Red Hat Inc. + */ +public class ThemeResourceDefinition extends SimpleResourceDefinition { + + public static final String TAG_NAME = "theme"; + + // This is the internal name of the singleton resource + public static final String RESOURCE_NAME = "defaults"; + + // NOTE: All attributes must be SimpleAttributeDefinition. If that needs to + // change then refactor starting with lookup() method below. + static final SimpleAttributeDefinition STATIC_MAX_AGE = + new SimpleAttributeDefinitionBuilder("staticMaxAge", ModelType.LONG, true) + .setAllowExpression(true) + .setDefaultValue(new ModelNode("2592000")) + .setRestartAllServices() + .build(); + + static final SimpleAttributeDefinition CACHE_THEMES = + new SimpleAttributeDefinitionBuilder("cacheThemes", ModelType.BOOLEAN, true) + .setAllowExpression(true) + .setDefaultValue(new ModelNode(true)) + .setAllowNull(false) + .setRestartAllServices() + .build(); + + static final SimpleAttributeDefinition CACHE_TEMPLATES = + new SimpleAttributeDefinitionBuilder("cacheTemplates", ModelType.BOOLEAN, true) + .setAllowExpression(true) + .setDefaultValue(new ModelNode(true)) + .setAllowNull(false) + .setRestartAllServices() + .build(); + + static final SimpleAttributeDefinition WELCOME_THEME = + new SimpleAttributeDefinitionBuilder("welcomeTheme", ModelType.STRING, true) + .setAllowExpression(true) + .setRestartAllServices() + .build(); + + static final SimpleAttributeDefinition DEFAULT = + new SimpleAttributeDefinitionBuilder("default", ModelType.STRING, true) + .setAllowExpression(true) + .setRestartAllServices() + .build(); + + static final SimpleAttributeDefinition DIR = + new SimpleAttributeDefinitionBuilder("dir", ModelType.STRING, true) + .setAllowExpression(true) + .setDefaultValue(new ModelNode("${jboss.home.dir}/themes")) + .setRestartAllServices() + .build(); + + static final StringListAttributeDefinition MODULES = new ModulesListAttributeBuilder().build(); + + static final List ALL_ATTRIBUTES = new ArrayList<>(); + + static { + ALL_ATTRIBUTES.add(STATIC_MAX_AGE); + ALL_ATTRIBUTES.add(CACHE_THEMES); + ALL_ATTRIBUTES.add(CACHE_TEMPLATES); + ALL_ATTRIBUTES.add(WELCOME_THEME); + ALL_ATTRIBUTES.add(DEFAULT); + ALL_ATTRIBUTES.add(DIR); + ALL_ATTRIBUTES.add(MODULES); + } + + private static final Map DEFINITION_LOOKUP = new HashMap<>(); + static { + for (AttributeDefinition def : ALL_ATTRIBUTES) { + DEFINITION_LOOKUP.put(def.getXmlName(), def); + } + } + + protected static final ReloadRequiredWriteAttributeHandler WRITE_ATTR_HANDLER = new ReloadRequiredWriteAttributeHandler(ALL_ATTRIBUTES); + + protected ThemeResourceDefinition() { + super(PathElement.pathElement(TAG_NAME), + KeycloakExtension.getResourceDescriptionResolver(TAG_NAME), + ThemeResourceAddHandler.INSTANCE, + ThemeResourceRemoveHandler.INSTANCE + ); + } + + @Override + public void registerAttributes(ManagementResourceRegistration resourceRegistration) { + super.registerAttributes(resourceRegistration); + + for (AttributeDefinition def : ALL_ATTRIBUTES) { + resourceRegistration.registerReadWriteAttribute(def, null, WRITE_ATTR_HANDLER); + } + } + + public static SimpleAttributeDefinition lookup(String name) { + return (SimpleAttributeDefinition)DEFINITION_LOOKUP.get(name); + } + +} diff --git a/wildfly/server-subsystem/src/main/java/org/keycloak/subsystem/server/extension/ThemeResourceRemoveHandler.java b/wildfly/server-subsystem/src/main/java/org/keycloak/subsystem/server/extension/ThemeResourceRemoveHandler.java new file mode 100644 index 0000000000..797c564d8a --- /dev/null +++ b/wildfly/server-subsystem/src/main/java/org/keycloak/subsystem/server/extension/ThemeResourceRemoveHandler.java @@ -0,0 +1,32 @@ +/* + * 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.subsystem.server.extension; + +import org.jboss.as.controller.AbstractRemoveStepHandler; + +/** + * + * @author Stan Silvert ssilvert@redhat.com (C) 2016 Red Hat Inc. + */ +public class ThemeResourceRemoveHandler extends AbstractRemoveStepHandler { + + public static ThemeResourceRemoveHandler INSTANCE = new ThemeResourceRemoveHandler(); + + private ThemeResourceRemoveHandler() {} + +} diff --git a/wildfly/server-subsystem/src/main/resources/cli/default-keycloak-subsys-config.cli b/wildfly/server-subsystem/src/main/resources/cli/default-keycloak-subsys-config.cli new file mode 100644 index 0000000000..6adef01e49 --- /dev/null +++ b/wildfly/server-subsystem/src/main/resources/cli/default-keycloak-subsys-config.cli @@ -0,0 +1,21 @@ +/subsystem=keycloak-server:add(web-context=auth,master-realm-name=master,scheduled-task-interval=900,providers=[classpath:${jboss.home.dir}/providers/*]) +/subsystem=keycloak-server/theme=defaults/:add(dir=${jboss.home.dir}/themes,staticMaxAge=2592000,cacheTemplates=true,cacheThemes=true) +/subsystem=keycloak-server/spi=eventsStore/:add(default-provider=jpa) +/subsystem=keycloak-server/spi=eventsStore/provider=jpa/:add(properties={exclude-events => "[\"REFRESH_TOKEN\"]"},enabled=true) +/subsystem=keycloak-server/spi=realm/:add(default-provider=jpa) +/subsystem=keycloak-server/spi=user/:add(default-provider=jpa) +/subsystem=keycloak-server/spi=userCache/:add +/subsystem=keycloak-server/spi=userCache/provider=default/:add(enabled=true) +/subsystem=keycloak-server/spi=userSessionPersister/:add(default-provider=jpa) +/subsystem=keycloak-server/spi=authorizationPersister/:add(default-provider=jpa) +/subsystem=keycloak-server/spi=timer/:add(default-provider=basic) +/subsystem=keycloak-server/spi=connectionsHttpClient/:add +/subsystem=keycloak-server/spi=connectionsHttpClient/provider=default/:add(enabled=true) +/subsystem=keycloak-server/spi=connectionsJpa/:add +/subsystem=keycloak-server/spi=connectionsJpa/provider=default/:add(properties={dataSource => "java:jboss/datasources/KeycloakDS",initializeEmpty => "true",migrationStrategy => "update",migrationExport => "${jboss.home.dir}/keycloak-database-update.sql"},enabled=true) +/subsystem=keycloak-server/spi=realmCache/:add +/subsystem=keycloak-server/spi=realmCache/provider=default/:add(enabled=true) +/subsystem=keycloak-server/spi=connectionsInfinispan/:add(default-provider=default) +/subsystem=keycloak-server/spi=connectionsInfinispan/provider=default/:add(properties={cacheContainer => "java:comp/env/infinispan/Keycloak"},enabled=true) +/subsystem=keycloak-server/spi=jta-lookup/:add(default-provider=${keycloak.jta.lookup.provider:jboss}) +/subsystem=keycloak-server/spi=jta-lookup/provider=jboss/:add(enabled=true) \ No newline at end of file diff --git a/wildfly/server-subsystem/src/main/resources/default-config/keycloak-server-default-config.xml b/wildfly/server-subsystem/src/main/resources/default-config/keycloak-server-default-config.xml new file mode 100644 index 0000000000..e5206116c6 --- /dev/null +++ b/wildfly/server-subsystem/src/main/resources/default-config/keycloak-server-default-config.xml @@ -0,0 +1 @@ +${keycloak.server.subsys.default.config} \ No newline at end of file diff --git a/wildfly/server-subsystem/src/main/resources/org/keycloak/subsystem/server/extension/LocalDescriptions.properties b/wildfly/server-subsystem/src/main/resources/org/keycloak/subsystem/server/extension/LocalDescriptions.properties index bf29d067e3..8d5c5a780d 100755 --- a/wildfly/server-subsystem/src/main/resources/org/keycloak/subsystem/server/extension/LocalDescriptions.properties +++ b/wildfly/server-subsystem/src/main/resources/org/keycloak/subsystem/server/extension/LocalDescriptions.properties @@ -15,7 +15,38 @@ # limitations under the License. # +keycloak-server.migrate-json=Migrate keycloak-server.json to standalone.xml or domain.xml +keycloak-server.migrate-json.file=Optional local path to keycloak-server.json + keycloak-server.subsystem=Keycloak subsystem keycloak-server.subsystem.add=Operation Adds Keycloak subsystem keycloak-server.subsystem.remove=Operation removes Keycloak subsystem keycloak-server.subsystem.web-context=Web context where Keycloak server is bound. Default value is 'auth'. +keycloak-server.subsystem.providers=Paths to search for Keycloak provider jars. +keycloak-server.subsystem.master-realm-name=The name of the master admin realm. +keycloak-server.subsystem.scheduled-task-interval=The interval (in seconds) to run scheduled tasks. +keycloak-server.subsystem.spi=A Service Provider type. +keycloak-server.subsystem.theme=Theme configuration properties. + +keycloak-server.theme=Theme configuration properties. +keycloak-server.theme.add=Add the theme config properties. +keycloak-server.theme.remove=Remove the theme config properties. +keycloak-server.theme.staticMaxAge=Maximum time the browser should cache theme resources. A value of -1 will disable caching. +keycloak-server.theme.cacheThemes=If true, themes are cached. +keycloak-server.theme.cacheTemplates=If true, theme templates are cached. +keycloak-server.theme.welcomeTheme=The welcome theme. +keycloak-server.theme.default=The default theme to use if no theme is specified for a realm. +keycloak-server.theme.dir=Directory where themes can be located. +keycloak-server.theme.modules=List of modules containing themes. + +keycloak-server.spi=A Service Provider type. +keycloak-server.spi.add=Add an spi. +keycloak-server.spi.remove=Remove an spi. +keycloak-server.spi.default-provider=The default provider for the spi. +keycloak-server.spi.provider=A provider for the spi. + +keycloak-server.provider=A provider for the spi. +keycloak-server.provider.add=Add a provider. +keycloak-server.provider.remove=Remove a provider. +keycloak-server.provider.enabled=Enable or disable the provider. +keycloak-server.provider.properties=The properties for the provider. diff --git a/wildfly/server-subsystem/src/main/resources/schema/wildfly-keycloak-server_1_1.xsd b/wildfly/server-subsystem/src/main/resources/schema/wildfly-keycloak-server_1_1.xsd index df02124eb3..fa9f9e0478 100755 --- a/wildfly/server-subsystem/src/main/resources/schema/wildfly-keycloak-server_1_1.xsd +++ b/wildfly/server-subsystem/src/main/resources/schema/wildfly-keycloak-server_1_1.xsd @@ -1,21 +1,21 @@ +~ 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. +--> - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/wildfly/server-subsystem/src/main/resources/subsystem-templates/keycloak-server.xml b/wildfly/server-subsystem/src/main/resources/subsystem-templates/keycloak-server.xml index c269c314c3..67bf03e733 100644 --- a/wildfly/server-subsystem/src/main/resources/subsystem-templates/keycloak-server.xml +++ b/wildfly/server-subsystem/src/main/resources/subsystem-templates/keycloak-server.xml @@ -1,25 +1,23 @@ +~ 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. +--> - org.keycloak.keycloak-server-subsystem - - auth - + org.keycloak.keycloak-server-subsystem + ${keycloak.server.subsys.default.config} diff --git a/wildfly/server-subsystem/src/test/java/org/keycloak/subsystem/server/extension/JsonConfigConverterTestCase.java b/wildfly/server-subsystem/src/test/java/org/keycloak/subsystem/server/extension/JsonConfigConverterTestCase.java new file mode 100644 index 0000000000..4c6bf04f0d --- /dev/null +++ b/wildfly/server-subsystem/src/test/java/org/keycloak/subsystem/server/extension/JsonConfigConverterTestCase.java @@ -0,0 +1,446 @@ +/* + * 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.subsystem.server.extension; + +import java.util.ArrayList; +import java.util.List; +import org.jboss.as.controller.PathAddress; +import org.jboss.as.controller.PathElement; +import org.jboss.dmr.ModelNode; +import org.junit.Test; +import static org.junit.Assert.*; + +import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.ADDRESS; + +/** + * + * @author Stan Silvert ssilvert@redhat.com (C) 2016 Red Hat Inc. + */ +public class JsonConfigConverterTestCase { + + private final PathElement domainRoot = PathElement.pathElement("profile", "auth-server-clustered"); + private final PathAddress domainAddress = PathAddress.pathAddress(domainRoot) + .append(KeycloakExtension.PATH_SUBSYSTEM); + private final PathAddress standaloneAddress = PathAddress.pathAddress(KeycloakExtension.PATH_SUBSYSTEM); + + @Test + public void testConvertJsonStandaloneWithModules() throws Exception { + String json = basicJsonConfig(true); + List expResult = expectedOperations(true, false); + + List result = JsonConfigConverter.convertJsonConfig(json, standaloneAddress); + assertEquals(expResult, result); + } + + @Test + public void testConvertJsonStandaloneWithoutModules() throws Exception { + String json = basicJsonConfig(false); + List expResult = expectedOperations(false, false); + + List result = JsonConfigConverter.convertJsonConfig(json, standaloneAddress); + assertEquals(expResult, result); + } + + @Test + public void testConvertJsonDomainWithModules() throws Exception { + String json = basicJsonConfig(true); + List expResult = expectedOperations(true, true); + + List result = JsonConfigConverter.convertJsonConfig(json, domainAddress); + assertEquals(expResult, result); + } + + @Test + public void testConvertJsonDomainWithoutModules() throws Exception { + String json = basicJsonConfig(false); + List expResult = expectedOperations(false, true); + + List result = JsonConfigConverter.convertJsonConfig(json, domainAddress); + assertEquals(expResult, result); + } + + private String basicJsonConfig(boolean includeModules) { + String basicConfig = + "{\n" + + " \"providers\": [\n" + + " \"classpath:${jboss.home.dir}/providers/*\"\n" + + " ],\n" + + "\n" + + " \"admin\": {\n" + + " \"realm\": \"master\"\n" + + " },\n" + + "\n" + + " \"eventsStore\": {\n" + + " \"provider\": \"jpa\",\n" + + " \"jpa\": {\n" + + " \"exclude-events\": [ \"REFRESH_TOKEN\" ]\n" + + " }\n" + + " },\n" + + "\n" + + " \"realm\": {\n" + + " \"provider\": \"jpa\"\n" + + " },\n" + + "\n" + + " \"user\": {\n" + + " \"provider\": \"jpa\"\n" + + " },\n" + + "\n" + + " \"userCache\": {\n" + + " \"default\" : {\n" + + " \"enabled\": true\n" + + " }\n" + + " },\n" + + "\n" + + " \"userSessionPersister\": {\n" + + " \"provider\": \"jpa\"\n" + + " },\n" + + "\n" + + " \"authorizationPersister\": {\n" + + " \"provider\": \"jpa\"\n" + + " },\n" + + "\n" + + " \"timer\": {\n" + + " \"provider\": \"basic\"\n" + + " },\n" + + "\n" + + " \"theme\": {\n" + + " \"staticMaxAge\": 2592001,\n" + + " \"cacheTemplates\": false,\n" + + " \"cacheThemes\": false,\n" + + " \"welcomeTheme\": \"welcome\",\n" + + " \"default\": \"default\",\n" + + " \"folder\": {\n" + + " \"dir\": \"${jboss.home.dir}/themes\"\n"; + + + if (includeModules) { + basicConfig += + " },\n" + + " \"module\": {\n" + + " \"modules\": [ \"org.keycloak.example.themes\" ]\n" + + " }\n"; + } else { + basicConfig += + " }\n"; + } + + basicConfig += + " },\n" + + "\n" + + " \"scheduled\": {\n" + + " \"interval\": 900\n" + + " },\n" + + "\n" + + " \"connectionsHttpClient\": {\n" + + " \"default\": {}\n" + + " },\n" + + "\n" + + " \"connectionsJpa\": {\n" + + " \"default\": {\n" + + " \"dataSource\": \"java:jboss/datasources/KeycloakDS\",\n" + + " \"databaseSchema\": \"update\"\n" + + " }\n" + + " },\n" + + "\n" + + " \"realmCache\": {\n" + + " \"default\" : {\n" + + " \"enabled\": true\n" + + " }\n" + + " },\n" + + "\n" + + " \"connectionsInfinispan\": {\n" + + " \"provider\": \"default\",\n" + + " \"default\": {\n" + + " \"cacheContainer\" : \"java:comp/env/infinispan/Keycloak\"\n" + + " }\n" + + " }\n" + + "}"; + + return basicConfig; + } + + private List expectedOperations(boolean includeModules, boolean isDomain) { + List ops = new ArrayList<>(); + + ops.add(ModelNode.fromString( + "{\n" + + " \"operation\" => \"write-attribute\",\n" + + " \"address\" => [(\"subsystem\" => \"keycloak-server\")],\n" + + " \"name\" => \"master-realm-name\",\n" + + " \"value\" => \"master\"\n" + + "}" + )); + + ops.add(ModelNode.fromString( + "{\n" + + " \"operation\" => \"write-attribute\",\n" + + " \"address\" => [(\"subsystem\" => \"keycloak-server\")],\n" + + " \"name\" => \"scheduled-task-interval\",\n" + + " \"value\" => 900L\n" + + "}" + )); + + ops.add(ModelNode.fromString( + "{\n" + + " \"operation\" => \"write-attribute\",\n" + + " \"address\" => [(\"subsystem\" => \"keycloak-server\")],\n" + + " \"name\" => \"providers\",\n" + + " \"value\" => [\"classpath:${jboss.home.dir}/providers/*\"]\n" + + "}" + )); + + if (includeModules) { + ops.add(ModelNode.fromString( + "{\n" + + " \"operation\" => \"add\",\n" + + " \"address\" => [\n" + + " (\"subsystem\" => \"keycloak-server\"),\n" + + " (\"theme\" => \"defaults\")\n" + + " ],\n" + + " \"staticMaxAge\" => 2592001L,\n" + + " \"cacheTemplates\" => false,\n" + + " \"cacheThemes\" => false,\n" + + " \"dir\" => \"${jboss.home.dir}/themes\",\n" + + " \"welcomeTheme\" => \"welcome\",\n" + + " \"default\" => \"default\",\n" + + " \"modules\" => [\"org.keycloak.example.themes\"]\n" + + "}" + )); + } else { + ops.add(ModelNode.fromString( + "{\n" + + " \"operation\" => \"add\",\n" + + " \"address\" => [\n" + + " (\"subsystem\" => \"keycloak-server\"),\n" + + " (\"theme\" => \"defaults\")\n" + + " ],\n" + + " \"staticMaxAge\" => 2592001L,\n" + + " \"cacheTemplates\" => false,\n" + + " \"cacheThemes\" => false,\n" + + " \"dir\" => \"${jboss.home.dir}/themes\",\n" + + " \"welcomeTheme\" => \"welcome\",\n" + + " \"default\" => \"default\",\n" + + "}" + )); + } + + ops.add(ModelNode.fromString( + "{\n" + + " \"operation\" => \"add\",\n" + + " \"address\" => [\n" + + " (\"subsystem\" => \"keycloak-server\"),\n" + + " (\"spi\" => \"eventsStore\")\n" + + " ],\n" + + " \"default-provider\" => \"jpa\"\n" + + "}" + )); + + ops.add(ModelNode.fromString( + "{\n" + + " \"operation\" => \"add\",\n" + + " \"address\" => [\n" + + " (\"subsystem\" => \"keycloak-server\"),\n" + + " (\"spi\" => \"eventsStore\"),\n" + + " (\"provider\" => \"jpa\")\n" + + " ],\n" + + " \"properties\" => {\"exclude-events\" => \"[\\\"REFRESH_TOKEN\\\"]\"},\n" + + " \"enabled\" => true\n" + + "}" + )); + + ops.add(ModelNode.fromString( + "{\n" + + " \"operation\" => \"add\",\n" + + " \"address\" => [\n" + + " (\"subsystem\" => \"keycloak-server\"),\n" + + " (\"spi\" => \"realm\")\n" + + " ],\n" + + " \"default-provider\" => \"jpa\"\n" + + "}" + )); + + ops.add(ModelNode.fromString( + "{\n" + + " \"operation\" => \"add\",\n" + + " \"address\" => [\n" + + " (\"subsystem\" => \"keycloak-server\"),\n" + + " (\"spi\" => \"user\")\n" + + " ],\n" + + " \"default-provider\" => \"jpa\"\n" + + "}" + )); + + ops.add(ModelNode.fromString( + "{\n" + + " \"operation\" => \"add\",\n" + + " \"address\" => [\n" + + " (\"subsystem\" => \"keycloak-server\"),\n" + + " (\"spi\" => \"userCache\")\n" + + " ]\n" + + "}" + )); + + ops.add(ModelNode.fromString( + "{\n" + + " \"operation\" => \"add\",\n" + + " \"address\" => [\n" + + " (\"subsystem\" => \"keycloak-server\"),\n" + + " (\"spi\" => \"userCache\"),\n" + + " (\"provider\" => \"default\")\n" + + " ],\n" + + " \"enabled\" => true\n" + + "}" + )); + + ops.add(ModelNode.fromString( + "{\n" + + " \"operation\" => \"add\",\n" + + " \"address\" => [\n" + + " (\"subsystem\" => \"keycloak-server\"),\n" + + " (\"spi\" => \"userSessionPersister\")\n" + + " ],\n" + + " \"default-provider\" => \"jpa\"\n" + + "}" + )); + + ops.add(ModelNode.fromString( + "{\n" + + " \"operation\" => \"add\",\n" + + " \"address\" => [\n" + + " (\"subsystem\" => \"keycloak-server\"),\n" + + " (\"spi\" => \"authorizationPersister\")\n" + + " ],\n" + + " \"default-provider\" => \"jpa\"\n" + + "}" + )); + + ops.add(ModelNode.fromString( + "{\n" + + " \"operation\" => \"add\",\n" + + " \"address\" => [\n" + + " (\"subsystem\" => \"keycloak-server\"),\n" + + " (\"spi\" => \"timer\")\n" + + " ],\n" + + " \"default-provider\" => \"basic\"\n" + + "}" + )); + + ops.add(ModelNode.fromString( + "{\n" + + " \"operation\" => \"add\",\n" + + " \"address\" => [\n" + + " (\"subsystem\" => \"keycloak-server\"),\n" + + " (\"spi\" => \"connectionsHttpClient\")\n" + + " ]\n" + + "}" + )); + + ops.add(ModelNode.fromString( + "{\n" + + " \"operation\" => \"add\",\n" + + " \"address\" => [\n" + + " (\"subsystem\" => \"keycloak-server\"),\n" + + " (\"spi\" => \"connectionsHttpClient\"),\n" + + " (\"provider\" => \"default\")\n" + + " ],\n" + + " \"enabled\" => true\n" + + "}" + )); + + ops.add(ModelNode.fromString( + "{\n" + + " \"operation\" => \"add\",\n" + + " \"address\" => [\n" + + " (\"subsystem\" => \"keycloak-server\"),\n" + + " (\"spi\" => \"connectionsJpa\")\n" + + " ]\n" + + "}" + )); + + ops.add(ModelNode.fromString( + "{\n" + + " \"operation\" => \"add\",\n" + + " \"address\" => [\n" + + " (\"subsystem\" => \"keycloak-server\"),\n" + + " (\"spi\" => \"connectionsJpa\"),\n" + + " (\"provider\" => \"default\")\n" + + " ],\n" + + " \"properties\" => {\n" + + " \"dataSource\" => \"java:jboss/datasources/KeycloakDS\",\n" + + " \"databaseSchema\" => \"update\"\n" + + " },\n" + + " \"enabled\" => true\n" + + "}" + )); + + ops.add(ModelNode.fromString( + "{\n" + + " \"operation\" => \"add\",\n" + + " \"address\" => [\n" + + " (\"subsystem\" => \"keycloak-server\"),\n" + + " (\"spi\" => \"realmCache\")\n" + + " ]\n" + + "}" + )); + + ops.add(ModelNode.fromString( + "{\n" + + " \"operation\" => \"add\",\n" + + " \"address\" => [\n" + + " (\"subsystem\" => \"keycloak-server\"),\n" + + " (\"spi\" => \"realmCache\"),\n" + + " (\"provider\" => \"default\")\n" + + " ],\n" + + " \"enabled\" => true\n" + + "}" + )); + + ops.add(ModelNode.fromString( + "{\n" + + " \"operation\" => \"add\",\n" + + " \"address\" => [\n" + + " (\"subsystem\" => \"keycloak-server\"),\n" + + " (\"spi\" => \"connectionsInfinispan\")\n" + + " ],\n" + + " \"default-provider\" => \"default\"\n" + + "}" + )); + + ops.add(ModelNode.fromString( + "{\n" + + " \"operation\" => \"add\",\n" + + " \"address\" => [\n" + + " (\"subsystem\" => \"keycloak-server\"),\n" + + " (\"spi\" => \"connectionsInfinispan\"),\n" + + " (\"provider\" => \"default\")\n" + + " ],\n" + + " \"properties\" => {\"cacheContainer\" => \"java:comp/env/infinispan/Keycloak\"},\n" + + " \"enabled\" => true\n" + + "}" + )); + + if (isDomain) { // prepend the domain root + for (ModelNode op : ops) { + PathAddress addr = PathAddress.pathAddress(op.get(ADDRESS)); + PathAddress domainAddr = PathAddress.pathAddress(domainRoot).append(addr); + op.get(ADDRESS).set(domainAddr.toModelNode()); + } + } + + return ops; + } +} \ No newline at end of file diff --git a/wildfly/server-subsystem/src/test/java/org/keycloak/subsystem/server/extension/SubsystemParsingTestCase.java b/wildfly/server-subsystem/src/test/java/org/keycloak/subsystem/server/extension/SubsystemParsingTestCase.java index c84acc26e2..0bc41106dd 100755 --- a/wildfly/server-subsystem/src/test/java/org/keycloak/subsystem/server/extension/SubsystemParsingTestCase.java +++ b/wildfly/server-subsystem/src/test/java/org/keycloak/subsystem/server/extension/SubsystemParsingTestCase.java @@ -17,10 +17,9 @@ package org.keycloak.subsystem.server.extension; import java.io.IOException; +import java.util.Properties; import org.jboss.as.subsystem.test.AbstractSubsystemBaseTest; -import org.jboss.dmr.ModelNode; -import org.junit.Test; /** * Tests all management expects for subsystem, parsing, marshaling, model definition and other @@ -38,14 +37,14 @@ public class SubsystemParsingTestCase extends AbstractSubsystemBaseTest { super(KeycloakExtension.SUBSYSTEM_NAME, new KeycloakExtension()); } - @Test - public void testJson() throws Exception { - ModelNode node = new ModelNode(); - node.get("web-context").set("auth"); - - System.out.println("json=" + node.toJSONString(false)); + @Override + protected Properties getResolvedProperties() { + Properties properties = new Properties(); + properties.put("jboss.home.dir", System.getProperty("java.io.tmpdir")); + properties.put("keycloak.jta.lookup.provider", "jboss"); + return properties; } - + @Override protected String getSubsystemXml() throws IOException { return readResource("keycloak-server-1.1.xml"); diff --git a/wildfly/server-subsystem/src/test/resources/org/keycloak/subsystem/server/extension/keycloak-server-1.1.xml b/wildfly/server-subsystem/src/test/resources/org/keycloak/subsystem/server/extension/keycloak-server-1.1.xml index 103e1625fc..e5206116c6 100644 --- a/wildfly/server-subsystem/src/test/resources/org/keycloak/subsystem/server/extension/keycloak-server-1.1.xml +++ b/wildfly/server-subsystem/src/test/resources/org/keycloak/subsystem/server/extension/keycloak-server-1.1.xml @@ -1,20 +1 @@ - - - - auth - \ No newline at end of file +${keycloak.server.subsys.default.config} \ No newline at end of file