diff --git a/.gitignore b/.gitignore index 32e898b25a..bf5d094f0c 100644 --- a/.gitignore +++ b/.gitignore @@ -49,3 +49,7 @@ target # Maven shade ############# *dependency-reduced-pom.xml + +# nodejs # +########## +node_modules \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index ec6ed80dac..d3ea4f1c4d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,33 +1,27 @@ language: java +dist: precise cache: - directories: - - $HOME/.m2 - -before_cache: - - rm -rf $HOME/.m2/repository/org/keycloak + cache: false env: global: - MAVEN_SKIP_RC=true - - MAVEN_OPTS="-Xms512m -Xmx2048m" + - MAVEN_OPTS="-Xms512m -Xmx1536m" matrix: - - TESTS=group1 - - TESTS=group2 - - TESTS=group3 - - TESTS=group4 + - TESTS=unit + - TESTS=server-group1 + - TESTS=server-group2 + - TESTS=server-group3 + - TESTS=server-group4 - TESTS=old jdk: - oraclejdk8 -before_script: - - export MAVEN_SKIP_RC=true +install: true -install: - - travis_wait 60 mvn install --no-snapshot-updates -Pdistribution -DskipTestsuite -B -V -q - -script: +script: - ./travis-run-tests.sh $TESTS sudo: false diff --git a/adapters/oidc/adapter-core/pom.xml b/adapters/oidc/adapter-core/pom.xml index c2afe453e2..a2f5b5bcff 100755 --- a/adapters/oidc/adapter-core/pom.xml +++ b/adapters/oidc/adapter-core/pom.xml @@ -21,7 +21,7 @@ keycloak-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../../../pom.xml 4.0.0 diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeployment.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeployment.java index 45c4557609..d5761bcf1a 100755 --- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeployment.java +++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeployment.java @@ -91,6 +91,8 @@ public class KeycloakDeployment { // https://tools.ietf.org/html/rfc7636 protected boolean pkce = false; protected boolean ignoreOAuthQueryParameter; + + protected Map redirectRewriteRules; public KeycloakDeployment() { } @@ -446,4 +448,14 @@ public class KeycloakDeployment { public boolean isOAuthQueryParameterEnabled() { return !this.ignoreOAuthQueryParameter; } + + public Map getRedirectRewriteRules() { + return redirectRewriteRules; + } + + public void setRewriteRedirectRules(Map redirectRewriteRules) { + this.redirectRewriteRules = redirectRewriteRules; + } + + } diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeploymentBuilder.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeploymentBuilder.java index eca6849914..7fca1f1ac8 100755 --- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeploymentBuilder.java +++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeploymentBuilder.java @@ -116,6 +116,7 @@ public class KeycloakDeploymentBuilder { deployment.setMinTimeBetweenJwksRequests(adapterConfig.getMinTimeBetweenJwksRequests()); deployment.setPublicKeyCacheTtl(adapterConfig.getPublicKeyCacheTtl()); deployment.setIgnoreOAuthQueryParameter(adapterConfig.isIgnoreOAuthQueryParameter()); + deployment.setRewriteRedirectRules(adapterConfig.getRedirectRewriteRules()); if (realmKeyPem == null && adapterConfig.isBearerOnly() && adapterConfig.getAuthServerUrl() == null) { throw new IllegalArgumentException("For bearer auth, you must set the realm-public-key or auth-server-url"); diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/OAuthRequestAuthenticator.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/OAuthRequestAuthenticator.java index fe8dfdce51..ee3f214e24 100755 --- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/OAuthRequestAuthenticator.java +++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/OAuthRequestAuthenticator.java @@ -25,7 +25,6 @@ import org.keycloak.adapters.spi.AuthChallenge; import org.keycloak.adapters.spi.AuthOutcome; import org.keycloak.adapters.spi.HttpFacade; import org.keycloak.common.VerificationException; -import org.keycloak.common.util.Encode; import org.keycloak.common.util.KeycloakUriBuilder; import org.keycloak.common.util.UriUtils; import org.keycloak.constants.AdapterConstants; @@ -38,7 +37,10 @@ import org.keycloak.representations.IDToken; import org.keycloak.util.TokenUtil; import java.io.IOException; -import java.util.concurrent.atomic.AtomicLong; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Map; +import java.util.logging.Level; /** @@ -141,6 +143,7 @@ public class OAuthRequestAuthenticator { protected String getRedirectUri(String state) { String url = getRequestUrl(); log.debugf("callback uri: %s", url); + if (!facade.getRequest().isSecure() && deployment.getSslRequired().isRequired(facade.getRequest().getRemoteAddr())) { int port = sslRedirectPort(); if (port < 0) { @@ -170,7 +173,7 @@ public class OAuthRequestAuthenticator { KeycloakUriBuilder redirectUriBuilder = deployment.getAuthUrl().clone() .queryParam(OAuth2Constants.RESPONSE_TYPE, OAuth2Constants.CODE) .queryParam(OAuth2Constants.CLIENT_ID, deployment.getResourceName()) - .queryParam(OAuth2Constants.REDIRECT_URI, Encode.encodeQueryParamAsIs(url)) // Need to encode uri ourselves as queryParam() will not encode % characters. + .queryParam(OAuth2Constants.REDIRECT_URI, rewrittenRedirectUri(url)) .queryParam(OAuth2Constants.STATE, state) .queryParam("login", "true"); if(loginHint != null && loginHint.length() > 0){ @@ -320,10 +323,11 @@ public class OAuthRequestAuthenticator { AccessTokenResponse tokenResponse = null; strippedOauthParametersRequestUri = stripOauthParametersFromRedirect(); + try { // For COOKIE store we don't have httpSessionId and single sign-out won't be available String httpSessionId = deployment.getTokenStore() == TokenStore.SESSION ? reqAuthenticator.changeHttpSessionId(true) : null; - tokenResponse = ServerRequest.invokeAccessCodeToToken(deployment, code, strippedOauthParametersRequestUri, httpSessionId); + tokenResponse = ServerRequest.invokeAccessCodeToToken(deployment, code, rewrittenRedirectUri(strippedOauthParametersRequestUri), httpSessionId); } catch (ServerRequest.HttpFailure failure) { log.error("failed to turn code into token"); log.error("status from server: " + failure.getStatus()); @@ -375,6 +379,23 @@ public class OAuthRequestAuthenticator { .replaceQueryParam(OAuth2Constants.STATE, null); return builder.build().toString(); } - + + private String rewrittenRedirectUri(String originalUri) { + Map rewriteRules = deployment.getRedirectRewriteRules(); + if(rewriteRules != null && !rewriteRules.isEmpty()) { + try { + URL url = new URL(originalUri); + Map.Entry rule = rewriteRules.entrySet().iterator().next(); + StringBuilder redirectUriBuilder = new StringBuilder(url.getProtocol()); + redirectUriBuilder.append("://"+ url.getAuthority()); + redirectUriBuilder.append(url.getPath().replaceFirst(rule.getKey(), rule.getValue())); + return redirectUriBuilder.toString(); + } catch (MalformedURLException ex) { + log.error("Not a valid request url"); + throw new RuntimeException(ex); + } + } + return originalUri; + } } diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/RefreshableKeycloakSecurityContext.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/RefreshableKeycloakSecurityContext.java index c70bce1261..fa1cefdff5 100755 --- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/RefreshableKeycloakSecurityContext.java +++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/RefreshableKeycloakSecurityContext.java @@ -155,7 +155,9 @@ public class RefreshableKeycloakSecurityContext extends KeycloakSecurityContext this.refreshToken = response.getRefreshToken(); } this.tokenString = tokenString; - tokenStore.refreshCallback(this); + if (tokenStore != null) { + tokenStore.refreshCallback(this); + } return true; } diff --git a/adapters/oidc/adapter-core/src/test/java/org/keycloak/adapters/KeycloakDeploymentBuilderTest.java b/adapters/oidc/adapter-core/src/test/java/org/keycloak/adapters/KeycloakDeploymentBuilderTest.java index cd191e26ae..af58b33834 100644 --- a/adapters/oidc/adapter-core/src/test/java/org/keycloak/adapters/KeycloakDeploymentBuilderTest.java +++ b/adapters/oidc/adapter-core/src/test/java/org/keycloak/adapters/KeycloakDeploymentBuilderTest.java @@ -75,6 +75,7 @@ public class KeycloakDeploymentBuilderTest { assertEquals(10, deployment.getTokenMinimumTimeToLive()); assertEquals(20, deployment.getMinTimeBetweenJwksRequests()); assertEquals(120, deployment.getPublicKeyCacheTtl()); + assertEquals("/api/$1", deployment.getRedirectRewriteRules().get("^/wsmaster/api/(.*)$")); } @Test diff --git a/adapters/oidc/adapter-core/src/test/resources/keycloak.json b/adapters/oidc/adapter-core/src/test/resources/keycloak.json index 521b8a9bc7..e1b88816f4 100644 --- a/adapters/oidc/adapter-core/src/test/resources/keycloak.json +++ b/adapters/oidc/adapter-core/src/test/resources/keycloak.json @@ -33,5 +33,8 @@ "token-minimum-time-to-live": 10, "min-time-between-jwks-requests": 20, "public-key-cache-ttl": 120, - "ignore-oauth-query-parameter": true + "ignore-oauth-query-parameter": true, + "redirect-rewrite-rules" : { + "^/wsmaster/api/(.*)$" : "/api/$1" + } } \ No newline at end of file diff --git a/adapters/oidc/as7-eap6/as7-adapter-spi/pom.xml b/adapters/oidc/as7-eap6/as7-adapter-spi/pom.xml index 0ed189ddbd..89faf8b97b 100755 --- a/adapters/oidc/as7-eap6/as7-adapter-spi/pom.xml +++ b/adapters/oidc/as7-eap6/as7-adapter-spi/pom.xml @@ -21,7 +21,7 @@ keycloak-as7-integration-pom org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../pom.xml 4.0.0 diff --git a/adapters/oidc/as7-eap6/as7-adapter/pom.xml b/adapters/oidc/as7-eap6/as7-adapter/pom.xml index ed8c3c2bda..47e344f914 100755 --- a/adapters/oidc/as7-eap6/as7-adapter/pom.xml +++ b/adapters/oidc/as7-eap6/as7-adapter/pom.xml @@ -21,7 +21,7 @@ keycloak-as7-integration-pom org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../pom.xml 4.0.0 diff --git a/adapters/oidc/as7-eap6/as7-subsystem/pom.xml b/adapters/oidc/as7-eap6/as7-subsystem/pom.xml index de9b4244d1..5a2d24e3e9 100755 --- a/adapters/oidc/as7-eap6/as7-subsystem/pom.xml +++ b/adapters/oidc/as7-eap6/as7-subsystem/pom.xml @@ -21,7 +21,7 @@ org.keycloak keycloak-as7-integration-pom - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../pom.xml diff --git a/adapters/oidc/as7-eap6/pom.xml b/adapters/oidc/as7-eap6/pom.xml index 3d3a1745ee..4b5a4272ec 100755 --- a/adapters/oidc/as7-eap6/pom.xml +++ b/adapters/oidc/as7-eap6/pom.xml @@ -20,7 +20,7 @@ keycloak-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../../../pom.xml Keycloak AS7 / JBoss EAP 6 Integration diff --git a/adapters/oidc/cli-sso/README.md b/adapters/oidc/cli-sso/README.md new file mode 100755 index 0000000000..fb0fdbecc5 --- /dev/null +++ b/adapters/oidc/cli-sso/README.md @@ -0,0 +1,9 @@ +CLI Single Sign On +=================================== + +This java-based utility is meant for providing Keycloak integration to +command line applications that are either written in Java or another language. The +idea is that the Java app provided by this utility performs a login for a specific +client, parses responses, and exports an access token as an environment variable +that can be used by the command line utility you are accessing. + diff --git a/adapters/oidc/cli-sso/login.sh b/adapters/oidc/cli-sso/login.sh new file mode 100755 index 0000000000..ff33a015e9 --- /dev/null +++ b/adapters/oidc/cli-sso/login.sh @@ -0,0 +1,10 @@ +#!/bin/sh +export KC_AUTH_SERVER=http://localhost:8080/auth +export KC_REALM=master +export KC_CLIENT=cli + +export KC_ACCESS_TOKEN=`java -DKEYCLOAK_AUTH_SERVER=$KC_AUTH_SERVER -DKEYCLOAK_REALM=$KC_REALM -DKEYCLOAK_CLIENT=$KC_CLIENT -jar target/keycloak-cli-sso-3.3.0.CR1-SNAPSHOT.jar login` + + + + diff --git a/adapters/oidc/cli-sso/logout.sh b/adapters/oidc/cli-sso/logout.sh new file mode 100644 index 0000000000..ca99f88b76 --- /dev/null +++ b/adapters/oidc/cli-sso/logout.sh @@ -0,0 +1,9 @@ +#!/bin/sh + +java -DKEYCLOAK_AUTH_SERVER=$KC_AUTH_SERVER -DKEYCLOAK_REALM=$KC_REALM -DKEYCLOAK_CLIENT=$KC_CLIENT -jar target/keycloak-cli-sso-3.3.0.CR1-SNAPSHOT.jar logout + +unset KC_ACCESS_TOKEN + + + + diff --git a/adapters/oidc/cli-sso/pom.xml b/adapters/oidc/cli-sso/pom.xml new file mode 100755 index 0000000000..216c3b794e --- /dev/null +++ b/adapters/oidc/cli-sso/pom.xml @@ -0,0 +1,84 @@ + + + + + + keycloak-parent + org.keycloak + 3.3.0.CR1-SNAPSHOT + ../../../pom.xml + + 4.0.0 + + keycloak-cli-sso + Keycloak CLI SSO Framework + + + + + org.keycloak + keycloak-installed-adapter + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + ${maven.compiler.source} + ${maven.compiler.target} + + + + org.apache.maven.plugins + maven-shade-plugin + 3.0.0 + + + + org.keycloak.adapters.KeycloakCliSsoMain + + + + + + *:* + + META-INF/*.SF + META-INF/*.DSA + META-INF/*.RSA + + + + + + + package + + shade + + + + + + + + diff --git a/adapters/oidc/cli-sso/src/main/java/org/keycloak/adapters/KeycloakCliSsoMain.java b/adapters/oidc/cli-sso/src/main/java/org/keycloak/adapters/KeycloakCliSsoMain.java new file mode 100644 index 0000000000..3aaeb9b18a --- /dev/null +++ b/adapters/oidc/cli-sso/src/main/java/org/keycloak/adapters/KeycloakCliSsoMain.java @@ -0,0 +1,45 @@ +/* + * 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.adapters; + +import org.keycloak.adapters.installed.KeycloakCliSso; +import org.keycloak.adapters.installed.KeycloakInstalled; +import org.keycloak.common.util.Time; +import org.keycloak.representations.AccessTokenResponse; +import org.keycloak.representations.adapters.config.AdapterConfig; +import org.keycloak.util.JsonSerialization; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.file.Paths; +import java.util.HashMap; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class KeycloakCliSsoMain extends KeycloakCliSso { + + public static void main(String[] args) throws Exception { + new KeycloakCliSsoMain().mainCmd(args); + } +} diff --git a/adapters/oidc/installed/pom.xml b/adapters/oidc/installed/pom.xml index b509be49f5..3c14afa2de 100755 --- a/adapters/oidc/installed/pom.xml +++ b/adapters/oidc/installed/pom.xml @@ -21,7 +21,7 @@ keycloak-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../../../pom.xml 4.0.0 diff --git a/adapters/oidc/installed/src/main/java/org/keycloak/adapters/installed/KeycloakCliSso.java b/adapters/oidc/installed/src/main/java/org/keycloak/adapters/installed/KeycloakCliSso.java new file mode 100644 index 0000000000..3c1d3655a1 --- /dev/null +++ b/adapters/oidc/installed/src/main/java/org/keycloak/adapters/installed/KeycloakCliSso.java @@ -0,0 +1,266 @@ +/* + * 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.adapters.installed; + +import org.keycloak.adapters.KeycloakDeployment; +import org.keycloak.adapters.KeycloakDeploymentBuilder; +import org.keycloak.adapters.ServerRequest; +import org.keycloak.common.util.Time; +import org.keycloak.representations.AccessTokenResponse; +import org.keycloak.representations.adapters.config.AdapterConfig; +import org.keycloak.util.JsonSerialization; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.file.Paths; +import java.util.HashMap; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * + * + * + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class KeycloakCliSso { + + public void mainCmd(String[] args) throws Exception { + if (args.length != 1) { + printHelp(); + return; + } + + if (args[0].equalsIgnoreCase("login")) { + login(); + } else if (args[0].equalsIgnoreCase("login-manual")) { + loginManual(); + } else if (args[0].equalsIgnoreCase("token")) { + token(); + } else if (args[0].equalsIgnoreCase("logout")) { + logout(); + } else if (args[0].equalsIgnoreCase("env")) { + System.out.println(System.getenv().toString()); + } else { + printHelp(); + } + } + + + public void printHelp() { + System.err.println("Commands:"); + System.err.println(" login - login with desktop browser if available, otherwise do manual login. Output is access token."); + System.err.println(" login-manual - manual login"); + System.err.println(" token - print access token if logged in"); + System.err.println(" logout - logout."); + System.exit(1); + } + + public AdapterConfig getConfig() { + String url = System.getProperty("KEYCLOAK_AUTH_SERVER"); + if (url == null) { + System.err.println("KEYCLOAK_AUTH_SERVER property not set"); + System.exit(1); + } + String realm = System.getProperty("KEYCLOAK_REALM"); + if (realm == null) { + System.err.println("KEYCLOAK_REALM property not set"); + System.exit(1); + + } + String client = System.getProperty("KEYCLOAK_CLIENT"); + if (client == null) { + System.err.println("KEYCLOAK_CLIENT property not set"); + System.exit(1); + } + String secret = System.getProperty("KEYCLOAK_CLIENT_SECRET"); + + + + AdapterConfig config = new AdapterConfig(); + config.setAuthServerUrl(url); + config.setRealm(realm); + config.setResource(client); + config.setSslRequired("external"); + if (secret != null) { + Map creds = new HashMap<>(); + creds.put("secret", secret); + config.setCredentials(creds); + } else { + config.setPublicClient(true); + } + return config; + } + + public boolean checkToken() throws Exception { + String token = getTokenResponse(); + if (token == null) return false; + + + if (token != null) { + Matcher m = Pattern.compile("\\{.*\\}\\z").matcher(token); + if (m.find()) { + String json = m.group(0); + try { + AccessTokenResponse tokenResponse = JsonSerialization.readValue(json, AccessTokenResponse.class); + if (Time.currentTime() < tokenResponse.getExpiresIn()) { + return true; + } + AdapterConfig config = getConfig(); + KeycloakInstalled installed = new KeycloakInstalled(KeycloakDeploymentBuilder.build(config)); + installed.refreshToken(tokenResponse.getRefreshToken()); + processResponse(installed); + return true; + } catch (Exception e) { + System.err.println("Error processing existing token"); + e.printStackTrace(); + } + + } + } + return false; + + } + + private String getTokenResponse() throws IOException { + String token = null; + File tokenFile = getTokenFilePath(); + if (tokenFile.exists()) { + FileInputStream fis = new FileInputStream(tokenFile); + byte[] data = new byte[(int) tokenFile.length()]; + fis.read(data); + fis.close(); + token = new String(data, "UTF-8"); + } + return token; + } + + public void token() throws Exception { + String token = getTokenResponse(); + if (token == null) { + System.err.println("There is no token for client"); + System.exit(1); + } else { + Matcher m = Pattern.compile("\\{.*\\}\\z").matcher(token); + if (m.find()) { + String json = m.group(0); + try { + AccessTokenResponse tokenResponse = JsonSerialization.readValue(json, AccessTokenResponse.class); + if (Time.currentTime() < tokenResponse.getExpiresIn()) { + System.out.println(tokenResponse.getToken()); + return; + } else { + System.err.println("token in response file is expired"); + System.exit(1); + } + } catch (Exception e) { + System.err.println("Failure processing token response file"); + e.printStackTrace(); + System.exit(1); + } + } else { + System.err.println("Could not find json within token response file"); + System.exit(1); + } + } + } + + public void login() throws Exception { + if (checkToken()) return; + AdapterConfig config = getConfig(); + KeycloakInstalled installed = new KeycloakInstalled(KeycloakDeploymentBuilder.build(config)); + installed.login(); + processResponse(installed); + } + + public String getHome() { + String home = System.getenv("HOME"); + if (home == null) { + home = System.getProperty("HOME"); + if (home == null) { + home = Paths.get("").toAbsolutePath().normalize().toString(); + } + } + return home; + } + + public File getTokenDirectory() { + return Paths.get(getHome(), System.getProperty("basepath", ".keycloak-sso"), System.getProperty("KEYCLOAK_REALM")).toFile(); + } + + public File getTokenFilePath() { + return Paths.get(getHome(), System.getProperty("basepath", ".keycloak-sso"), System.getProperty("KEYCLOAK_REALM"), System.getProperty("KEYCLOAK_CLIENT") + ".json").toFile(); + } + + private void processResponse(KeycloakInstalled installed) throws IOException { + AccessTokenResponse tokenResponse = installed.getTokenResponse(); + tokenResponse.setExpiresIn(Time.currentTime() + tokenResponse.getExpiresIn()); + tokenResponse.setIdToken(null); + String output = JsonSerialization.writeValueAsString(tokenResponse); + getTokenDirectory().mkdirs(); + FileOutputStream fos = new FileOutputStream(getTokenFilePath()); + fos.write(output.getBytes("UTF-8")); + fos.flush(); + fos.close(); + System.out.println(tokenResponse.getToken()); + } + + public void loginManual() throws Exception { + if (checkToken()) return; + AdapterConfig config = getConfig(); + KeycloakDeployment deployment = KeycloakDeploymentBuilder.build(config); + KeycloakInstalled installed = new KeycloakInstalled(deployment); + installed.loginManual(); + processResponse(installed); + } + + public void logout() throws Exception { + String token = getTokenResponse(); + if (token != null) { + Matcher m = Pattern.compile("\\{.*\\}\\z").matcher(token); + if (m.find()) { + String json = m.group(0); + try { + AccessTokenResponse tokenResponse = JsonSerialization.readValue(json, AccessTokenResponse.class); + if (Time.currentTime() > tokenResponse.getExpiresIn()) { + System.err.println("Login is expired"); + System.exit(1); + } + AdapterConfig config = getConfig(); + KeycloakDeployment deployment = KeycloakDeploymentBuilder.build(config); + ServerRequest.invokeLogout(deployment, tokenResponse.getRefreshToken()); + for (File fp : getTokenDirectory().listFiles()) fp.delete(); + System.out.println("logout complete"); + } catch (Exception e) { + System.err.println("Failure processing token response file"); + e.printStackTrace(); + System.exit(1); + } + } else { + System.err.println("Could not find json within token response file"); + System.exit(1); + } + } else { + System.err.println("Not logged in"); + System.exit(1); + } + } +} diff --git a/adapters/oidc/installed/src/main/java/org/keycloak/adapters/installed/KeycloakInstalled.java b/adapters/oidc/installed/src/main/java/org/keycloak/adapters/installed/KeycloakInstalled.java index 9834fe24de..61ca06e520 100644 --- a/adapters/oidc/installed/src/main/java/org/keycloak/adapters/installed/KeycloakInstalled.java +++ b/adapters/oidc/installed/src/main/java/org/keycloak/adapters/installed/KeycloakInstalled.java @@ -17,6 +17,7 @@ package org.keycloak.adapters.installed; +import org.apache.commons.codec.Charsets; import org.keycloak.OAuth2Constants; import org.keycloak.OAuthErrorException; import org.keycloak.adapters.KeycloakDeployment; @@ -24,6 +25,7 @@ import org.keycloak.adapters.KeycloakDeploymentBuilder; import org.keycloak.adapters.ServerRequest; import org.keycloak.adapters.rotation.AdapterRSATokenVerifier; import org.keycloak.common.VerificationException; +import org.keycloak.common.util.KeycloakUriBuilder; import org.keycloak.jose.jws.JWSInput; import org.keycloak.jose.jws.JWSInputException; import org.keycloak.representations.AccessToken; @@ -43,6 +45,7 @@ import java.net.ServerSocket; import java.net.Socket; import java.net.URI; import java.net.URISyntaxException; +import java.util.Locale; import java.util.UUID; import java.util.concurrent.TimeUnit; @@ -51,6 +54,11 @@ import java.util.concurrent.TimeUnit; */ public class KeycloakInstalled { + public interface HttpResponseWriter { + void success(PrintWriter pw, KeycloakInstalled ki); + void failure(PrintWriter pw, KeycloakInstalled ki); + } + private static final String KEYCLOAK_JSON = "META-INF/keycloak.json"; private KeycloakDeployment deployment; @@ -59,12 +67,18 @@ public class KeycloakInstalled { LOGGED_MANUAL, LOGGED_DESKTOP } + private AccessTokenResponse tokenResponse; private String tokenString; private String idTokenString; private IDToken idToken; private AccessToken token; private String refreshToken; private Status status; + private Locale locale; + private HttpResponseWriter loginResponseWriter; + private HttpResponseWriter logoutResponseWriter; + + public KeycloakInstalled() { InputStream config = Thread.currentThread().getContextClassLoader().getResourceAsStream(KEYCLOAK_JSON); @@ -75,6 +89,92 @@ public class KeycloakInstalled { deployment = KeycloakDeploymentBuilder.build(config); } + public KeycloakInstalled(KeycloakDeployment deployment) { + this.deployment = deployment; + } + + private static HttpResponseWriter defaultLoginWriter = new HttpResponseWriter() { + @Override + public void success(PrintWriter pw, KeycloakInstalled ki) { + pw.println("HTTP/1.1 200 OK"); + pw.println("Content-Type: text/html"); + pw.println(); + pw.println("

Login completed.

"); + pw.println("This browser will remain logged in until you close it, logout, or the session expires."); + pw.println("
"); + pw.flush(); + + } + + @Override + public void failure(PrintWriter pw, KeycloakInstalled ki) { + pw.println("HTTP/1.1 200 OK"); + pw.println("Content-Type: text/html"); + pw.println(); + pw.println("

Login attempt failed.

"); + pw.println("
"); + pw.flush(); + + } + }; + private static HttpResponseWriter defaultLogoutWriter = new HttpResponseWriter() { + @Override + public void success(PrintWriter pw, KeycloakInstalled ki) { + pw.println("HTTP/1.1 200 OK"); + pw.println("Content-Type: text/html"); + pw.println(); + pw.println("

Logout completed.

"); + pw.println("You may close this browser tab."); + pw.println("
"); + pw.flush(); + + } + + @Override + public void failure(PrintWriter pw, KeycloakInstalled ki) { + pw.println("HTTP/1.1 200 OK"); + pw.println("Content-Type: text/html"); + pw.println(); + pw.println("

Logout failed.

"); + pw.println("You may close this browser tab."); + pw.println("
"); + pw.flush(); + + } + }; + + public HttpResponseWriter getLoginResponseWriter() { + if (loginResponseWriter == null) { + return defaultLoginWriter; + } else { + return loginResponseWriter; + } + } + + public HttpResponseWriter getLogoutResponseWriter() { + if (logoutResponseWriter == null) { + return defaultLogoutWriter; + } else { + return logoutResponseWriter; + } + } + + public void setLoginResponseWriter(HttpResponseWriter loginResponseWriter) { + this.loginResponseWriter = loginResponseWriter; + } + + public void setLogoutResponseWriter(HttpResponseWriter logoutResponseWriter) { + this.logoutResponseWriter = logoutResponseWriter; + } + + public Locale getLocale() { + return locale; + } + + public void setLocale(Locale locale) { + this.locale = locale; + } + public void login() throws IOException, ServerRequest.HttpFailure, VerificationException, InterruptedException, OAuthErrorException, URISyntaxException { if (isDesktopSupported()) { loginDesktop(); @@ -108,19 +208,22 @@ public class KeycloakInstalled { } public void loginDesktop() throws IOException, VerificationException, OAuthErrorException, URISyntaxException, ServerRequest.HttpFailure, InterruptedException { - CallbackListener callback = new CallbackListener(); + CallbackListener callback = new CallbackListener(getLoginResponseWriter()); callback.start(); String redirectUri = "http://localhost:" + callback.server.getLocalPort(); String state = UUID.randomUUID().toString(); - String authUrl = deployment.getAuthUrl().clone() + KeycloakUriBuilder builder = deployment.getAuthUrl().clone() .queryParam(OAuth2Constants.RESPONSE_TYPE, OAuth2Constants.CODE) .queryParam(OAuth2Constants.CLIENT_ID, deployment.getResourceName()) .queryParam(OAuth2Constants.REDIRECT_URI, redirectUri) .queryParam(OAuth2Constants.STATE, state) - .queryParam(OAuth2Constants.SCOPE, OAuth2Constants.SCOPE_OPENID) - .build().toString(); + .queryParam(OAuth2Constants.SCOPE, OAuth2Constants.SCOPE_OPENID); + if (locale != null) { + builder.queryParam(OAuth2Constants.UI_LOCALES_PARAM, locale.getLanguage()); + } + String authUrl = builder.build().toString(); Desktop.getDesktop().browse(new URI(authUrl)); @@ -144,7 +247,7 @@ public class KeycloakInstalled { } private void logoutDesktop() throws IOException, URISyntaxException, InterruptedException { - CallbackListener callback = new CallbackListener(); + CallbackListener callback = new CallbackListener(getLogoutResponseWriter()); callback.start(); String redirectUri = "http://localhost:" + callback.server.getLocalPort(); @@ -167,9 +270,6 @@ public class KeycloakInstalled { } public void loginManual(PrintStream printer, Reader reader) throws IOException, ServerRequest.HttpFailure, VerificationException { - CallbackListener callback = new CallbackListener(); - callback.start(); - String redirectUri = "urn:ietf:wg:oauth:2.0:oob"; String authUrl = deployment.getAuthUrl().clone() @@ -208,7 +308,14 @@ public class KeycloakInstalled { parseAccessToken(tokenResponse); } + public void refreshToken(String refreshToken) throws IOException, ServerRequest.HttpFailure, VerificationException { + AccessTokenResponse tokenResponse = ServerRequest.invokeRefresh(deployment, refreshToken); + parseAccessToken(tokenResponse); + + } + private void parseAccessToken(AccessTokenResponse tokenResponse) throws VerificationException { + this.tokenResponse = tokenResponse; tokenString = tokenResponse.getToken(); refreshToken = tokenResponse.getRefreshToken(); idTokenString = tokenResponse.getIdToken(); @@ -240,6 +347,10 @@ public class KeycloakInstalled { return refreshToken; } + public AccessTokenResponse getTokenResponse() { + return tokenResponse; + } + public boolean isDesktopSupported() { return Desktop.isDesktopSupported(); } @@ -248,6 +359,8 @@ public class KeycloakInstalled { return deployment; } + + private void processCode(String code, String redirectUri) throws IOException, ServerRequest.HttpFailure, VerificationException { AccessTokenResponse tokenResponse = ServerRequest.invokeAccessCodeToToken(deployment, code, redirectUri, null); parseAccessToken(tokenResponse); @@ -269,6 +382,7 @@ public class KeycloakInstalled { return sb.toString(); } + public class CallbackListener extends Thread { private ServerSocket server; @@ -283,14 +397,19 @@ public class KeycloakInstalled { private String state; - public CallbackListener() throws IOException { + private Socket socket; + + private HttpResponseWriter writer; + + public CallbackListener(HttpResponseWriter writer) throws IOException { + this.writer = writer; server = new ServerSocket(0); } @Override public void run() { try { - Socket socket = server.accept(); + socket = server.accept(); BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream())); String request = br.readLine(); @@ -314,10 +433,15 @@ public class KeycloakInstalled { } } - PrintWriter pw = new PrintWriter(new OutputStreamWriter(socket.getOutputStream())); - pw.println("Please close window and return to application"); - pw.flush(); + OutputStreamWriter out = new OutputStreamWriter(socket.getOutputStream()); + PrintWriter pw = new PrintWriter(out); + if (error == null) { + writer.success(pw, KeycloakInstalled.this); + } else { + writer.failure(pw, KeycloakInstalled.this); + } + pw.flush(); socket.close(); } catch (IOException e) { errorException = e; @@ -328,6 +452,8 @@ public class KeycloakInstalled { } catch (IOException e) { } } + } + } diff --git a/adapters/oidc/jaxrs-oauth-client/pom.xml b/adapters/oidc/jaxrs-oauth-client/pom.xml index bcd585f5c7..6f46e7c9cc 100755 --- a/adapters/oidc/jaxrs-oauth-client/pom.xml +++ b/adapters/oidc/jaxrs-oauth-client/pom.xml @@ -21,7 +21,7 @@ keycloak-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../../../pom.xml 4.0.0 diff --git a/adapters/oidc/jetty/jetty-core/pom.xml b/adapters/oidc/jetty/jetty-core/pom.xml index 2f0ad16a41..3ee1c5e155 100755 --- a/adapters/oidc/jetty/jetty-core/pom.xml +++ b/adapters/oidc/jetty/jetty-core/pom.xml @@ -21,7 +21,7 @@ keycloak-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../../../../pom.xml 4.0.0 diff --git a/adapters/oidc/jetty/jetty8.1/pom.xml b/adapters/oidc/jetty/jetty8.1/pom.xml index 7a3fa0cd61..312475509b 100755 --- a/adapters/oidc/jetty/jetty8.1/pom.xml +++ b/adapters/oidc/jetty/jetty8.1/pom.xml @@ -21,7 +21,7 @@ keycloak-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../../../../pom.xml 4.0.0 diff --git a/adapters/oidc/jetty/jetty9.1/pom.xml b/adapters/oidc/jetty/jetty9.1/pom.xml index 1c3edd09c1..c5a4784711 100755 --- a/adapters/oidc/jetty/jetty9.1/pom.xml +++ b/adapters/oidc/jetty/jetty9.1/pom.xml @@ -21,7 +21,7 @@ keycloak-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../../../../pom.xml 4.0.0 diff --git a/adapters/oidc/jetty/jetty9.2/pom.xml b/adapters/oidc/jetty/jetty9.2/pom.xml index 6d34e8bf89..f8b6335404 100755 --- a/adapters/oidc/jetty/jetty9.2/pom.xml +++ b/adapters/oidc/jetty/jetty9.2/pom.xml @@ -21,7 +21,7 @@ keycloak-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../../../../pom.xml 4.0.0 diff --git a/adapters/oidc/jetty/jetty9.3/pom.xml b/adapters/oidc/jetty/jetty9.3/pom.xml index 0077b8dc0a..c4cb37489a 100644 --- a/adapters/oidc/jetty/jetty9.3/pom.xml +++ b/adapters/oidc/jetty/jetty9.3/pom.xml @@ -21,7 +21,7 @@ keycloak-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../../../../pom.xml 4.0.0 diff --git a/adapters/oidc/jetty/jetty9.4/pom.xml b/adapters/oidc/jetty/jetty9.4/pom.xml index 377ddc06d3..acb36c6bcc 100644 --- a/adapters/oidc/jetty/jetty9.4/pom.xml +++ b/adapters/oidc/jetty/jetty9.4/pom.xml @@ -21,7 +21,7 @@ keycloak-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../../../../pom.xml 4.0.0 diff --git a/adapters/oidc/jetty/pom.xml b/adapters/oidc/jetty/pom.xml index ede26fc29b..5ec0c168b1 100755 --- a/adapters/oidc/jetty/pom.xml +++ b/adapters/oidc/jetty/pom.xml @@ -20,7 +20,7 @@ keycloak-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../../../pom.xml Keycloak Jetty Integration diff --git a/adapters/oidc/js/pom.xml b/adapters/oidc/js/pom.xml index 7d939c7a8d..8b4cc6789c 100755 --- a/adapters/oidc/js/pom.xml +++ b/adapters/oidc/js/pom.xml @@ -21,7 +21,7 @@ keycloak-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../../../pom.xml 4.0.0 diff --git a/adapters/oidc/js/src/main/resources/keycloak.js b/adapters/oidc/js/src/main/resources/keycloak.js index 89b15b8a52..a784936c78 100755 --- a/adapters/oidc/js/src/main/resources/keycloak.js +++ b/adapters/oidc/js/src/main/resources/keycloak.js @@ -33,6 +33,13 @@ interval: 5 }; + var scripts = document.getElementsByTagName('script'); + for (var i = 0; i < scripts.length; i++) { + if ((scripts[i].src.indexOf('keycloak.js') !== -1 || scripts[i].src.indexOf('keycloak.min.js') !== -1) && scripts[i].src.indexOf('version=') !== -1) { + kc.iframeVersion = scripts[i].src.substring(scripts[i].src.indexOf('version=') + 8).split('&')[0]; + } + } + kc.init = function (initOptions) { kc.authenticated = false; @@ -831,6 +838,10 @@ } var src = getRealmUrl() + '/protocol/openid-connect/login-status-iframe.html'; + if (kc.iframeVersion) { + src = src + '?version=' + kc.iframeVersion; + } + iframe.setAttribute('src', src ); iframe.style.display = 'none'; document.body.appendChild(iframe); diff --git a/adapters/oidc/js/src/main/resources/login-status-iframe.html b/adapters/oidc/js/src/main/resources/login-status-iframe.html index b1012f7694..f58f76abca 100755 --- a/adapters/oidc/js/src/main/resources/login-status-iframe.html +++ b/adapters/oidc/js/src/main/resources/login-status-iframe.html @@ -28,7 +28,7 @@ } else if (!init) { var req = new XMLHttpRequest(); - var url = location.href + "/init"; + var url = location.href.split("?")[0] + "/init"; url += "?client_id=" + encodeURIComponent(clientId); url += "&origin=" + encodeURIComponent(origin); diff --git a/adapters/oidc/osgi-adapter/pom.xml b/adapters/oidc/osgi-adapter/pom.xml index 7bb5004f7d..26a90ec487 100755 --- a/adapters/oidc/osgi-adapter/pom.xml +++ b/adapters/oidc/osgi-adapter/pom.xml @@ -21,7 +21,7 @@ keycloak-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../../../pom.xml 4.0.0 diff --git a/adapters/oidc/pom.xml b/adapters/oidc/pom.xml index d68f3598b9..9207401a1c 100755 --- a/adapters/oidc/pom.xml +++ b/adapters/oidc/pom.xml @@ -20,7 +20,7 @@ keycloak-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../../pom.xml Keycloak OIDC Client Adapter Modules @@ -34,6 +34,7 @@ adapter-core as7-eap6 installed + cli-sso jaxrs-oauth-client jetty js diff --git a/adapters/oidc/servlet-filter/pom.xml b/adapters/oidc/servlet-filter/pom.xml index 053710b8c0..3d81dc7322 100755 --- a/adapters/oidc/servlet-filter/pom.xml +++ b/adapters/oidc/servlet-filter/pom.xml @@ -21,7 +21,7 @@ keycloak-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../../../pom.xml 4.0.0 diff --git a/adapters/oidc/servlet-filter/src/main/java/org/keycloak/adapters/servlet/KeycloakOIDCFilter.java b/adapters/oidc/servlet-filter/src/main/java/org/keycloak/adapters/servlet/KeycloakOIDCFilter.java index 2763ff1a9d..c51b9db4f7 100755 --- a/adapters/oidc/servlet-filter/src/main/java/org/keycloak/adapters/servlet/KeycloakOIDCFilter.java +++ b/adapters/oidc/servlet-filter/src/main/java/org/keycloak/adapters/servlet/KeycloakOIDCFilter.java @@ -54,72 +54,96 @@ import java.util.regex.Pattern; */ public class KeycloakOIDCFilter implements Filter { + private final static Logger log = Logger.getLogger("" + KeycloakOIDCFilter.class); + public static final String SKIP_PATTERN_PARAM = "keycloak.config.skipPattern"; + public static final String CONFIG_RESOLVER_PARAM = "keycloak.config.resolver"; + + public static final String CONFIG_FILE_PARAM = "keycloak.config.file"; + + public static final String CONFIG_PATH_PARAM = "keycloak.config.path"; + protected AdapterDeploymentContext deploymentContext; + protected SessionIdMapper idMapper = new InMemorySessionIdMapper(); + protected NodesRegistrationManagement nodesRegistrationManagement; + protected Pattern skipPattern; - private final static Logger log = Logger.getLogger(""+KeycloakOIDCFilter.class); + private final KeycloakConfigResolver definedconfigResolver; + + /** + * Constructor that can be used to define a {@code KeycloakConfigResolver} that will be used at initialization to + * provide the {@code KeycloakDeployment}. + * @param definedconfigResolver the resolver + */ + public KeycloakOIDCFilter(KeycloakConfigResolver definedconfigResolver) { + this.definedconfigResolver = definedconfigResolver; + } + + public KeycloakOIDCFilter() { + this(null); + } @Override public void init(final FilterConfig filterConfig) throws ServletException { - String skipPatternDefinition = filterConfig.getInitParameter(SKIP_PATTERN_PARAM); if (skipPatternDefinition != null) { skipPattern = Pattern.compile(skipPatternDefinition, Pattern.DOTALL); } - String configResolverClass = filterConfig.getInitParameter("keycloak.config.resolver"); - if (configResolverClass != null) { - try { - KeycloakConfigResolver configResolver = (KeycloakConfigResolver) getClass().getClassLoader().loadClass(configResolverClass).newInstance(); - deploymentContext = new AdapterDeploymentContext(configResolver); - log.log(Level.INFO, "Using {0} to resolve Keycloak configuration on a per-request basis.", configResolverClass); - } catch (Exception ex) { - log.log(Level.FINE, "The specified resolver {0} could NOT be loaded. Keycloak is unconfigured and will deny all requests. Reason: {1}", new Object[]{configResolverClass, ex.getMessage()}); - deploymentContext = new AdapterDeploymentContext(new KeycloakDeployment()); - } + if (definedconfigResolver != null) { + deploymentContext = new AdapterDeploymentContext(definedconfigResolver); + log.log(Level.INFO, "Using {0} to resolve Keycloak configuration on a per-request basis.", definedconfigResolver.getClass()); } else { - String fp = filterConfig.getInitParameter("keycloak.config.file"); - InputStream is = null; - if (fp != null) { + String configResolverClass = filterConfig.getInitParameter(CONFIG_RESOLVER_PARAM); + if (configResolverClass != null) { try { - is = new FileInputStream(fp); - } catch (FileNotFoundException e) { - throw new RuntimeException(e); + KeycloakConfigResolver configResolver = (KeycloakConfigResolver) getClass().getClassLoader().loadClass(configResolverClass).newInstance(); + deploymentContext = new AdapterDeploymentContext(configResolver); + log.log(Level.INFO, "Using {0} to resolve Keycloak configuration on a per-request basis.", configResolverClass); + } catch (Exception ex) { + log.log(Level.FINE, "The specified resolver {0} could NOT be loaded. Keycloak is unconfigured and will deny all requests. Reason: {1}", new Object[]{configResolverClass, ex.getMessage()}); + deploymentContext = new AdapterDeploymentContext(new KeycloakDeployment()); } } else { - String path = "/WEB-INF/keycloak.json"; - String pathParam = filterConfig.getInitParameter("keycloak.config.path"); - if (pathParam != null) path = pathParam; - is = filterConfig.getServletContext().getResourceAsStream(path); + String fp = filterConfig.getInitParameter(CONFIG_FILE_PARAM); + InputStream is = null; + if (fp != null) { + try { + is = new FileInputStream(fp); + } catch (FileNotFoundException e) { + throw new RuntimeException(e); + } + } else { + String path = "/WEB-INF/keycloak.json"; + String pathParam = filterConfig.getInitParameter(CONFIG_PATH_PARAM); + if (pathParam != null) path = pathParam; + is = filterConfig.getServletContext().getResourceAsStream(path); + } + KeycloakDeployment kd = createKeycloakDeploymentFrom(is); + deploymentContext = new AdapterDeploymentContext(kd); + log.fine("Keycloak is using a per-deployment configuration."); } - KeycloakDeployment kd = createKeycloakDeploymentFrom(is); - deploymentContext = new AdapterDeploymentContext(kd); - log.fine("Keycloak is using a per-deployment configuration."); } filterConfig.getServletContext().setAttribute(AdapterDeploymentContext.class.getName(), deploymentContext); nodesRegistrationManagement = new NodesRegistrationManagement(); } private KeycloakDeployment createKeycloakDeploymentFrom(InputStream is) { - if (is == null) { log.fine("No adapter configuration. Keycloak is unconfigured and will deny all requests."); return new KeycloakDeployment(); } - return KeycloakDeploymentBuilder.build(is); } @Override public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { - log.fine("Keycloak OIDC Filter"); - //System.err.println("Keycloak OIDC Filter: " + ((HttpServletRequest)req).getRequestURL().toString()); HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) res; @@ -201,7 +225,7 @@ public class KeycloakOIDCFilter implements Filter { * * @param request the request to check * @return {@code true} if the request should not be handled, - * {@code false} otherwise. + * {@code false} otherwise. */ private boolean shouldSkip(HttpServletRequest request) { diff --git a/adapters/oidc/servlet-oauth-client/pom.xml b/adapters/oidc/servlet-oauth-client/pom.xml index 836186fc7f..bb709f1dc2 100755 --- a/adapters/oidc/servlet-oauth-client/pom.xml +++ b/adapters/oidc/servlet-oauth-client/pom.xml @@ -21,7 +21,7 @@ keycloak-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../../../pom.xml 4.0.0 diff --git a/adapters/oidc/spring-boot-container-bundle/pom.xml b/adapters/oidc/spring-boot-container-bundle/pom.xml index 4f68494d38..49cab4ebe0 100644 --- a/adapters/oidc/spring-boot-container-bundle/pom.xml +++ b/adapters/oidc/spring-boot-container-bundle/pom.xml @@ -4,7 +4,7 @@ keycloak-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../../../pom.xml spring-boot-container-bundle diff --git a/adapters/oidc/spring-boot/pom.xml b/adapters/oidc/spring-boot/pom.xml index abd1512b02..6a720f65a5 100755 --- a/adapters/oidc/spring-boot/pom.xml +++ b/adapters/oidc/spring-boot/pom.xml @@ -21,7 +21,7 @@ keycloak-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../../../pom.xml 4.0.0 diff --git a/adapters/oidc/spring-security/pom.xml b/adapters/oidc/spring-security/pom.xml index d1a975ac9f..b304cd5b0d 100755 --- a/adapters/oidc/spring-security/pom.xml +++ b/adapters/oidc/spring-security/pom.xml @@ -21,7 +21,7 @@ keycloak-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../../../pom.xml 4.0.0 diff --git a/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/facade/SimpleHttpFacade.java b/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/facade/SimpleHttpFacade.java index cb9ddcd064..2c9876eace 100755 --- a/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/facade/SimpleHttpFacade.java +++ b/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/facade/SimpleHttpFacade.java @@ -19,6 +19,9 @@ package org.keycloak.adapters.springsecurity.facade; import org.keycloak.KeycloakSecurityContext; import org.keycloak.adapters.OIDCHttpFacade; +import org.keycloak.adapters.spi.KeycloakAccount; +import org.keycloak.adapters.springsecurity.account.SimpleKeycloakAccount; +import org.keycloak.adapters.springsecurity.token.KeycloakAuthenticationToken; import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.util.Assert; @@ -57,7 +60,8 @@ public class SimpleHttpFacade implements OIDCHttpFacade { SecurityContext context = SecurityContextHolder.getContext(); if (context != null && context.getAuthentication() != null) { - return (KeycloakSecurityContext) context.getAuthentication().getDetails(); + KeycloakAuthenticationToken authentication = (KeycloakAuthenticationToken) context.getAuthentication(); + return authentication.getAccount().getKeycloakSecurityContext(); } return null; diff --git a/adapters/oidc/spring-security/src/test/java/org/keycloak/adapters/springsecurity/facade/SimpleHttpFacadeTest.java b/adapters/oidc/spring-security/src/test/java/org/keycloak/adapters/springsecurity/facade/SimpleHttpFacadeTest.java new file mode 100644 index 0000000000..28c6ce8eaf --- /dev/null +++ b/adapters/oidc/spring-security/src/test/java/org/keycloak/adapters/springsecurity/facade/SimpleHttpFacadeTest.java @@ -0,0 +1,41 @@ +package org.keycloak.adapters.springsecurity.facade; + +import org.junit.Before; +import org.junit.Test; +import org.keycloak.adapters.RefreshableKeycloakSecurityContext; +import org.keycloak.adapters.spi.KeycloakAccount; +import org.keycloak.adapters.springsecurity.account.SimpleKeycloakAccount; +import org.keycloak.adapters.springsecurity.token.KeycloakAuthenticationToken; +import org.mockito.internal.util.collections.Sets; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; + +import java.security.Principal; +import java.util.Set; + +import static org.junit.Assert.assertNotNull; +import static org.mockito.Mockito.mock; + +public class SimpleHttpFacadeTest { + + @Before + public void setup() { + SecurityContext springSecurityContext = SecurityContextHolder.createEmptyContext(); + SecurityContextHolder.setContext(springSecurityContext); + Set roles = Sets.newSet("user"); + Principal principal = mock(Principal.class); + RefreshableKeycloakSecurityContext keycloakSecurityContext = mock(RefreshableKeycloakSecurityContext.class); + KeycloakAccount account = new SimpleKeycloakAccount(principal, roles, keycloakSecurityContext); + KeycloakAuthenticationToken token = new KeycloakAuthenticationToken(account); + springSecurityContext.setAuthentication(token); + } + + @Test + public void shouldRetrieveKeycloakSecurityContext() { + SimpleHttpFacade facade = new SimpleHttpFacade(new MockHttpServletRequest(), new MockHttpServletResponse()); + + assertNotNull(facade.getSecurityContext()); + } +} diff --git a/adapters/oidc/tomcat/pom.xml b/adapters/oidc/tomcat/pom.xml index d691c1d7fe..d733dbd699 100755 --- a/adapters/oidc/tomcat/pom.xml +++ b/adapters/oidc/tomcat/pom.xml @@ -20,7 +20,7 @@ keycloak-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../../../pom.xml Keycloak Tomcat Integration diff --git a/adapters/oidc/tomcat/tomcat-core/pom.xml b/adapters/oidc/tomcat/tomcat-core/pom.xml index 397f6226d2..c47cc4dde9 100755 --- a/adapters/oidc/tomcat/tomcat-core/pom.xml +++ b/adapters/oidc/tomcat/tomcat-core/pom.xml @@ -21,7 +21,7 @@ keycloak-tomcat-integration-pom org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../pom.xml 4.0.0 diff --git a/adapters/oidc/tomcat/tomcat6/pom.xml b/adapters/oidc/tomcat/tomcat6/pom.xml index 47972ac2a7..d0b7059351 100755 --- a/adapters/oidc/tomcat/tomcat6/pom.xml +++ b/adapters/oidc/tomcat/tomcat6/pom.xml @@ -21,7 +21,7 @@ keycloak-tomcat-integration-pom org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../pom.xml 4.0.0 diff --git a/adapters/oidc/tomcat/tomcat7/pom.xml b/adapters/oidc/tomcat/tomcat7/pom.xml index 01f12ef28d..d42530557a 100755 --- a/adapters/oidc/tomcat/tomcat7/pom.xml +++ b/adapters/oidc/tomcat/tomcat7/pom.xml @@ -21,7 +21,7 @@ keycloak-tomcat-integration-pom org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../pom.xml 4.0.0 diff --git a/adapters/oidc/tomcat/tomcat8/pom.xml b/adapters/oidc/tomcat/tomcat8/pom.xml index ef6ea29801..8bf86063b0 100755 --- a/adapters/oidc/tomcat/tomcat8/pom.xml +++ b/adapters/oidc/tomcat/tomcat8/pom.xml @@ -21,7 +21,7 @@ keycloak-tomcat-integration-pom org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../pom.xml 4.0.0 diff --git a/adapters/oidc/undertow/pom.xml b/adapters/oidc/undertow/pom.xml index 048e249406..ccdc34e60e 100755 --- a/adapters/oidc/undertow/pom.xml +++ b/adapters/oidc/undertow/pom.xml @@ -21,7 +21,7 @@ keycloak-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../../../pom.xml 4.0.0 diff --git a/adapters/oidc/wildfly-elytron/pom.xml b/adapters/oidc/wildfly-elytron/pom.xml index edfd4aa325..71d5681818 100755 --- a/adapters/oidc/wildfly-elytron/pom.xml +++ b/adapters/oidc/wildfly-elytron/pom.xml @@ -22,7 +22,7 @@ keycloak-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../../../pom.xml 4.0.0 diff --git a/adapters/oidc/wildfly-elytron/src/main/java/org/keycloak/adapters/elytron/ElytronHttpFacade.java b/adapters/oidc/wildfly-elytron/src/main/java/org/keycloak/adapters/elytron/ElytronHttpFacade.java index bc2e9039ca..4472af75f9 100644 --- a/adapters/oidc/wildfly-elytron/src/main/java/org/keycloak/adapters/elytron/ElytronHttpFacade.java +++ b/adapters/oidc/wildfly-elytron/src/main/java/org/keycloak/adapters/elytron/ElytronHttpFacade.java @@ -90,6 +90,11 @@ class ElytronHttpFacade implements OIDCHttpFacade { void authenticationComplete() { if (securityIdentity != null) { + HttpScope requestScope = request.getScope(Scope.EXCHANGE); + RefreshableKeycloakSecurityContext keycloakSecurityContext = account.getKeycloakSecurityContext(); + + requestScope.setAttachment(KeycloakSecurityContext.class.getName(), keycloakSecurityContext); + this.request.authenticationComplete(response -> { if (!restored) { responseConsumer.accept(response); diff --git a/adapters/oidc/wildfly-elytron/src/main/java/org/keycloak/adapters/elytron/KeycloakHttpServerAuthenticationMechanism.java b/adapters/oidc/wildfly-elytron/src/main/java/org/keycloak/adapters/elytron/KeycloakHttpServerAuthenticationMechanism.java index 3fcf9bf484..8d0cd1d538 100644 --- a/adapters/oidc/wildfly-elytron/src/main/java/org/keycloak/adapters/elytron/KeycloakHttpServerAuthenticationMechanism.java +++ b/adapters/oidc/wildfly-elytron/src/main/java/org/keycloak/adapters/elytron/KeycloakHttpServerAuthenticationMechanism.java @@ -71,7 +71,7 @@ class KeycloakHttpServerAuthenticationMechanism implements HttpServerAuthenticat AdapterDeploymentContext deploymentContext = getDeploymentContext(request); if (deploymentContext == null) { - LOGGER.debugf("Ignoring request for path [%s] from mechanism [%s]. No deployment context found.", request.getRequestURI()); + LOGGER.debugf("Ignoring request for path [%s] from mechanism [%s]. No deployment context found.", request.getRequestURI(), getMechanismName()); request.noAuthenticationInProgress(); return; } diff --git a/adapters/oidc/wildfly/pom.xml b/adapters/oidc/wildfly/pom.xml index d6a2184284..e93be16c2b 100755 --- a/adapters/oidc/wildfly/pom.xml +++ b/adapters/oidc/wildfly/pom.xml @@ -20,7 +20,7 @@ keycloak-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../../../pom.xml Keycloak WildFly Integration diff --git a/adapters/oidc/wildfly/wf8-subsystem/pom.xml b/adapters/oidc/wildfly/wf8-subsystem/pom.xml index e3e2c34ecb..2afcf14cdf 100755 --- a/adapters/oidc/wildfly/wf8-subsystem/pom.xml +++ b/adapters/oidc/wildfly/wf8-subsystem/pom.xml @@ -21,7 +21,7 @@ org.keycloak keycloak-parent - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../../../../pom.xml diff --git a/adapters/oidc/wildfly/wildfly-adapter/pom.xml b/adapters/oidc/wildfly/wildfly-adapter/pom.xml index 89d38703fb..686661d8cf 100644 --- a/adapters/oidc/wildfly/wildfly-adapter/pom.xml +++ b/adapters/oidc/wildfly/wildfly-adapter/pom.xml @@ -21,7 +21,7 @@ keycloak-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../../../../pom.xml 4.0.0 diff --git a/adapters/oidc/wildfly/wildfly-subsystem/pom.xml b/adapters/oidc/wildfly/wildfly-subsystem/pom.xml index 596f00f3ec..1d8e6cf916 100755 --- a/adapters/oidc/wildfly/wildfly-subsystem/pom.xml +++ b/adapters/oidc/wildfly/wildfly-subsystem/pom.xml @@ -21,7 +21,7 @@ org.keycloak keycloak-parent - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../../../../pom.xml diff --git a/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/KeycloakAdapterConfigService.java b/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/KeycloakAdapterConfigService.java index e96a5e51f8..5a71e615a8 100755 --- a/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/KeycloakAdapterConfigService.java +++ b/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/KeycloakAdapterConfigService.java @@ -37,6 +37,8 @@ import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.ADD public final class KeycloakAdapterConfigService { private static final String CREDENTIALS_JSON_NAME = "credentials"; + + private static final String REDIRECT_REWRITE_RULE_JSON_NAME = "redirect-rewrite-rule"; private static final KeycloakAdapterConfigService INSTANCE = new KeycloakAdapterConfigService(); @@ -129,6 +131,56 @@ public final class KeycloakAdapterConfigService { ModelNode deployment = this.secureDeployments.get(deploymentNameFromOp(operation)); return deployment.get(CREDENTIALS_JSON_NAME); } + + public void addRedirectRewriteRule(ModelNode operation, ModelNode model) { + ModelNode redirectRewritesRules = redirectRewriteRuleFromOp(operation); + if (!redirectRewritesRules.isDefined()) { + redirectRewritesRules = new ModelNode(); + } + + String redirectRewriteRuleName = redirectRewriteRule(operation); + if (!redirectRewriteRuleName.contains(".")) { + redirectRewritesRules.get(redirectRewriteRuleName).set(model.get("value").asString()); + } else { + String[] parts = redirectRewriteRuleName.split("\\."); + String provider = parts[0]; + String property = parts[1]; + ModelNode redirectRewriteRule = redirectRewritesRules.get(provider); + if (!redirectRewriteRule.isDefined()) { + redirectRewriteRule = new ModelNode(); + } + redirectRewriteRule.get(property).set(model.get("value").asString()); + redirectRewritesRules.set(provider, redirectRewriteRule); + } + + ModelNode deployment = this.secureDeployments.get(deploymentNameFromOp(operation)); + deployment.get(REDIRECT_REWRITE_RULE_JSON_NAME).set(redirectRewritesRules); + } + + public void removeRedirectRewriteRule(ModelNode operation) { + ModelNode redirectRewritesRules = redirectRewriteRuleFromOp(operation); + if (!redirectRewritesRules.isDefined()) { + throw new RuntimeException("Can not remove redirect rewrite rule. No rules defined for deployment in op " + operation.toString()); + } + + String ruleName = credentialNameFromOp(operation); + redirectRewritesRules.remove(ruleName); + } + + public void updateRedirectRewriteRule(ModelNode operation, String attrName, ModelNode resolvedValue) { + ModelNode redirectRewritesRules = redirectRewriteRuleFromOp(operation); + if (!redirectRewritesRules.isDefined()) { + throw new RuntimeException("Can not update redirect rewrite rule. No rules defined for deployment in op " + operation.toString()); + } + + String ruleName = credentialNameFromOp(operation); + redirectRewritesRules.get(ruleName).set(resolvedValue); + } + + private ModelNode redirectRewriteRuleFromOp(ModelNode operation) { + ModelNode deployment = this.secureDeployments.get(deploymentNameFromOp(operation)); + return deployment.get(REDIRECT_REWRITE_RULE_JSON_NAME); + } private String realmNameFromOp(ModelNode operation) { return valueFromOpAddress(RealmDefinition.TAG_NAME, operation); @@ -141,6 +193,10 @@ public final class KeycloakAdapterConfigService { private String credentialNameFromOp(ModelNode operation) { return valueFromOpAddress(CredentialDefinition.TAG_NAME, operation); } + + private String redirectRewriteRule(ModelNode operation) { + return valueFromOpAddress(RedirecRewritetRuleDefinition.TAG_NAME, operation); + } private String valueFromOpAddress(String addrElement, ModelNode operation) { String deploymentName = getValueOfAddrElement(operation.get(ADDRESS), addrElement); diff --git a/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/KeycloakExtension.java b/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/KeycloakExtension.java index 541454a37d..d04e72d403 100755 --- a/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/KeycloakExtension.java +++ b/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/KeycloakExtension.java @@ -48,6 +48,7 @@ public class KeycloakExtension implements Extension { static final RealmDefinition REALM_DEFINITION = new RealmDefinition(); static final SecureDeploymentDefinition SECURE_DEPLOYMENT_DEFINITION = new SecureDeploymentDefinition(); static final CredentialDefinition CREDENTIAL_DEFINITION = new CredentialDefinition(); + static final RedirecRewritetRuleDefinition REDIRECT_RULE_DEFINITON = new RedirecRewritetRuleDefinition(); public static StandardResourceDescriptionResolver getResourceDescriptionResolver(final String... keyPrefix) { StringBuilder prefix = new StringBuilder(SUBSYSTEM_NAME); @@ -77,6 +78,7 @@ public class KeycloakExtension implements Extension { registration.registerSubModel(REALM_DEFINITION); ManagementResourceRegistration secureDeploymentRegistration = registration.registerSubModel(SECURE_DEPLOYMENT_DEFINITION); secureDeploymentRegistration.registerSubModel(CREDENTIAL_DEFINITION); + secureDeploymentRegistration.registerSubModel(REDIRECT_RULE_DEFINITON); subsystem.registerXMLElementWriter(PARSER); } diff --git a/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/KeycloakSubsystemParser.java b/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/KeycloakSubsystemParser.java index d4ddc02e3d..79555e3b7b 100755 --- a/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/KeycloakSubsystemParser.java +++ b/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/KeycloakSubsystemParser.java @@ -96,12 +96,17 @@ class KeycloakSubsystemParser implements XMLStreamConstants, XMLElementReader
  • credentialsToAdd = new ArrayList(); + List redirectRulesToAdd = new ArrayList(); while (reader.hasNext() && nextTag(reader) != END_ELEMENT) { String tagName = reader.getLocalName(); if (tagName.equals(CredentialDefinition.TAG_NAME)) { readCredential(reader, addr, credentialsToAdd); continue; } + if (tagName.equals(RedirecRewritetRuleDefinition.TAG_NAME)) { + readRewriteRule(reader, addr, redirectRulesToAdd); + continue; + } SimpleAttributeDefinition def = SecureDeploymentDefinition.lookup(tagName); if (def == null) throw new XMLStreamException("Unknown secure-deployment tag " + tagName); @@ -111,6 +116,7 @@ class KeycloakSubsystemParser implements XMLStreamConstants, XMLElementReader
  • credentialsToAdd) throws XMLStreamException { @@ -149,6 +155,43 @@ class KeycloakSubsystemParser implements XMLStreamConstants, XMLElementReader
  • rewriteRuleToToAdd) throws XMLStreamException { + String name = readNameAttribute(reader); + + Map values = new HashMap<>(); + String textValue = null; + while (reader.hasNext()) { + int next = reader.next(); + if (next == CHARACTERS) { + // text value of redirect rule element + String text = reader.getText(); + if (text == null || text.trim().isEmpty()) { + continue; + } + textValue = text; + } else if (next == START_ELEMENT) { + String key = reader.getLocalName(); + reader.next(); + String value = reader.getText(); + reader.next(); + + values.put(key, value); + } else if (next == END_ELEMENT) { + break; + } + } + + if (textValue != null) { + ModelNode addRedirectRule = getRedirectRuleToAdd(parent, name, textValue); + rewriteRuleToToAdd.add(addRedirectRule); + } else { + for (Map.Entry entry : values.entrySet()) { + ModelNode addRedirectRule = getRedirectRuleToAdd(parent, name + "." + entry.getKey(), entry.getValue()); + rewriteRuleToToAdd.add(addRedirectRule); + } + } + } private ModelNode getCredentialToAdd(PathAddress parent, String name, String value) { ModelNode addCredential = new ModelNode(); @@ -158,6 +201,15 @@ class KeycloakSubsystemParser implements XMLStreamConstants, XMLElementReader
  • parsed = new LinkedHashMap<>(); + for (Property redirectRule : redirectRules.asPropertyList()) { + String ruleName = redirectRule.getName(); + String ruleValue = redirectRule.getValue().get(RedirecRewritetRuleDefinition.VALUE.getName()).asString(); + parsed.put(ruleName, ruleValue); + } + + for (Map.Entry entry : parsed.entrySet()) { + writer.writeStartElement(RedirecRewritetRuleDefinition.TAG_NAME); + writer.writeAttribute("name", entry.getKey()); + + Object value = entry.getValue(); + if (value instanceof String) { + writeCharacters(writer, (String) value); + } else { + Map redirectRulesProps = (Map) value; + for (Map.Entry prop : redirectRulesProps.entrySet()) { + writer.writeStartElement(prop.getKey()); + writeCharacters(writer, prop.getValue()); + writer.writeEndElement(); + } + } + + writer.writeEndElement(); + } + } // code taken from org.jboss.as.controller.AttributeMarshaller private void writeCharacters(XMLExtendedStreamWriter writer, String content) throws XMLStreamException { diff --git a/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/RedirecRewritetRuleDefinition.java b/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/RedirecRewritetRuleDefinition.java new file mode 100644 index 0000000000..a9095c7d8e --- /dev/null +++ b/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/RedirecRewritetRuleDefinition.java @@ -0,0 +1,61 @@ +/* + * 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.adapter.extension; + +import org.jboss.as.controller.AttributeDefinition; +import org.jboss.as.controller.PathElement; +import org.jboss.as.controller.SimpleAttributeDefinitionBuilder; +import org.jboss.as.controller.SimpleResourceDefinition; +import org.jboss.as.controller.operations.common.GenericSubsystemDescribeHandler; +import org.jboss.as.controller.operations.validation.StringLengthValidator; +import org.jboss.as.controller.registry.ManagementResourceRegistration; +import org.jboss.dmr.ModelType; + +/** + * + * @author sblanc + */ +public class RedirecRewritetRuleDefinition extends SimpleResourceDefinition { + + public static final String TAG_NAME = "redirect-rewrite-rule"; + + protected static final AttributeDefinition VALUE = + new SimpleAttributeDefinitionBuilder("value", ModelType.STRING, false) + .setAllowExpression(true) + .setValidator(new StringLengthValidator(1, Integer.MAX_VALUE, false, true)) + .build(); + + public RedirecRewritetRuleDefinition() { + super(PathElement.pathElement(TAG_NAME), + KeycloakExtension.getResourceDescriptionResolver(TAG_NAME), + new RedirectRewriteRuleAddHandler(VALUE), + RedirectRewriteRuleRemoveHandler.INSTANCE); + } + + @Override + public void registerOperations(ManagementResourceRegistration resourceRegistration) { + super.registerOperations(resourceRegistration); + resourceRegistration.registerOperationHandler(GenericSubsystemDescribeHandler.DEFINITION, GenericSubsystemDescribeHandler.INSTANCE); + } + + @Override + public void registerAttributes(ManagementResourceRegistration resourceRegistration) { + super.registerAttributes(resourceRegistration); + resourceRegistration.registerReadWriteAttribute(VALUE, null, new RedirectRewriteRuleReadWriteAttributeHandler()); + } +} diff --git a/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/RedirectRewriteRuleAddHandler.java b/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/RedirectRewriteRuleAddHandler.java new file mode 100644 index 0000000000..2fc25f7df7 --- /dev/null +++ b/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/RedirectRewriteRuleAddHandler.java @@ -0,0 +1,38 @@ +/* + * 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.adapter.extension; + +import org.jboss.as.controller.AbstractAddStepHandler; +import org.jboss.as.controller.AttributeDefinition; +import org.jboss.as.controller.OperationContext; +import org.jboss.as.controller.OperationFailedException; +import org.jboss.dmr.ModelNode; + +public class RedirectRewriteRuleAddHandler extends AbstractAddStepHandler { + + public RedirectRewriteRuleAddHandler(AttributeDefinition... attributes) { + super(attributes); + } + + @Override + protected void performRuntime(OperationContext context, ModelNode operation, ModelNode model) throws OperationFailedException { + KeycloakAdapterConfigService ckService = KeycloakAdapterConfigService.getInstance(); + ckService.addRedirectRewriteRule(operation, context.resolveExpressions(model)); + } + +} diff --git a/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/RedirectRewriteRuleReadWriteAttributeHandler.java b/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/RedirectRewriteRuleReadWriteAttributeHandler.java new file mode 100644 index 0000000000..171e7555bc --- /dev/null +++ b/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/RedirectRewriteRuleReadWriteAttributeHandler.java @@ -0,0 +1,44 @@ +/* + * Copyright 2016 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.keycloak.subsystem.adapter.extension; + +import org.jboss.as.controller.AbstractWriteAttributeHandler; +import org.jboss.as.controller.OperationContext; +import org.jboss.as.controller.OperationFailedException; +import org.jboss.dmr.ModelNode; + +public class RedirectRewriteRuleReadWriteAttributeHandler extends AbstractWriteAttributeHandler { + + @Override + protected boolean applyUpdateToRuntime(OperationContext context, ModelNode operation, String attributeName, + ModelNode resolvedValue, ModelNode currentValue, AbstractWriteAttributeHandler.HandbackHolder hh) throws OperationFailedException { + + KeycloakAdapterConfigService ckService = KeycloakAdapterConfigService.getInstance(); + ckService.updateRedirectRewriteRule(operation, attributeName, resolvedValue); + + hh.setHandback(ckService); + + return false; + } + + @Override + protected void revertUpdateToRuntime(OperationContext context, ModelNode operation, String attributeName, + ModelNode valueToRestore, ModelNode valueToRevert, KeycloakAdapterConfigService ckService) throws OperationFailedException { + ckService.updateRedirectRewriteRule(operation, attributeName, valueToRestore); + } + +} diff --git a/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/RedirectRewriteRuleRemoveHandler.java b/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/RedirectRewriteRuleRemoveHandler.java new file mode 100644 index 0000000000..de17c9666e --- /dev/null +++ b/adapters/oidc/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/RedirectRewriteRuleRemoveHandler.java @@ -0,0 +1,36 @@ +/* + * 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.adapter.extension; + +import org.jboss.as.controller.AbstractRemoveStepHandler; +import org.jboss.as.controller.OperationContext; +import org.jboss.as.controller.OperationFailedException; +import org.jboss.dmr.ModelNode; + +public class RedirectRewriteRuleRemoveHandler extends AbstractRemoveStepHandler { + + public static RedirectRewriteRuleRemoveHandler INSTANCE = new RedirectRewriteRuleRemoveHandler(); + + private RedirectRewriteRuleRemoveHandler() {} + + @Override + protected void performRuntime(OperationContext context, ModelNode operation, ModelNode model) throws OperationFailedException { + KeycloakAdapterConfigService ckService = KeycloakAdapterConfigService.getInstance(); + ckService.removeRedirectRewriteRule(operation); + } + +} diff --git a/adapters/oidc/wildfly/wildfly-subsystem/src/main/resources/org/keycloak/subsystem/adapter/extension/LocalDescriptions.properties b/adapters/oidc/wildfly/wildfly-subsystem/src/main/resources/org/keycloak/subsystem/adapter/extension/LocalDescriptions.properties index 1df59796a2..c9cea77787 100755 --- a/adapters/oidc/wildfly/wildfly-subsystem/src/main/resources/org/keycloak/subsystem/adapter/extension/LocalDescriptions.properties +++ b/adapters/oidc/wildfly/wildfly-subsystem/src/main/resources/org/keycloak/subsystem/adapter/extension/LocalDescriptions.properties @@ -65,6 +65,7 @@ keycloak.secure-deployment.connection-pool-size=Connection pool size for the cli keycloak.secure-deployment.resource=Application name keycloak.secure-deployment.use-resource-role-mappings=Use resource level permissions from token keycloak.secure-deployment.credentials=Adapter credentials +keycloak.secure-deployment.redirect-rewrite-rule=Apply a rewrite rule for the redirect URI keycloak.secure-deployment.bearer-only=Bearer Token Auth only keycloak.secure-deployment.enable-basic-auth=Enable Basic Authentication keycloak.secure-deployment.public-client=Public client @@ -94,4 +95,9 @@ keycloak.secure-deployment.credential=Credential value keycloak.credential=Credential keycloak.credential.value=Credential value keycloak.credential.add=Credential add -keycloak.credential.remove=Credential remove \ No newline at end of file +keycloak.credential.remove=Credential remove + +keycloak.redirect-rewrite-rule=redirect-rewrite-rule +keycloak.redirect-rewrite-rule.value=redirect-rewrite-rule value +keycloak.redirect-rewrite-rule.add=redirect-rewrite-rule add +keycloak.redirect-rewrite-rule.remove=redirect-rewrite-rule remove \ No newline at end of file diff --git a/adapters/oidc/wildfly/wildfly-subsystem/src/main/resources/schema/wildfly-keycloak_1_1.xsd b/adapters/oidc/wildfly/wildfly-subsystem/src/main/resources/schema/wildfly-keycloak_1_1.xsd index 604e6ac62d..d8f5bc3d74 100755 --- a/adapters/oidc/wildfly/wildfly-subsystem/src/main/resources/schema/wildfly-keycloak_1_1.xsd +++ b/adapters/oidc/wildfly/wildfly-subsystem/src/main/resources/schema/wildfly-keycloak_1_1.xsd @@ -101,6 +101,7 @@ + @@ -127,4 +128,10 @@ + + + + + + diff --git a/adapters/oidc/wildfly/wildfly-subsystem/src/test/resources/org/keycloak/subsystem/adapter/extension/keycloak-1.1.xml b/adapters/oidc/wildfly/wildfly-subsystem/src/test/resources/org/keycloak/subsystem/adapter/extension/keycloak-1.1.xml index 3dcb61d4f0..246d76855f 100755 --- a/adapters/oidc/wildfly/wildfly-subsystem/src/test/resources/org/keycloak/subsystem/adapter/extension/keycloak-1.1.xml +++ b/adapters/oidc/wildfly/wildfly-subsystem/src/test/resources/org/keycloak/subsystem/adapter/extension/keycloak-1.1.xml @@ -53,6 +53,7 @@ http://localhost:8080/auth EXTERNAL 0aa31d98-e0aa-404c-b6e0-e771dba1e798 + api/$1/ master @@ -66,5 +67,6 @@ /tmp/keystore.jks + /api/$1/ \ No newline at end of file diff --git a/adapters/pom.xml b/adapters/pom.xml index 847b84eaff..21e23b987a 100755 --- a/adapters/pom.xml +++ b/adapters/pom.xml @@ -20,7 +20,7 @@ keycloak-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../pom.xml Keycloak Adapters diff --git a/adapters/saml/as7-eap6/adapter/pom.xml b/adapters/saml/as7-eap6/adapter/pom.xml index 43cfcb1816..dd8aff5317 100755 --- a/adapters/saml/as7-eap6/adapter/pom.xml +++ b/adapters/saml/as7-eap6/adapter/pom.xml @@ -21,7 +21,7 @@ keycloak-saml-eap-integration-pom org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../pom.xml 4.0.0 diff --git a/adapters/saml/as7-eap6/pom.xml b/adapters/saml/as7-eap6/pom.xml index 7233dfb383..66bdf0d5ad 100755 --- a/adapters/saml/as7-eap6/pom.xml +++ b/adapters/saml/as7-eap6/pom.xml @@ -20,7 +20,7 @@ keycloak-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../../../pom.xml Keycloak SAML EAP Integration diff --git a/adapters/saml/as7-eap6/subsystem/pom.xml b/adapters/saml/as7-eap6/subsystem/pom.xml index e89cbd7753..d3c2a5d9a4 100755 --- a/adapters/saml/as7-eap6/subsystem/pom.xml +++ b/adapters/saml/as7-eap6/subsystem/pom.xml @@ -21,7 +21,7 @@ org.keycloak keycloak-saml-eap-integration-pom - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../pom.xml diff --git a/adapters/saml/as7-eap6/subsystem/src/main/java/org/keycloak/subsystem/saml/as7/xml/FormattingXMLStreamWriter.java b/adapters/saml/as7-eap6/subsystem/src/main/java/org/keycloak/subsystem/saml/as7/xml/FormattingXMLStreamWriter.java index 0d566597b3..2334a63ad7 100644 --- a/adapters/saml/as7-eap6/subsystem/src/main/java/org/keycloak/subsystem/saml/as7/xml/FormattingXMLStreamWriter.java +++ b/adapters/saml/as7-eap6/subsystem/src/main/java/org/keycloak/subsystem/saml/as7/xml/FormattingXMLStreamWriter.java @@ -81,7 +81,7 @@ public final class FormattingXMLStreamWriter implements XMLExtendedStreamWriter, public void writeStartElement(final String localName) throws XMLStreamException { ArrayDeque namespaces = unspecifiedNamespaces; String namespace = namespaces.getFirst(); - if (namespace != NO_NAMESPACE) { + if (namespace == null ? NO_NAMESPACE != null : ! namespace.equals(NO_NAMESPACE)) { writeStartElement(namespace, localName); return; } @@ -140,9 +140,9 @@ public final class FormattingXMLStreamWriter implements XMLExtendedStreamWriter, attrQueue.add(new ArgRunnable() { public void run(int arg) throws XMLStreamException { if (arg == 0) { - delegate.writeStartElement(prefix, namespaceURI, localName); + delegate.writeStartElement(prefix, localName, namespaceURI); } else { - delegate.writeEmptyElement(prefix, namespaceURI, localName); + delegate.writeEmptyElement(prefix, localName, namespaceURI); } } }); @@ -165,14 +165,14 @@ public final class FormattingXMLStreamWriter implements XMLExtendedStreamWriter, runAttrQueue(); nl(); indent(); - delegate.writeEmptyElement(prefix, namespaceURI, localName); + delegate.writeEmptyElement(prefix, localName, namespaceURI); state = END_ELEMENT; } @Override public void writeEmptyElement(final String localName) throws XMLStreamException { String namespace = unspecifiedNamespaces.getFirst(); - if (namespace != NO_NAMESPACE) { + if (namespace == null ? NO_NAMESPACE != null : ! namespace.equals(NO_NAMESPACE)) { writeEmptyElement(namespace, localName); return; } diff --git a/adapters/saml/core-public/pom.xml b/adapters/saml/core-public/pom.xml index e56da0ec07..29e35a9761 100755 --- a/adapters/saml/core-public/pom.xml +++ b/adapters/saml/core-public/pom.xml @@ -21,7 +21,7 @@ keycloak-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../../../pom.xml 4.0.0 diff --git a/adapters/saml/core-public/src/main/java/org/keycloak/adapters/saml/SamlAuthenticationError.java b/adapters/saml/core-public/src/main/java/org/keycloak/adapters/saml/SamlAuthenticationError.java index 29bbbfa566..f44534d542 100755 --- a/adapters/saml/core-public/src/main/java/org/keycloak/adapters/saml/SamlAuthenticationError.java +++ b/adapters/saml/core-public/src/main/java/org/keycloak/adapters/saml/SamlAuthenticationError.java @@ -18,7 +18,10 @@ package org.keycloak.adapters.saml; import org.keycloak.adapters.spi.AuthenticationError; +import org.keycloak.dom.saml.v2.protocol.StatusCodeType; import org.keycloak.dom.saml.v2.protocol.StatusResponseType; +import org.keycloak.saml.common.constants.JBossSAMLURIConstants; +import java.util.Objects; /** * Object that describes the SAML error that happened. @@ -27,6 +30,7 @@ import org.keycloak.dom.saml.v2.protocol.StatusResponseType; * @version $Revision: 1 $ */ public class SamlAuthenticationError implements AuthenticationError { + public static enum Reason { EXTRACTION_FAILURE, INVALID_SIGNATURE, @@ -59,7 +63,18 @@ public class SamlAuthenticationError implements AuthenticationError { @Override public String toString() { - return "SamlAuthenticationError [reason=" + reason + ", status=" + status + "]"; + return "SamlAuthenticationError [reason=" + reason + ", status=" + + ((status == null || status.getStatus() == null) ? "UNKNOWN" : extractStatusCode(status.getStatus().getStatusCode())) + + "]"; } + private String extractStatusCode(StatusCodeType statusCode) { + if (statusCode == null || statusCode.getValue() == null) { + return "UNKNOWN"; + } + if (Objects.equals(JBossSAMLURIConstants.STATUS_RESPONDER.get(), statusCode.getValue().toString())) { + return extractStatusCode(statusCode.getStatusCode()); + } + return statusCode.getValue().toString(); + } } diff --git a/adapters/saml/core/pom.xml b/adapters/saml/core/pom.xml index 244e69ed13..be1e686dc0 100755 --- a/adapters/saml/core/pom.xml +++ b/adapters/saml/core/pom.xml @@ -21,7 +21,7 @@ keycloak-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../../../pom.xml 4.0.0 diff --git a/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/descriptor/parsers/SamlDescriptorIDPKeysExtractor.java b/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/descriptor/parsers/SamlDescriptorIDPKeysExtractor.java index 0858675c50..b8d5d6658a 100644 --- a/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/descriptor/parsers/SamlDescriptorIDPKeysExtractor.java +++ b/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/descriptor/parsers/SamlDescriptorIDPKeysExtractor.java @@ -34,6 +34,7 @@ import org.keycloak.common.util.MultivaluedHashMap; import org.keycloak.saml.common.constants.JBossSAMLConstants; import org.keycloak.saml.common.constants.JBossSAMLURIConstants; import org.keycloak.saml.common.exceptions.ParsingException; +import org.keycloak.saml.common.util.DocumentUtil; import org.keycloak.saml.processing.core.util.NamespaceContext; import org.w3c.dom.Document; import org.w3c.dom.Element; @@ -65,9 +66,7 @@ public class SamlDescriptorIDPKeysExtractor { MultivaluedHashMap res = new MultivaluedHashMap<>(); try { - DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); - factory.setNamespaceAware(true); - DocumentBuilder builder = factory.newDocumentBuilder(); + DocumentBuilder builder = DocumentUtil.getDocumentBuilder(); Document doc = builder.parse(stream); XPathExpression expr = xpath.compile("/m:EntitiesDescriptor/m:EntityDescriptor/m:IDPSSODescriptor/m:KeyDescriptor"); diff --git a/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/profile/AbstractSamlAuthenticationHandler.java b/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/profile/AbstractSamlAuthenticationHandler.java index 08ce4a988b..2b40a424e8 100644 --- a/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/profile/AbstractSamlAuthenticationHandler.java +++ b/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/profile/AbstractSamlAuthenticationHandler.java @@ -407,8 +407,8 @@ public abstract class AbstractSamlAuthenticationHandler implements SamlAuthentic SubjectType subject = assertion.getSubject(); SubjectType.STSubType subType = subject.getSubType(); - NameIDType subjectNameID = (NameIDType) subType.getBaseID(); - String principalName = subjectNameID.getValue(); + NameIDType subjectNameID = subType == null ? null : (NameIDType) subType.getBaseID(); + String principalName = subjectNameID == null ? null : subjectNameID.getValue(); final Set roles = new HashSet<>(); MultivaluedHashMap attributes = new MultivaluedHashMap<>(); @@ -473,7 +473,7 @@ public abstract class AbstractSamlAuthenticationHandler implements SamlAuthentic } - URI nameFormat = subjectNameID.getFormat(); + URI nameFormat = subjectNameID == null ? null : subjectNameID.getFormat(); String nameFormatString = nameFormat == null ? JBossSAMLURIConstants.NAMEID_FORMAT_UNSPECIFIED.get() : nameFormat.toString(); final SamlPrincipal principal = new SamlPrincipal(assertion, principalName, principalName, nameFormatString, attributes, friendlyAttributes); String index = authn == null ? null : authn.getSessionIndex(); diff --git a/adapters/saml/jetty/jetty-core/pom.xml b/adapters/saml/jetty/jetty-core/pom.xml index c6a7cc0c7d..316cb7e6b1 100755 --- a/adapters/saml/jetty/jetty-core/pom.xml +++ b/adapters/saml/jetty/jetty-core/pom.xml @@ -21,7 +21,7 @@ keycloak-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../../../../pom.xml 4.0.0 diff --git a/adapters/saml/jetty/jetty8.1/pom.xml b/adapters/saml/jetty/jetty8.1/pom.xml index 62ac8f399e..ca569821b3 100755 --- a/adapters/saml/jetty/jetty8.1/pom.xml +++ b/adapters/saml/jetty/jetty8.1/pom.xml @@ -21,7 +21,7 @@ keycloak-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../../../../pom.xml 4.0.0 diff --git a/adapters/saml/jetty/jetty9.1/pom.xml b/adapters/saml/jetty/jetty9.1/pom.xml index f613b6b218..c01af403a2 100755 --- a/adapters/saml/jetty/jetty9.1/pom.xml +++ b/adapters/saml/jetty/jetty9.1/pom.xml @@ -21,7 +21,7 @@ keycloak-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../../../../pom.xml 4.0.0 diff --git a/adapters/saml/jetty/jetty9.2/pom.xml b/adapters/saml/jetty/jetty9.2/pom.xml index d66b670571..88bf685595 100755 --- a/adapters/saml/jetty/jetty9.2/pom.xml +++ b/adapters/saml/jetty/jetty9.2/pom.xml @@ -21,7 +21,7 @@ keycloak-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../../../../pom.xml 4.0.0 diff --git a/adapters/saml/jetty/jetty9.3/pom.xml b/adapters/saml/jetty/jetty9.3/pom.xml index 8104214f98..d34f83e0ed 100644 --- a/adapters/saml/jetty/jetty9.3/pom.xml +++ b/adapters/saml/jetty/jetty9.3/pom.xml @@ -21,7 +21,7 @@ keycloak-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../../../../pom.xml 4.0.0 diff --git a/adapters/saml/jetty/jetty9.4/pom.xml b/adapters/saml/jetty/jetty9.4/pom.xml index 23b05d08c3..cee8d453d9 100644 --- a/adapters/saml/jetty/jetty9.4/pom.xml +++ b/adapters/saml/jetty/jetty9.4/pom.xml @@ -21,7 +21,7 @@ keycloak-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../../../../pom.xml 4.0.0 diff --git a/adapters/saml/jetty/pom.xml b/adapters/saml/jetty/pom.xml index 621ae3c2f9..2f53996d71 100755 --- a/adapters/saml/jetty/pom.xml +++ b/adapters/saml/jetty/pom.xml @@ -20,7 +20,7 @@ keycloak-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../../../pom.xml Keycloak SAML Jetty Integration diff --git a/adapters/saml/pom.xml b/adapters/saml/pom.xml index 18e32cf0a2..4ae655c4e9 100755 --- a/adapters/saml/pom.xml +++ b/adapters/saml/pom.xml @@ -20,7 +20,7 @@ keycloak-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../../pom.xml Keycloak SAML Client Adapter Modules diff --git a/adapters/saml/servlet-filter/pom.xml b/adapters/saml/servlet-filter/pom.xml index c7688b61b7..fd7f9393ba 100755 --- a/adapters/saml/servlet-filter/pom.xml +++ b/adapters/saml/servlet-filter/pom.xml @@ -21,7 +21,7 @@ keycloak-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../../../pom.xml 4.0.0 diff --git a/adapters/saml/tomcat/pom.xml b/adapters/saml/tomcat/pom.xml index 3f76eba91e..0911796e01 100755 --- a/adapters/saml/tomcat/pom.xml +++ b/adapters/saml/tomcat/pom.xml @@ -20,7 +20,7 @@ keycloak-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../../../pom.xml Keycloak SAML Tomcat Integration diff --git a/adapters/saml/tomcat/tomcat-core/pom.xml b/adapters/saml/tomcat/tomcat-core/pom.xml index 466b6b5aee..e493969a3f 100755 --- a/adapters/saml/tomcat/tomcat-core/pom.xml +++ b/adapters/saml/tomcat/tomcat-core/pom.xml @@ -21,7 +21,7 @@ keycloak-saml-tomcat-integration-pom org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../pom.xml 4.0.0 diff --git a/adapters/saml/tomcat/tomcat6/pom.xml b/adapters/saml/tomcat/tomcat6/pom.xml index fb55eccc66..12ad22bb0a 100755 --- a/adapters/saml/tomcat/tomcat6/pom.xml +++ b/adapters/saml/tomcat/tomcat6/pom.xml @@ -21,7 +21,7 @@ keycloak-saml-tomcat-integration-pom org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../pom.xml 4.0.0 diff --git a/adapters/saml/tomcat/tomcat7/pom.xml b/adapters/saml/tomcat/tomcat7/pom.xml index ef00d9b6dd..ff59bfc0ef 100755 --- a/adapters/saml/tomcat/tomcat7/pom.xml +++ b/adapters/saml/tomcat/tomcat7/pom.xml @@ -21,7 +21,7 @@ keycloak-saml-tomcat-integration-pom org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../pom.xml 4.0.0 diff --git a/adapters/saml/tomcat/tomcat8/pom.xml b/adapters/saml/tomcat/tomcat8/pom.xml index b87505f9d6..835e4d52d2 100755 --- a/adapters/saml/tomcat/tomcat8/pom.xml +++ b/adapters/saml/tomcat/tomcat8/pom.xml @@ -21,7 +21,7 @@ keycloak-saml-tomcat-integration-pom org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../pom.xml 4.0.0 diff --git a/adapters/saml/tomcat/tomcat8/src/main/java/org/keycloak/adapters/saml/tomcat/SamlAuthenticatorValve.java b/adapters/saml/tomcat/tomcat8/src/main/java/org/keycloak/adapters/saml/tomcat/SamlAuthenticatorValve.java index eef8c6a0ba..caf1bf98a7 100755 --- a/adapters/saml/tomcat/tomcat8/src/main/java/org/keycloak/adapters/saml/tomcat/SamlAuthenticatorValve.java +++ b/adapters/saml/tomcat/tomcat8/src/main/java/org/keycloak/adapters/saml/tomcat/SamlAuthenticatorValve.java @@ -41,10 +41,20 @@ import java.util.List; * @version $Revision: 1 $ */ public class SamlAuthenticatorValve extends AbstractSamlAuthenticatorValve { + /** + * Method called by Tomcat < 8.5.5 + */ public boolean authenticate(Request request, HttpServletResponse response) throws IOException { return authenticateInternal(request, response, request.getContext().getLoginConfig()); } + /** + * Method called by Tomcat >= 8.5.5 + */ + protected boolean doAuthenticate(Request request, HttpServletResponse response) throws IOException { + return this.authenticate(request, response); + } + @Override protected boolean forwardToErrorPageInternal(Request request, HttpServletResponse response, Object loginConfig) throws IOException { if (loginConfig == null) return false; diff --git a/adapters/saml/undertow/pom.xml b/adapters/saml/undertow/pom.xml index 40757160f7..b314f7efaf 100755 --- a/adapters/saml/undertow/pom.xml +++ b/adapters/saml/undertow/pom.xml @@ -21,7 +21,7 @@ keycloak-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../../../pom.xml 4.0.0 diff --git a/adapters/saml/wildfly-elytron/pom.xml b/adapters/saml/wildfly-elytron/pom.xml index 8d6df2e636..4161b092e9 100755 --- a/adapters/saml/wildfly-elytron/pom.xml +++ b/adapters/saml/wildfly-elytron/pom.xml @@ -21,7 +21,7 @@ keycloak-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../../../pom.xml 4.0.0 diff --git a/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/ElytronHttpFacade.java b/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/ElytronHttpFacade.java index 88e96f8bd3..68c6922fae 100644 --- a/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/ElytronHttpFacade.java +++ b/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/ElytronHttpFacade.java @@ -47,10 +47,8 @@ import org.wildfly.security.auth.callback.AnonymousAuthorizationCallback; import org.wildfly.security.auth.callback.AuthenticationCompleteCallback; import org.wildfly.security.auth.callback.SecurityIdentityCallback; import org.wildfly.security.auth.server.SecurityIdentity; -import org.wildfly.security.http.HttpAuthenticationException; import org.wildfly.security.http.HttpScope; import org.wildfly.security.http.HttpServerCookie; -import org.wildfly.security.http.HttpServerMechanismsResponder; import org.wildfly.security.http.HttpServerRequest; import org.wildfly.security.http.HttpServerResponse; import org.wildfly.security.http.Scope; @@ -87,11 +85,14 @@ class ElytronHttpFacade implements HttpFacade { void authenticationComplete() { this.securityIdentity = SecurityIdentityUtil.authorize(this.callbackHandler, samlSession.getPrincipal()); - this.request.authenticationComplete(response -> { - if (!restored) { - responseConsumer.accept(response); - } - }, () -> ((ElytronTokeStore) sessionStore).logout(true)); + + if (this.securityIdentity != null) { + this.request.authenticationComplete(response -> { + if (!restored) { + responseConsumer.accept(response); + } + }, () -> ((ElytronTokeStore) sessionStore).logout(true)); + } } void authenticationCompleteAnonymous() { diff --git a/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/KeycloakHttpServerAuthenticationMechanism.java b/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/KeycloakHttpServerAuthenticationMechanism.java index 9fce501d93..1f71bae329 100644 --- a/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/KeycloakHttpServerAuthenticationMechanism.java +++ b/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/KeycloakHttpServerAuthenticationMechanism.java @@ -65,7 +65,7 @@ class KeycloakHttpServerAuthenticationMechanism implements HttpServerAuthenticat SamlDeploymentContext deploymentContext = getDeploymentContext(request); if (deploymentContext == null) { - LOGGER.debugf("Ignoring request for path [%s] from mechanism [%s]. No deployment context found.", request.getRequestURI()); + LOGGER.debugf("Ignoring request for path [%s] from mechanism [%s]. No deployment context found.", request.getRequestURI(), getMechanismName()); request.noAuthenticationInProgress(); return; } diff --git a/adapters/saml/wildfly/pom.xml b/adapters/saml/wildfly/pom.xml index 430696008d..43108bb00f 100755 --- a/adapters/saml/wildfly/pom.xml +++ b/adapters/saml/wildfly/pom.xml @@ -20,7 +20,7 @@ keycloak-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../../../pom.xml Keycloak SAML Wildfly Integration diff --git a/adapters/saml/wildfly/wildfly-adapter/pom.xml b/adapters/saml/wildfly/wildfly-adapter/pom.xml index 1ba105731a..3be5e7e4c0 100755 --- a/adapters/saml/wildfly/wildfly-adapter/pom.xml +++ b/adapters/saml/wildfly/wildfly-adapter/pom.xml @@ -21,7 +21,7 @@ keycloak-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../../../../pom.xml 4.0.0 diff --git a/adapters/saml/wildfly/wildfly-subsystem/pom.xml b/adapters/saml/wildfly/wildfly-subsystem/pom.xml index 7195cf4ba4..acaf7f160f 100755 --- a/adapters/saml/wildfly/wildfly-subsystem/pom.xml +++ b/adapters/saml/wildfly/wildfly-subsystem/pom.xml @@ -21,7 +21,7 @@ org.keycloak keycloak-parent - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../../../../pom.xml diff --git a/adapters/spi/adapter-spi/pom.xml b/adapters/spi/adapter-spi/pom.xml index 23963fe544..0145009a5f 100755 --- a/adapters/spi/adapter-spi/pom.xml +++ b/adapters/spi/adapter-spi/pom.xml @@ -21,7 +21,7 @@ keycloak-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../../../pom.xml 4.0.0 diff --git a/adapters/spi/jboss-adapter-core/pom.xml b/adapters/spi/jboss-adapter-core/pom.xml index ccd687d4b9..41be1d3d19 100755 --- a/adapters/spi/jboss-adapter-core/pom.xml +++ b/adapters/spi/jboss-adapter-core/pom.xml @@ -21,7 +21,7 @@ keycloak-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../../../pom.xml 4.0.0 diff --git a/adapters/spi/jetty-adapter-spi/pom.xml b/adapters/spi/jetty-adapter-spi/pom.xml index 0841616fe6..03adfdf126 100755 --- a/adapters/spi/jetty-adapter-spi/pom.xml +++ b/adapters/spi/jetty-adapter-spi/pom.xml @@ -21,7 +21,7 @@ keycloak-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../../../pom.xml 4.0.0 diff --git a/adapters/spi/pom.xml b/adapters/spi/pom.xml index ce656153d0..45805179b1 100755 --- a/adapters/spi/pom.xml +++ b/adapters/spi/pom.xml @@ -20,7 +20,7 @@ keycloak-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../../pom.xml Keycloak Client Adapter SPI Modules diff --git a/adapters/spi/servlet-adapter-spi/pom.xml b/adapters/spi/servlet-adapter-spi/pom.xml index b54d2665a1..c9228b6e7f 100755 --- a/adapters/spi/servlet-adapter-spi/pom.xml +++ b/adapters/spi/servlet-adapter-spi/pom.xml @@ -21,7 +21,7 @@ keycloak-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../../../pom.xml 4.0.0 diff --git a/adapters/spi/tomcat-adapter-spi/pom.xml b/adapters/spi/tomcat-adapter-spi/pom.xml index ba39fa0b78..dcf55e5f50 100755 --- a/adapters/spi/tomcat-adapter-spi/pom.xml +++ b/adapters/spi/tomcat-adapter-spi/pom.xml @@ -21,7 +21,7 @@ keycloak-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../../../pom.xml 4.0.0 diff --git a/adapters/spi/undertow-adapter-spi/pom.xml b/adapters/spi/undertow-adapter-spi/pom.xml index 9256187d82..0f04bdb14d 100755 --- a/adapters/spi/undertow-adapter-spi/pom.xml +++ b/adapters/spi/undertow-adapter-spi/pom.xml @@ -21,7 +21,7 @@ keycloak-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../../../pom.xml 4.0.0 diff --git a/authz/client/pom.xml b/authz/client/pom.xml index c1c063103c..c45476a6e0 100644 --- a/authz/client/pom.xml +++ b/authz/client/pom.xml @@ -7,7 +7,7 @@ org.keycloak keycloak-authz-parent - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../pom.xml diff --git a/authz/policy/common/pom.xml b/authz/policy/common/pom.xml index 7e309a5b63..193dcf49af 100644 --- a/authz/policy/common/pom.xml +++ b/authz/policy/common/pom.xml @@ -25,7 +25,7 @@ org.keycloak keycloak-authz-provider-parent - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../pom.xml diff --git a/authz/policy/drools/pom.xml b/authz/policy/drools/pom.xml index 54fdfe9343..eb6674110d 100644 --- a/authz/policy/drools/pom.xml +++ b/authz/policy/drools/pom.xml @@ -7,7 +7,7 @@ org.keycloak keycloak-authz-provider-parent - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../pom.xml diff --git a/authz/policy/pom.xml b/authz/policy/pom.xml index 3c064d4432..f119652c85 100644 --- a/authz/policy/pom.xml +++ b/authz/policy/pom.xml @@ -7,7 +7,7 @@ org.keycloak keycloak-authz-parent - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../pom.xml diff --git a/authz/pom.xml b/authz/pom.xml index 6121b2386d..36c54baf5e 100644 --- a/authz/pom.xml +++ b/authz/pom.xml @@ -7,7 +7,7 @@ org.keycloak keycloak-parent - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../pom.xml diff --git a/boms/adapter/pom.xml b/boms/adapter/pom.xml index 46c6272e08..47333617df 100644 --- a/boms/adapter/pom.xml +++ b/boms/adapter/pom.xml @@ -22,7 +22,7 @@ org.keycloak.bom keycloak-bom-parent - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT org.keycloak.bom @@ -37,97 +37,97 @@ org.keycloak keycloak-core - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT org.keycloak keycloak-adapter-core - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT org.keycloak keycloak-adapter-spi - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT org.keycloak keycloak-wildfly-adapter-dist - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT org.keycloak keycloak-saml-adapter-core - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT org.keycloak keycloak-saml-adapter-api-public - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT org.keycloak keycloak-tomcat8-adapter - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT org.keycloak keycloak-tomcat7-adapter - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT org.keycloak keycloak-tomcat6-adapter - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT org.keycloak keycloak-jetty81-adapter - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT org.keycloak keycloak-jetty91-adapter - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT org.keycloak keycloak-jetty92-adapter - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT org.keycloak keycloak-jetty93-adapter - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT org.keycloak keycloak-undertow-adapter - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT org.keycloak keycloak-spring-boot-adapter - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT org.keycloak spring-boot-container-bundle - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT org.keycloak keycloak-spring-security-adapter - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT org.keycloak keycloak-spring-boot-starter - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT org.keycloak keycloak-authz-client - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT diff --git a/boms/pom.xml b/boms/pom.xml index a5f75a8a4a..bbbc0dbaf5 100644 --- a/boms/pom.xml +++ b/boms/pom.xml @@ -26,7 +26,7 @@ org.keycloak.bom keycloak-bom-parent - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT pom diff --git a/boms/spi/pom.xml b/boms/spi/pom.xml index 555ca375b7..9d970d1df0 100644 --- a/boms/spi/pom.xml +++ b/boms/spi/pom.xml @@ -23,7 +23,7 @@ org.keycloak.bom keycloak-bom-parent - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT org.keycloak.bom @@ -38,12 +38,12 @@ org.keycloak keycloak-core - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT org.keycloak keycloak-server-spi - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT diff --git a/common/pom.xml b/common/pom.xml index 1039ff884f..70476b6743 100755 --- a/common/pom.xml +++ b/common/pom.xml @@ -21,7 +21,7 @@ keycloak-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../pom.xml 4.0.0 diff --git a/common/src/main/java/org/keycloak/common/Profile.java b/common/src/main/java/org/keycloak/common/Profile.java index 91b0a806cc..7f97e55b8a 100755 --- a/common/src/main/java/org/keycloak/common/Profile.java +++ b/common/src/main/java/org/keycloak/common/Profile.java @@ -35,13 +35,13 @@ import java.util.Set; public class Profile { public enum Feature { - AUTHORIZATION, IMPERSONATION, SCRIPTS + AUTHORIZATION, IMPERSONATION, SCRIPTS, DOCKER } private enum ProfileValue { - PRODUCT(Feature.AUTHORIZATION, Feature.SCRIPTS), + PRODUCT(Feature.AUTHORIZATION, Feature.SCRIPTS, Feature.DOCKER), PREVIEW, - COMMUNITY; + COMMUNITY(Feature.DOCKER); private List disabled; diff --git a/common/src/main/java/org/keycloak/common/Version.java b/common/src/main/java/org/keycloak/common/Version.java index 862ccd2def..75fbe92ec9 100755 --- a/common/src/main/java/org/keycloak/common/Version.java +++ b/common/src/main/java/org/keycloak/common/Version.java @@ -45,6 +45,10 @@ public class Version { Version.VERSION = props.getProperty("version"); Version.BUILD_TIME = props.getProperty("build-time"); Version.RESOURCES_VERSION = Version.VERSION.toLowerCase(); + + if (Version.RESOURCES_VERSION.endsWith("-snapshot")) { + Version.RESOURCES_VERSION = Version.RESOURCES_VERSION.replace("-snapshot", "-" + Version.BUILD_TIME.replace(" ", "").replace(":", "").replace("-", "")); + } } catch (IOException e) { Version.VERSION = Version.UNKNOWN; Version.BUILD_TIME = Version.UNKNOWN; diff --git a/common/src/main/java/org/keycloak/common/util/Encode.java b/common/src/main/java/org/keycloak/common/util/Encode.java index 63b8f3653f..b19536240f 100755 --- a/common/src/main/java/org/keycloak/common/util/Encode.java +++ b/common/src/main/java/org/keycloak/common/util/Encode.java @@ -24,6 +24,7 @@ import java.nio.ByteBuffer; import java.nio.charset.CharacterCodingException; import java.nio.charset.Charset; import java.nio.charset.CharsetDecoder; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -36,7 +37,7 @@ import java.util.regex.Pattern; */ public class Encode { - private static final String UTF_8 = "UTF-8"; + private static final String UTF_8 = StandardCharsets.UTF_8.name(); private static final Pattern PARAM_REPLACEMENT = Pattern.compile("_resteasy_uri_parameter"); @@ -84,9 +85,7 @@ public class Encode case '@': continue; } - StringBuffer sb = new StringBuffer(); - sb.append((char) i); - pathEncoding[i] = URLEncoder.encode(sb.toString()); + pathEncoding[i] = URLEncoder.encode(String.valueOf((char) i)); } pathEncoding[' '] = "%20"; System.arraycopy(pathEncoding, 0, matrixParameterEncoding, 0, pathEncoding.length); @@ -119,9 +118,7 @@ public class Encode queryNameValueEncoding[i] = "+"; continue; } - StringBuffer sb = new StringBuffer(); - sb.append((char) i); - queryNameValueEncoding[i] = URLEncoder.encode(sb.toString()); + queryNameValueEncoding[i] = URLEncoder.encode(String.valueOf((char) i)); } /* @@ -159,9 +156,7 @@ public class Encode queryStringEncoding[i] = "%20"; continue; } - StringBuffer sb = new StringBuffer(); - sb.append((char) i); - queryStringEncoding[i] = URLEncoder.encode(sb.toString()); + queryStringEncoding[i] = URLEncoder.encode(String.valueOf((char) i)); } } @@ -194,7 +189,7 @@ public class Encode */ public static String encodeFragment(String value) { - return encodeValue(value, queryNameValueEncoding); + return encodeValue(value, queryStringEncoding); } /** @@ -221,18 +216,19 @@ public class Encode public static String decodePath(String path) { Matcher matcher = encodedCharsMulti.matcher(path); - StringBuffer buf = new StringBuffer(); + int start=0; + StringBuilder builder = new StringBuilder(); CharsetDecoder decoder = Charset.forName(UTF_8).newDecoder(); while (matcher.find()) { + builder.append(path, start, matcher.start()); decoder.reset(); String decoded = decodeBytes(matcher.group(1), decoder); - decoded = decoded.replace("\\", "\\\\"); - decoded = decoded.replace("$", "\\$"); - matcher.appendReplacement(buf, decoded); + builder.append(decoded); + start = matcher.end(); } - matcher.appendTail(buf); - return buf.toString(); + builder.append(path, start, path.length()); + return builder.toString(); } private static String decodeBytes(String enc, CharsetDecoder decoder) @@ -264,7 +260,7 @@ public class Encode public static String encodeNonCodes(String string) { Matcher matcher = nonCodes.matcher(string); - StringBuffer buf = new StringBuffer(); + StringBuilder builder = new StringBuilder(); // FYI: we do not use the no-arg matcher.find() @@ -276,29 +272,32 @@ public class Encode while (matcher.find(idx)) { int start = matcher.start(); - buf.append(string.substring(idx, start)); - buf.append("%25"); + builder.append(string.substring(idx, start)); + builder.append("%25"); idx = start + 1; } - buf.append(string.substring(idx)); - return buf.toString(); + builder.append(string.substring(idx)); + return builder.toString(); } - private static boolean savePathParams(String segment, StringBuffer newSegment, List params) + public static boolean savePathParams(String segment, StringBuilder newSegment, List params) { boolean foundParam = false; // Regular expressions can have '{' and '}' characters. Replace them to do match segment = PathHelper.replaceEnclosedCurlyBraces(segment); Matcher matcher = PathHelper.URI_TEMPLATE_PATTERN.matcher(segment); + int start = 0; while (matcher.find()) { + newSegment.append(segment, start, matcher.start()); foundParam = true; String group = matcher.group(); // Regular expressions can have '{' and '}' characters. Recover earlier replacement params.add(PathHelper.recoverEnclosedCurlyBraces(group)); - matcher.appendReplacement(newSegment, "_resteasy_uri_parameter"); + newSegment.append("_resteasy_uri_parameter"); + start = matcher.end(); } - matcher.appendTail(newSegment); + newSegment.append(segment, start, segment.length()); return foundParam; } @@ -309,11 +308,11 @@ public class Encode * @param encoding * @return */ - private static String encodeValue(String segment, String[] encoding) + public static String encodeValue(String segment, String[] encoding) { ArrayList params = new ArrayList(); boolean foundParam = false; - StringBuffer newSegment = new StringBuffer(); + StringBuilder newSegment = new StringBuilder(); if (savePathParams(segment, newSegment, params)) { foundParam = true; @@ -411,21 +410,21 @@ public class Encode return encodeFromArray(nameOrValue, queryNameValueEncoding, true); } - private static String encodeFromArray(String segment, String[] encodingMap, boolean encodePercent) + protected static String encodeFromArray(String segment, String[] encodingMap, boolean encodePercent) { - StringBuffer result = new StringBuffer(); + StringBuilder result = new StringBuilder(); for (int i = 0; i < segment.length(); i++) { - if (!encodePercent && segment.charAt(i) == '%') + char currentChar = segment.charAt(i); + if (!encodePercent && currentChar == '%') { - result.append(segment.charAt(i)); + result.append(currentChar); continue; } - int idx = segment.charAt(i); - String encoding = encode(idx, encodingMap); + String encoding = encode(currentChar, encodingMap); if (encoding == null) { - result.append(segment.charAt(i)); + result.append(currentChar); } else { @@ -461,20 +460,20 @@ public class Encode return encoded; } - private static String pathParamReplacement(String segment, List params) + public static String pathParamReplacement(String segment, List params) { - StringBuffer newSegment = new StringBuffer(); + StringBuilder newSegment = new StringBuilder(); Matcher matcher = PARAM_REPLACEMENT.matcher(segment); int i = 0; + int start = 0; while (matcher.find()) { + newSegment.append(segment, start, matcher.start()); String replacement = params.get(i++); - // double encode slashes, so that slashes stay where they are - replacement = replacement.replace("\\", "\\\\"); - replacement = replacement.replace("$", "\\$"); - matcher.appendReplacement(newSegment, replacement); + newSegment.append(replacement); + start = matcher.end(); } - matcher.appendTail(newSegment); + newSegment.append(segment, start, segment.length()); segment = newSegment.toString(); return segment; } @@ -505,6 +504,38 @@ public class Encode } return decoded; } + + /** + * decode an encoded map + * + * @param map + * @param charset + * @return + */ + public static MultivaluedHashMap decode(MultivaluedHashMap map, String charset) + { + if (charset == null) + { + charset = UTF_8; + } + MultivaluedHashMap decoded = new MultivaluedHashMap(); + for (Map.Entry> entry : map.entrySet()) + { + List values = entry.getValue(); + for (String value : values) + { + try + { + decoded.add(URLDecoder.decode(entry.getKey(), charset), URLDecoder.decode(value, charset)); + } + catch (UnsupportedEncodingException e) + { + throw new RuntimeException(e); + } + } + } + return decoded; + } public static MultivaluedHashMap encode(MultivaluedHashMap map) { diff --git a/common/src/main/java/org/keycloak/common/util/KeycloakUriBuilder.java b/common/src/main/java/org/keycloak/common/util/KeycloakUriBuilder.java index f064163cd2..a03c53cbe8 100755 --- a/common/src/main/java/org/keycloak/common/util/KeycloakUriBuilder.java +++ b/common/src/main/java/org/keycloak/common/util/KeycloakUriBuilder.java @@ -614,7 +614,7 @@ public class KeycloakUriBuilder { if (value == null) throw new IllegalArgumentException("A passed in value was null"); if (query == null) query = ""; else query += "&"; - query += Encode.encodeQueryParam(name) + "=" + Encode.encodeQueryParam(value.toString()); + query += Encode.encodeQueryParamAsIs(name) + "=" + Encode.encodeQueryParamAsIs(value.toString()); } return this; } diff --git a/core/pom.xml b/core/pom.xml index 77fd3b7e72..c759a8a8ff 100755 --- a/core/pom.xml +++ b/core/pom.xml @@ -21,7 +21,7 @@ keycloak-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../pom.xml 4.0.0 diff --git a/core/src/main/java/org/keycloak/OAuth2Constants.java b/core/src/main/java/org/keycloak/OAuth2Constants.java index 234b632b04..2e585c3278 100644 --- a/core/src/main/java/org/keycloak/OAuth2Constants.java +++ b/core/src/main/java/org/keycloak/OAuth2Constants.java @@ -50,6 +50,7 @@ public interface OAuth2Constants { String AUTHORIZATION_CODE = "authorization_code"; + String IMPLICIT = "implicit"; String PASSWORD = "password"; @@ -92,6 +93,16 @@ public interface OAuth2Constants { String PKCE_METHOD_PLAIN = "plain"; String PKCE_METHOD_S256 = "S256"; + String TOKEN_EXCHANGE_GRANT_TYPE="urn:ietf:params:oauth:grant-type:token-exchange"; + String AUDIENCE="audience"; + String SUBJECT_TOKEN="subject_token"; + String SUBJECT_TOKEN_TYPE="subject_token_type"; + String ACCESS_TOKEN_TYPE="urn:ietf:params:oauth:token-type:access_token"; + String REFRESH_TOKEN_TYPE="urn:ietf:params:oauth:token-type:refresh_token"; + String JWT_TOKEN_TYPE="urn:ietf:params:oauth:token-type:jwt"; + String ID_TOKEN_TYPE="urn:ietf:params:oauth:token-type:id_token"; + + } diff --git a/core/src/main/java/org/keycloak/representations/adapters/config/BaseAdapterConfig.java b/core/src/main/java/org/keycloak/representations/adapters/config/BaseAdapterConfig.java index 4a2b7e288f..ebd49ab9dc 100755 --- a/core/src/main/java/org/keycloak/representations/adapters/config/BaseAdapterConfig.java +++ b/core/src/main/java/org/keycloak/representations/adapters/config/BaseAdapterConfig.java @@ -62,7 +62,8 @@ public class BaseAdapterConfig extends BaseRealmConfig { protected boolean publicClient; @JsonProperty("credentials") protected Map credentials = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); - + @JsonProperty("redirect-rewrite-rules") + protected Map redirectRewriteRules; public boolean isUseResourceRoleMappings() { return useResourceRoleMappings; @@ -167,4 +168,14 @@ public class BaseAdapterConfig extends BaseRealmConfig { public void setPublicClient(boolean publicClient) { this.publicClient = publicClient; } + + public Map getRedirectRewriteRules() { + return redirectRewriteRules; + } + + public void setRedirectRewriteRules(Map redirectRewriteRules) { + this.redirectRewriteRules = redirectRewriteRules; + } + + } diff --git a/core/src/main/java/org/keycloak/representations/docker/DockerAccess.java b/core/src/main/java/org/keycloak/representations/docker/DockerAccess.java new file mode 100644 index 0000000000..969bcb03ab --- /dev/null +++ b/core/src/main/java/org/keycloak/representations/docker/DockerAccess.java @@ -0,0 +1,119 @@ +package org.keycloak.representations.docker; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; + + +/** + * Per the docker auth v2 spec, access is defined like this: + * + * { + * "type": "repository", + * "name": "samalba/my-app", + * "actions": [ + * "push", + * "pull" + * ] + * } + * + */ +public class DockerAccess { + + public static final int ACCESS_TYPE = 0; + public static final int REPOSITORY_NAME = 1; + public static final int PERMISSIONS = 2; + public static final String DECODE_ENCODING = "UTF-8"; + + @JsonProperty("type") + protected String type; + @JsonProperty("name") + protected String name; + @JsonProperty("actions") + protected List actions; + + public DockerAccess() { + } + + public DockerAccess(final String scopeParam) { + if (scopeParam != null) { + try { + final String unencoded = URLDecoder.decode(scopeParam, DECODE_ENCODING); + final String[] parts = unencoded.split(":"); + if (parts.length != 3) { + throw new IllegalArgumentException(String.format("Expecting input string to have %d parts delineated by a ':' character. " + + "Found %d parts: %s", 3, parts.length, unencoded)); + } + + type = parts[ACCESS_TYPE]; + name = parts[REPOSITORY_NAME]; + if (parts[PERMISSIONS] != null) { + actions = Arrays.asList(parts[PERMISSIONS].split(",")); + } + } catch (final UnsupportedEncodingException e) { + throw new IllegalStateException("Error attempting to decode scope parameter using encoding: " + DECODE_ENCODING); + } + } + } + + public String getType() { + return type; + } + + public DockerAccess setType(final String type) { + this.type = type; + return this; + } + + public String getName() { + return name; + } + + public DockerAccess setName(final String name) { + this.name = name; + return this; + } + + public List getActions() { + return actions; + } + + public DockerAccess setActions(final List actions) { + this.actions = actions; + return this; + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (!(o instanceof DockerAccess)) return false; + + final DockerAccess that = (DockerAccess) o; + + if (type != null ? !type.equals(that.type) : that.type != null) return false; + if (name != null ? !name.equals(that.name) : that.name != null) return false; + return actions != null ? actions.equals(that.actions) : that.actions == null; + + } + + @Override + public int hashCode() { + int result = type != null ? type.hashCode() : 0; + result = 31 * result + (name != null ? name.hashCode() : 0); + result = 31 * result + (actions != null ? actions.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return "DockerAccess{" + + "type='" + type + '\'' + + ", name='" + name + '\'' + + ", actions=" + actions + + '}'; + } +} diff --git a/core/src/main/java/org/keycloak/representations/docker/DockerError.java b/core/src/main/java/org/keycloak/representations/docker/DockerError.java new file mode 100644 index 0000000000..b33bb58749 --- /dev/null +++ b/core/src/main/java/org/keycloak/representations/docker/DockerError.java @@ -0,0 +1,84 @@ +package org.keycloak.representations.docker; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; + +/** + * JSON Representation of a Docker Error in the following format: + * + * + * { + * "code": "UNAUTHORIZED", + * "message": "access to the requested resource is not authorized", + * "detail": [ + * { + * "Type": "repository", + * "Name": "samalba/my-app", + * "Action": "pull" + * }, + * { + * "Type": "repository", + * "Name": "samalba/my-app", + * "Action": "push" + * } + * ] + * } + */ +public class DockerError { + + + @JsonProperty("code") + private final String errorCode; + @JsonProperty("message") + private final String message; + @JsonProperty("detail") + private final List dockerErrorDetails; + + public DockerError(final String errorCode, final String message, final List dockerErrorDetails) { + this.errorCode = errorCode; + this.message = message; + this.dockerErrorDetails = dockerErrorDetails; + } + + public String getErrorCode() { + return errorCode; + } + + public String getMessage() { + return message; + } + + public List getDockerErrorDetails() { + return dockerErrorDetails; + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (!(o instanceof DockerError)) return false; + + final DockerError that = (DockerError) o; + + if (errorCode != that.errorCode) return false; + if (message != null ? !message.equals(that.message) : that.message != null) return false; + return dockerErrorDetails != null ? dockerErrorDetails.equals(that.dockerErrorDetails) : that.dockerErrorDetails == null; + } + + @Override + public int hashCode() { + int result = errorCode != null ? errorCode.hashCode() : 0; + result = 31 * result + (message != null ? message.hashCode() : 0); + result = 31 * result + (dockerErrorDetails != null ? dockerErrorDetails.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return "DockerError{" + + "errorCode=" + errorCode + + ", message='" + message + '\'' + + ", dockerErrorDetails=" + dockerErrorDetails + + '}'; + } +} diff --git a/core/src/main/java/org/keycloak/representations/docker/DockerErrorResponseToken.java b/core/src/main/java/org/keycloak/representations/docker/DockerErrorResponseToken.java new file mode 100644 index 0000000000..3d961ce946 --- /dev/null +++ b/core/src/main/java/org/keycloak/representations/docker/DockerErrorResponseToken.java @@ -0,0 +1,38 @@ +package org.keycloak.representations.docker; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; + +public class DockerErrorResponseToken { + + + @JsonProperty("errors") + private final List errorList; + + public DockerErrorResponseToken(final List errorList) { + this.errorList = errorList; + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (!(o instanceof DockerErrorResponseToken)) return false; + + final DockerErrorResponseToken that = (DockerErrorResponseToken) o; + + return errorList != null ? errorList.equals(that.errorList) : that.errorList == null; + } + + @Override + public int hashCode() { + return errorList != null ? errorList.hashCode() : 0; + } + + @Override + public String toString() { + return "DockerErrorResponseToken{" + + "errorList=" + errorList + + '}'; + } +} diff --git a/core/src/main/java/org/keycloak/representations/docker/DockerResponse.java b/core/src/main/java/org/keycloak/representations/docker/DockerResponse.java new file mode 100644 index 0000000000..98074fa689 --- /dev/null +++ b/core/src/main/java/org/keycloak/representations/docker/DockerResponse.java @@ -0,0 +1,88 @@ +package org.keycloak.representations.docker; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Creates a response understandable by the docker client in the form: + * + { + "token" : "eyJh...nSQ", + "expires_in" : 300, + "issued_at" : "2016-09-02T10:56:33Z" + } + */ +public class DockerResponse { + + @JsonProperty("token") + private String token; + @JsonProperty("expires_in") + private Integer expires_in; + @JsonProperty("issued_at") + private String issued_at; + + public DockerResponse() { + } + + public DockerResponse(final String token, final Integer expires_in, final String issued_at) { + this.token = token; + this.expires_in = expires_in; + this.issued_at = issued_at; + } + + public String getToken() { + return token; + } + + public DockerResponse setToken(final String token) { + this.token = token; + return this; + } + + public Integer getExpires_in() { + return expires_in; + } + + public DockerResponse setExpires_in(final Integer expires_in) { + this.expires_in = expires_in; + return this; + } + + public String getIssued_at() { + return issued_at; + } + + public DockerResponse setIssued_at(final String issued_at) { + this.issued_at = issued_at; + return this; + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (!(o instanceof DockerResponse)) return false; + + final DockerResponse that = (DockerResponse) o; + + if (token != null ? !token.equals(that.token) : that.token != null) return false; + if (expires_in != null ? !expires_in.equals(that.expires_in) : that.expires_in != null) return false; + return issued_at != null ? issued_at.equals(that.issued_at) : that.issued_at == null; + + } + + @Override + public int hashCode() { + int result = token != null ? token.hashCode() : 0; + result = 31 * result + (expires_in != null ? expires_in.hashCode() : 0); + result = 31 * result + (issued_at != null ? issued_at.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return "DockerResponse{" + + "token='" + token + '\'' + + ", expires_in='" + expires_in + '\'' + + ", issued_at='" + issued_at + '\'' + + '}'; + } +} diff --git a/core/src/main/java/org/keycloak/representations/docker/DockerResponseToken.java b/core/src/main/java/org/keycloak/representations/docker/DockerResponseToken.java new file mode 100644 index 0000000000..faee452c5b --- /dev/null +++ b/core/src/main/java/org/keycloak/representations/docker/DockerResponseToken.java @@ -0,0 +1,97 @@ +package org.keycloak.representations.docker; + +import com.fasterxml.jackson.annotation.JsonProperty; +import org.keycloak.representations.JsonWebToken; + +import java.util.ArrayList; +import java.util.List; + +/** + * * { + * "iss": "auth.docker.com", + * "sub": "jlhawn", + * "aud": "registry.docker.com", + * "exp": 1415387315, + * "nbf": 1415387015, + * "iat": 1415387015, + * "jti": "tYJCO1c6cnyy7kAn0c7rKPgbV1H1bFws", + * "access": [ + * { + * "type": "repository", + * "name": "samalba/my-app", + * "actions": [ + * "push" + * ] + * } + * ] + * } + */ +public class DockerResponseToken extends JsonWebToken { + + @JsonProperty("access") + protected List accessItems = new ArrayList<>(); + + public List getAccessItems() { + return accessItems; + } + + @Override + public DockerResponseToken id(final String id) { + super.id(id); + return this; + } + + @Override + public DockerResponseToken expiration(final int expiration) { + super.expiration(expiration); + return this; + } + + @Override + public DockerResponseToken notBefore(final int notBefore) { + super.notBefore(notBefore); + return this; + } + + @Override + public DockerResponseToken issuedNow() { + super.issuedNow(); + return this; + } + + @Override + public DockerResponseToken issuedAt(final int issuedAt) { + super.issuedAt(issuedAt); + return this; + } + + @Override + public DockerResponseToken issuer(final String issuer) { + super.issuer(issuer); + return this; + } + + @Override + public DockerResponseToken audience(final String... audience) { + super.audience(audience); + return this; + } + + @Override + public DockerResponseToken subject(final String subject) { + super.subject(subject); + return this; + } + + @Override + public DockerResponseToken type(final String type) { + super.type(type); + return this; + } + + @Override + public DockerResponseToken issuedFor(final String issuedFor) { + super.issuedFor(issuedFor); + return this; + } +} diff --git a/core/src/main/java/org/keycloak/representations/idm/CredentialRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/CredentialRepresentation.java index d597cf32aa..95c7ca3d1a 100755 --- a/core/src/main/java/org/keycloak/representations/idm/CredentialRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/idm/CredentialRepresentation.java @@ -155,4 +155,101 @@ public class CredentialRepresentation { public void setConfig(MultivaluedHashMap config) { this.config = config; } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((algorithm == null) ? 0 : algorithm.hashCode()); + result = prime * result + ((config == null) ? 0 : config.hashCode()); + result = prime * result + ((counter == null) ? 0 : counter.hashCode()); + result = prime * result + ((createdDate == null) ? 0 : createdDate.hashCode()); + result = prime * result + ((device == null) ? 0 : device.hashCode()); + result = prime * result + ((digits == null) ? 0 : digits.hashCode()); + result = prime * result + ((hashIterations == null) ? 0 : hashIterations.hashCode()); + result = prime * result + ((hashedSaltedValue == null) ? 0 : hashedSaltedValue.hashCode()); + result = prime * result + ((period == null) ? 0 : period.hashCode()); + result = prime * result + ((salt == null) ? 0 : salt.hashCode()); + result = prime * result + ((temporary == null) ? 0 : temporary.hashCode()); + result = prime * result + ((type == null) ? 0 : type.hashCode()); + result = prime * result + ((value == null) ? 0 : value.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + CredentialRepresentation other = (CredentialRepresentation) obj; + if (algorithm == null) { + if (other.algorithm != null) + return false; + } else if (!algorithm.equals(other.algorithm)) + return false; + if (config == null) { + if (other.config != null) + return false; + } else if (!config.equals(other.config)) + return false; + if (counter == null) { + if (other.counter != null) + return false; + } else if (!counter.equals(other.counter)) + return false; + if (createdDate == null) { + if (other.createdDate != null) + return false; + } else if (!createdDate.equals(other.createdDate)) + return false; + if (device == null) { + if (other.device != null) + return false; + } else if (!device.equals(other.device)) + return false; + if (digits == null) { + if (other.digits != null) + return false; + } else if (!digits.equals(other.digits)) + return false; + if (hashIterations == null) { + if (other.hashIterations != null) + return false; + } else if (!hashIterations.equals(other.hashIterations)) + return false; + if (hashedSaltedValue == null) { + if (other.hashedSaltedValue != null) + return false; + } else if (!hashedSaltedValue.equals(other.hashedSaltedValue)) + return false; + if (period == null) { + if (other.period != null) + return false; + } else if (!period.equals(other.period)) + return false; + if (salt == null) { + if (other.salt != null) + return false; + } else if (!salt.equals(other.salt)) + return false; + if (temporary == null) { + if (other.temporary != null) + return false; + } else if (!temporary.equals(other.temporary)) + return false; + if (type == null) { + if (other.type != null) + return false; + } else if (!type.equals(other.type)) + return false; + if (value == null) { + if (other.value != null) + return false; + } else if (!value.equals(other.value)) + return false; + return true; + } } diff --git a/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java index 670e1d8bde..c3dd733262 100755 --- a/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java @@ -137,6 +137,7 @@ public class RealmRepresentation { protected String directGrantFlow; protected String resetCredentialsFlow; protected String clientAuthenticationFlow; + protected String dockerAuthenticationFlow; protected Map attributes; @@ -884,6 +885,15 @@ public class RealmRepresentation { this.clientAuthenticationFlow = clientAuthenticationFlow; } + public String getDockerAuthenticationFlow() { + return dockerAuthenticationFlow; + } + + public RealmRepresentation setDockerAuthenticationFlow(final String dockerAuthenticationFlow) { + this.dockerAuthenticationFlow = dockerAuthenticationFlow; + return this; + } + public String getKeycloakVersion() { return keycloakVersion; } diff --git a/core/src/main/java/org/keycloak/representations/info/ProviderRepresentation.java b/core/src/main/java/org/keycloak/representations/info/ProviderRepresentation.java index e1b704e206..8dcf00631f 100644 --- a/core/src/main/java/org/keycloak/representations/info/ProviderRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/info/ProviderRepresentation.java @@ -21,8 +21,18 @@ import java.util.Map; public class ProviderRepresentation { + private int order; + private Map operationalInfo; + public int getOrder() { + return order; + } + + public void setOrder(int priorityUI) { + this.order = priorityUI; + } + public Map getOperationalInfo() { return operationalInfo; } diff --git a/dependencies/pom.xml b/dependencies/pom.xml index e98ce5ac39..f0b973029d 100755 --- a/dependencies/pom.xml +++ b/dependencies/pom.xml @@ -21,7 +21,7 @@ keycloak-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT 4.0.0 diff --git a/dependencies/server-all/pom.xml b/dependencies/server-all/pom.xml index 4bf1baa5d2..4061b97a97 100755 --- a/dependencies/server-all/pom.xml +++ b/dependencies/server-all/pom.xml @@ -21,7 +21,7 @@ keycloak-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../../pom.xml 4.0.0 diff --git a/dependencies/server-min/pom.xml b/dependencies/server-min/pom.xml index f4938b011b..216bbad7e2 100755 --- a/dependencies/server-min/pom.xml +++ b/dependencies/server-min/pom.xml @@ -21,7 +21,7 @@ keycloak-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../../pom.xml 4.0.0 diff --git a/distribution/adapters/as7-eap6-adapter/as7-adapter-zip/pom.xml b/distribution/adapters/as7-eap6-adapter/as7-adapter-zip/pom.xml index bd92f052fc..f10d11a7eb 100755 --- a/distribution/adapters/as7-eap6-adapter/as7-adapter-zip/pom.xml +++ b/distribution/adapters/as7-eap6-adapter/as7-adapter-zip/pom.xml @@ -21,7 +21,7 @@ keycloak-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../../../../pom.xml diff --git a/distribution/adapters/as7-eap6-adapter/as7-modules/pom.xml b/distribution/adapters/as7-eap6-adapter/as7-modules/pom.xml index 004e6c1342..2529bc45db 100755 --- a/distribution/adapters/as7-eap6-adapter/as7-modules/pom.xml +++ b/distribution/adapters/as7-eap6-adapter/as7-modules/pom.xml @@ -25,7 +25,7 @@ keycloak-as7-eap6-adapter-dist-pom org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../pom.xml diff --git a/distribution/adapters/as7-eap6-adapter/eap6-adapter-zip/pom.xml b/distribution/adapters/as7-eap6-adapter/eap6-adapter-zip/pom.xml index 670fcd45f1..f2c5dac490 100755 --- a/distribution/adapters/as7-eap6-adapter/eap6-adapter-zip/pom.xml +++ b/distribution/adapters/as7-eap6-adapter/eap6-adapter-zip/pom.xml @@ -21,7 +21,7 @@ keycloak-as7-eap6-adapter-dist-pom org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../pom.xml diff --git a/distribution/adapters/as7-eap6-adapter/pom.xml b/distribution/adapters/as7-eap6-adapter/pom.xml index 6096915bca..e94db3a959 100644 --- a/distribution/adapters/as7-eap6-adapter/pom.xml +++ b/distribution/adapters/as7-eap6-adapter/pom.xml @@ -20,7 +20,7 @@ keycloak-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../../../pom.xml Keycloak AS7 / JBoss EAP 6 Adapter Distros diff --git a/distribution/adapters/fuse-adapter-zip/pom.xml b/distribution/adapters/fuse-adapter-zip/pom.xml index 7bfd0d2b89..03db2b4868 100644 --- a/distribution/adapters/fuse-adapter-zip/pom.xml +++ b/distribution/adapters/fuse-adapter-zip/pom.xml @@ -21,7 +21,7 @@ keycloak-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../../../pom.xml diff --git a/distribution/adapters/jetty81-adapter-zip/pom.xml b/distribution/adapters/jetty81-adapter-zip/pom.xml index 76713f4002..5cfa1988a1 100755 --- a/distribution/adapters/jetty81-adapter-zip/pom.xml +++ b/distribution/adapters/jetty81-adapter-zip/pom.xml @@ -21,7 +21,7 @@ keycloak-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../../../pom.xml diff --git a/distribution/adapters/jetty91-adapter-zip/pom.xml b/distribution/adapters/jetty91-adapter-zip/pom.xml index e83caa8c8a..506bf22f29 100755 --- a/distribution/adapters/jetty91-adapter-zip/pom.xml +++ b/distribution/adapters/jetty91-adapter-zip/pom.xml @@ -21,7 +21,7 @@ keycloak-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../../../pom.xml diff --git a/distribution/adapters/jetty92-adapter-zip/pom.xml b/distribution/adapters/jetty92-adapter-zip/pom.xml index eb247cbe1c..62c81b80a8 100755 --- a/distribution/adapters/jetty92-adapter-zip/pom.xml +++ b/distribution/adapters/jetty92-adapter-zip/pom.xml @@ -21,7 +21,7 @@ keycloak-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../../../pom.xml diff --git a/distribution/adapters/jetty93-adapter-zip/pom.xml b/distribution/adapters/jetty93-adapter-zip/pom.xml index c7ec2de517..f42fcd9283 100644 --- a/distribution/adapters/jetty93-adapter-zip/pom.xml +++ b/distribution/adapters/jetty93-adapter-zip/pom.xml @@ -21,7 +21,7 @@ keycloak-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../../../pom.xml diff --git a/distribution/adapters/jetty94-adapter-zip/pom.xml b/distribution/adapters/jetty94-adapter-zip/pom.xml index d776cb0a4a..bd199c9f5e 100644 --- a/distribution/adapters/jetty94-adapter-zip/pom.xml +++ b/distribution/adapters/jetty94-adapter-zip/pom.xml @@ -21,7 +21,7 @@ keycloak-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../../../pom.xml diff --git a/distribution/adapters/js-adapter-zip/pom.xml b/distribution/adapters/js-adapter-zip/pom.xml index 1a93bb4648..be37dc8729 100755 --- a/distribution/adapters/js-adapter-zip/pom.xml +++ b/distribution/adapters/js-adapter-zip/pom.xml @@ -21,7 +21,7 @@ keycloak-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../../../pom.xml diff --git a/distribution/adapters/osgi/features/pom.xml b/distribution/adapters/osgi/features/pom.xml index 8cef75b8f0..36ef9f35af 100755 --- a/distribution/adapters/osgi/features/pom.xml +++ b/distribution/adapters/osgi/features/pom.xml @@ -21,7 +21,7 @@ keycloak-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../../../../pom.xml Keycloak OSGI Features diff --git a/distribution/adapters/osgi/jaas/pom.xml b/distribution/adapters/osgi/jaas/pom.xml index 1c7182c8ce..2994b3cea2 100755 --- a/distribution/adapters/osgi/jaas/pom.xml +++ b/distribution/adapters/osgi/jaas/pom.xml @@ -21,7 +21,7 @@ keycloak-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../../../../pom.xml Keycloak OSGI JAAS Realm Configuration diff --git a/distribution/adapters/osgi/pom.xml b/distribution/adapters/osgi/pom.xml index 523e714035..61b801af46 100755 --- a/distribution/adapters/osgi/pom.xml +++ b/distribution/adapters/osgi/pom.xml @@ -21,7 +21,7 @@ keycloak-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../../../pom.xml Keycloak OSGI Integration diff --git a/distribution/adapters/osgi/thirdparty/pom.xml b/distribution/adapters/osgi/thirdparty/pom.xml index db047062fc..bf42fa8ca3 100755 --- a/distribution/adapters/osgi/thirdparty/pom.xml +++ b/distribution/adapters/osgi/thirdparty/pom.xml @@ -21,7 +21,7 @@ keycloak-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../../../../pom.xml diff --git a/distribution/adapters/pom.xml b/distribution/adapters/pom.xml index 2fc0f9c12b..6e4193fe2f 100755 --- a/distribution/adapters/pom.xml +++ b/distribution/adapters/pom.xml @@ -20,7 +20,7 @@ keycloak-distribution-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT Adapters Distribution Parent diff --git a/distribution/adapters/tomcat6-adapter-zip/pom.xml b/distribution/adapters/tomcat6-adapter-zip/pom.xml index 77dd6bc555..1ba65022c0 100755 --- a/distribution/adapters/tomcat6-adapter-zip/pom.xml +++ b/distribution/adapters/tomcat6-adapter-zip/pom.xml @@ -21,7 +21,7 @@ keycloak-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../../../pom.xml diff --git a/distribution/adapters/tomcat7-adapter-zip/pom.xml b/distribution/adapters/tomcat7-adapter-zip/pom.xml index 5ccad77d37..391c642dc8 100755 --- a/distribution/adapters/tomcat7-adapter-zip/pom.xml +++ b/distribution/adapters/tomcat7-adapter-zip/pom.xml @@ -21,7 +21,7 @@ keycloak-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../../../pom.xml diff --git a/distribution/adapters/tomcat8-adapter-zip/pom.xml b/distribution/adapters/tomcat8-adapter-zip/pom.xml index e02660f0b2..d87f87b67b 100755 --- a/distribution/adapters/tomcat8-adapter-zip/pom.xml +++ b/distribution/adapters/tomcat8-adapter-zip/pom.xml @@ -21,7 +21,7 @@ keycloak-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../../../pom.xml diff --git a/distribution/adapters/wf8-adapter/pom.xml b/distribution/adapters/wf8-adapter/pom.xml index f975da8aab..05b72b5ef1 100644 --- a/distribution/adapters/wf8-adapter/pom.xml +++ b/distribution/adapters/wf8-adapter/pom.xml @@ -20,7 +20,7 @@ keycloak-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../../../pom.xml Keycloak Wildfly 8 Adapter diff --git a/distribution/adapters/wf8-adapter/wf8-adapter-zip/pom.xml b/distribution/adapters/wf8-adapter/wf8-adapter-zip/pom.xml index fdb1c3fa1a..a88f373c91 100755 --- a/distribution/adapters/wf8-adapter/wf8-adapter-zip/pom.xml +++ b/distribution/adapters/wf8-adapter/wf8-adapter-zip/pom.xml @@ -21,7 +21,7 @@ keycloak-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../../../../pom.xml diff --git a/distribution/adapters/wf8-adapter/wf8-modules/pom.xml b/distribution/adapters/wf8-adapter/wf8-modules/pom.xml index b7cd80720c..88f191bffb 100755 --- a/distribution/adapters/wf8-adapter/wf8-modules/pom.xml +++ b/distribution/adapters/wf8-adapter/wf8-modules/pom.xml @@ -25,7 +25,7 @@ keycloak-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../../../../pom.xml diff --git a/distribution/adapters/wildfly-adapter/pom.xml b/distribution/adapters/wildfly-adapter/pom.xml index 6090e9cb1e..d116ab1a5f 100644 --- a/distribution/adapters/wildfly-adapter/pom.xml +++ b/distribution/adapters/wildfly-adapter/pom.xml @@ -21,7 +21,7 @@ keycloak-adapters-distribution-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT keycloak-wildfly-adapter-dist diff --git a/distribution/api-docs-dist/pom.xml b/distribution/api-docs-dist/pom.xml index a50916abfc..a13c521235 100755 --- a/distribution/api-docs-dist/pom.xml +++ b/distribution/api-docs-dist/pom.xml @@ -21,7 +21,7 @@ keycloak-distribution-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT keycloak-api-docs-dist @@ -62,13 +62,6 @@ - - org.apache.maven.plugins - maven-deploy-plugin - - true - - maven-assembly-plugin @@ -96,4 +89,27 @@ + + + + community + + + !product + + + + + + org.apache.maven.plugins + maven-deploy-plugin + + true + + + + + + + diff --git a/distribution/demo-dist/pom.xml b/distribution/demo-dist/pom.xml index c1668f3f9a..95ae460f83 100755 --- a/distribution/demo-dist/pom.xml +++ b/distribution/demo-dist/pom.xml @@ -21,7 +21,7 @@ keycloak-distribution-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT keycloak-demo-dist diff --git a/distribution/downloads/pom.xml b/distribution/downloads/pom.xml index eb22c9b557..4c9211911e 100755 --- a/distribution/downloads/pom.xml +++ b/distribution/downloads/pom.xml @@ -21,7 +21,7 @@ keycloak-distribution-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT keycloak-dist-downloads diff --git a/distribution/examples-dist/pom.xml b/distribution/examples-dist/pom.xml index ac57760d33..5db83ef6a8 100755 --- a/distribution/examples-dist/pom.xml +++ b/distribution/examples-dist/pom.xml @@ -21,7 +21,7 @@ keycloak-distribution-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT keycloak-examples-dist diff --git a/distribution/feature-packs/adapter-feature-pack/pom.xml b/distribution/feature-packs/adapter-feature-pack/pom.xml index 0dbb9fc74e..6f4b77b49c 100755 --- a/distribution/feature-packs/adapter-feature-pack/pom.xml +++ b/distribution/feature-packs/adapter-feature-pack/pom.xml @@ -19,7 +19,7 @@ org.keycloak feature-packs-parent - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT 4.0.0 diff --git a/distribution/feature-packs/pom.xml b/distribution/feature-packs/pom.xml index bf31d507dc..b32593d23e 100644 --- a/distribution/feature-packs/pom.xml +++ b/distribution/feature-packs/pom.xml @@ -20,7 +20,7 @@ keycloak-distribution-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT Feature Pack Builds diff --git a/distribution/feature-packs/server-feature-pack/pom.xml b/distribution/feature-packs/server-feature-pack/pom.xml index 2d39ad1063..e2f1f4144b 100644 --- a/distribution/feature-packs/server-feature-pack/pom.xml +++ b/distribution/feature-packs/server-feature-pack/pom.xml @@ -19,7 +19,7 @@ org.keycloak feature-packs-parent - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../pom.xml 4.0.0 diff --git a/distribution/pom.xml b/distribution/pom.xml index f8c0d2dcb3..165323ddc8 100755 --- a/distribution/pom.xml +++ b/distribution/pom.xml @@ -20,7 +20,7 @@ keycloak-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../pom.xml diff --git a/distribution/proxy-dist/pom.xml b/distribution/proxy-dist/pom.xml index ee89fa7252..79d4468de6 100755 --- a/distribution/proxy-dist/pom.xml +++ b/distribution/proxy-dist/pom.xml @@ -21,7 +21,7 @@ keycloak-distribution-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT keycloak-proxy-dist diff --git a/distribution/saml-adapters/as7-eap6-adapter/as7-adapter-zip/pom.xml b/distribution/saml-adapters/as7-eap6-adapter/as7-adapter-zip/pom.xml index 935c85f9be..4165230f30 100755 --- a/distribution/saml-adapters/as7-eap6-adapter/as7-adapter-zip/pom.xml +++ b/distribution/saml-adapters/as7-eap6-adapter/as7-adapter-zip/pom.xml @@ -21,7 +21,7 @@ keycloak-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../../../../pom.xml diff --git a/distribution/saml-adapters/as7-eap6-adapter/as7-modules/pom.xml b/distribution/saml-adapters/as7-eap6-adapter/as7-modules/pom.xml index 98ee15f2a9..f2650dfd1d 100755 --- a/distribution/saml-adapters/as7-eap6-adapter/as7-modules/pom.xml +++ b/distribution/saml-adapters/as7-eap6-adapter/as7-modules/pom.xml @@ -25,7 +25,7 @@ keycloak-saml-as7-eap6-adapter-dist-pom org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../pom.xml diff --git a/distribution/saml-adapters/as7-eap6-adapter/as7-modules/src/main/resources/modules/org/keycloak/keycloak-saml-adapter-core/main/module.xml b/distribution/saml-adapters/as7-eap6-adapter/as7-modules/src/main/resources/modules/org/keycloak/keycloak-saml-adapter-core/main/module.xml index 4973aa1a4f..ba66c82775 100755 --- a/distribution/saml-adapters/as7-eap6-adapter/as7-modules/src/main/resources/modules/org/keycloak/keycloak-saml-adapter-core/main/module.xml +++ b/distribution/saml-adapters/as7-eap6-adapter/as7-modules/src/main/resources/modules/org/keycloak/keycloak-saml-adapter-core/main/module.xml @@ -28,6 +28,7 @@ + diff --git a/distribution/saml-adapters/as7-eap6-adapter/eap6-adapter-zip/pom.xml b/distribution/saml-adapters/as7-eap6-adapter/eap6-adapter-zip/pom.xml index d0da2e4926..54c249d8ff 100755 --- a/distribution/saml-adapters/as7-eap6-adapter/eap6-adapter-zip/pom.xml +++ b/distribution/saml-adapters/as7-eap6-adapter/eap6-adapter-zip/pom.xml @@ -21,7 +21,7 @@ keycloak-saml-as7-eap6-adapter-dist-pom org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../pom.xml diff --git a/distribution/saml-adapters/as7-eap6-adapter/pom.xml b/distribution/saml-adapters/as7-eap6-adapter/pom.xml index 0a4d2ccafb..e1c378c220 100755 --- a/distribution/saml-adapters/as7-eap6-adapter/pom.xml +++ b/distribution/saml-adapters/as7-eap6-adapter/pom.xml @@ -20,7 +20,7 @@ keycloak-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../../../pom.xml Keycloak SAML AS7 / JBoss EAP 6 Adapter Distros diff --git a/distribution/saml-adapters/jetty81-adapter-zip/pom.xml b/distribution/saml-adapters/jetty81-adapter-zip/pom.xml index e74e1266c5..8c8f99f986 100755 --- a/distribution/saml-adapters/jetty81-adapter-zip/pom.xml +++ b/distribution/saml-adapters/jetty81-adapter-zip/pom.xml @@ -21,7 +21,7 @@ keycloak-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../../../pom.xml diff --git a/distribution/saml-adapters/jetty92-adapter-zip/pom.xml b/distribution/saml-adapters/jetty92-adapter-zip/pom.xml index c1250825e5..f5c3481c22 100755 --- a/distribution/saml-adapters/jetty92-adapter-zip/pom.xml +++ b/distribution/saml-adapters/jetty92-adapter-zip/pom.xml @@ -21,7 +21,7 @@ keycloak-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../../../pom.xml diff --git a/distribution/saml-adapters/jetty93-adapter-zip/pom.xml b/distribution/saml-adapters/jetty93-adapter-zip/pom.xml index 6fe0cb5797..e6ef28e651 100644 --- a/distribution/saml-adapters/jetty93-adapter-zip/pom.xml +++ b/distribution/saml-adapters/jetty93-adapter-zip/pom.xml @@ -21,7 +21,7 @@ keycloak-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../../../pom.xml diff --git a/distribution/saml-adapters/jetty94-adapter-zip/pom.xml b/distribution/saml-adapters/jetty94-adapter-zip/pom.xml index 5e66e8f763..8b86d43d74 100644 --- a/distribution/saml-adapters/jetty94-adapter-zip/pom.xml +++ b/distribution/saml-adapters/jetty94-adapter-zip/pom.xml @@ -21,7 +21,7 @@ keycloak-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../../../pom.xml diff --git a/distribution/saml-adapters/pom.xml b/distribution/saml-adapters/pom.xml index 4949548b46..81828d285e 100755 --- a/distribution/saml-adapters/pom.xml +++ b/distribution/saml-adapters/pom.xml @@ -20,7 +20,7 @@ keycloak-distribution-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT SAML Adapters Distribution Parent diff --git a/distribution/saml-adapters/tomcat6-adapter-zip/pom.xml b/distribution/saml-adapters/tomcat6-adapter-zip/pom.xml index 62ef4ce7cd..01cdab6a52 100755 --- a/distribution/saml-adapters/tomcat6-adapter-zip/pom.xml +++ b/distribution/saml-adapters/tomcat6-adapter-zip/pom.xml @@ -21,7 +21,7 @@ keycloak-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../../../pom.xml diff --git a/distribution/saml-adapters/tomcat7-adapter-zip/pom.xml b/distribution/saml-adapters/tomcat7-adapter-zip/pom.xml index d9eef4e114..f4a5a2d125 100755 --- a/distribution/saml-adapters/tomcat7-adapter-zip/pom.xml +++ b/distribution/saml-adapters/tomcat7-adapter-zip/pom.xml @@ -21,7 +21,7 @@ keycloak-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../../../pom.xml diff --git a/distribution/saml-adapters/tomcat8-adapter-zip/pom.xml b/distribution/saml-adapters/tomcat8-adapter-zip/pom.xml index be42ceee4a..da7c2187d6 100755 --- a/distribution/saml-adapters/tomcat8-adapter-zip/pom.xml +++ b/distribution/saml-adapters/tomcat8-adapter-zip/pom.xml @@ -21,7 +21,7 @@ keycloak-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../../../pom.xml diff --git a/distribution/saml-adapters/wildfly-adapter/pom.xml b/distribution/saml-adapters/wildfly-adapter/pom.xml index 6bec45308a..6d2a6e2c2e 100755 --- a/distribution/saml-adapters/wildfly-adapter/pom.xml +++ b/distribution/saml-adapters/wildfly-adapter/pom.xml @@ -20,7 +20,7 @@ keycloak-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../../../pom.xml Keycloak Wildfly SAML Adapter diff --git a/distribution/saml-adapters/wildfly-adapter/wildfly-adapter-zip/pom.xml b/distribution/saml-adapters/wildfly-adapter/wildfly-adapter-zip/pom.xml index a30ecef034..1b80a59d54 100755 --- a/distribution/saml-adapters/wildfly-adapter/wildfly-adapter-zip/pom.xml +++ b/distribution/saml-adapters/wildfly-adapter/wildfly-adapter-zip/pom.xml @@ -21,7 +21,7 @@ keycloak-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../../../../pom.xml diff --git a/distribution/saml-adapters/wildfly-adapter/wildfly-modules/pom.xml b/distribution/saml-adapters/wildfly-adapter/wildfly-modules/pom.xml index ec9659522e..e398bbbe5f 100755 --- a/distribution/saml-adapters/wildfly-adapter/wildfly-modules/pom.xml +++ b/distribution/saml-adapters/wildfly-adapter/wildfly-modules/pom.xml @@ -25,7 +25,7 @@ keycloak-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../../../../pom.xml diff --git a/distribution/saml-adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-saml-adapter-core/main/module.xml b/distribution/saml-adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-saml-adapter-core/main/module.xml index e19e0f026d..ef44695f72 100755 --- a/distribution/saml-adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-saml-adapter-core/main/module.xml +++ b/distribution/saml-adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-saml-adapter-core/main/module.xml @@ -28,6 +28,7 @@ + diff --git a/distribution/server-dist/pom.xml b/distribution/server-dist/pom.xml index fd216303ff..6b425c0a13 100755 --- a/distribution/server-dist/pom.xml +++ b/distribution/server-dist/pom.xml @@ -21,7 +21,7 @@ keycloak-distribution-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT keycloak-server-dist @@ -34,11 +34,23 @@ org.keycloak keycloak-server-feature-pack zip + + + * + * + + org.keycloak keycloak-client-cli-dist zip + + + * + * + + diff --git a/distribution/server-overlay/pom.xml b/distribution/server-overlay/pom.xml index d3310bbe26..f8a0c538ba 100755 --- a/distribution/server-overlay/pom.xml +++ b/distribution/server-overlay/pom.xml @@ -21,7 +21,7 @@ keycloak-distribution-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT keycloak-server-overlay diff --git a/examples/admin-client/pom.xml b/examples/admin-client/pom.xml index e7339f7de0..c5ac43eafb 100755 --- a/examples/admin-client/pom.xml +++ b/examples/admin-client/pom.xml @@ -22,7 +22,7 @@ keycloak-examples-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT Keycloak Examples - Admin Client diff --git a/examples/authz/hello-world-authz-service/pom.xml b/examples/authz/hello-world-authz-service/pom.xml index 2067d82f58..26c1777330 100755 --- a/examples/authz/hello-world-authz-service/pom.xml +++ b/examples/authz/hello-world-authz-service/pom.xml @@ -24,7 +24,7 @@ org.keycloak keycloak-authz-example-parent - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../pom.xml diff --git a/examples/authz/hello-world/pom.xml b/examples/authz/hello-world/pom.xml index 50e51993e6..afdc3fb879 100755 --- a/examples/authz/hello-world/pom.xml +++ b/examples/authz/hello-world/pom.xml @@ -24,7 +24,7 @@ org.keycloak keycloak-authz-example-parent - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../pom.xml diff --git a/examples/authz/photoz/photoz-authz-policy/pom.xml b/examples/authz/photoz/photoz-authz-policy/pom.xml index 8115179975..08267aa479 100755 --- a/examples/authz/photoz/photoz-authz-policy/pom.xml +++ b/examples/authz/photoz/photoz-authz-policy/pom.xml @@ -6,7 +6,7 @@ org.keycloak keycloak-authz-photoz-parent - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../pom.xml diff --git a/examples/authz/photoz/photoz-html5-client/pom.xml b/examples/authz/photoz/photoz-html5-client/pom.xml index 09db1e5aca..5ff5fdb136 100755 --- a/examples/authz/photoz/photoz-html5-client/pom.xml +++ b/examples/authz/photoz/photoz-html5-client/pom.xml @@ -5,7 +5,7 @@ org.keycloak keycloak-authz-photoz-parent - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../pom.xml diff --git a/examples/authz/photoz/photoz-restful-api/pom.xml b/examples/authz/photoz/photoz-restful-api/pom.xml index 918c2584fc..94b73dc07b 100755 --- a/examples/authz/photoz/photoz-restful-api/pom.xml +++ b/examples/authz/photoz/photoz-restful-api/pom.xml @@ -6,7 +6,7 @@ org.keycloak keycloak-authz-photoz-parent - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../pom.xml 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 129a11a080..1fe66757b5 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 @@ -83,14 +83,14 @@ public class AlbumService { @GET @Produces("application/json") public Response findAll() { - return Response.ok(this.entityManager.createQuery("from Album where userId = '" + request.getUserPrincipal().getName() + "'").getResultList()).build(); + return Response.ok(this.entityManager.createQuery("from Album where userId = :id").setParameter("id", request.getUserPrincipal().getName()).getResultList()).build(); } @GET @Path("{id}") @Produces("application/json") public Response findById(@PathParam("id") String id) { - List result = this.entityManager.createQuery("from Album where id = " + id).getResultList(); + List result = this.entityManager.createQuery("from Album where id = :id").setParameter("id", id).getResultList(); if (result.isEmpty()) { return Response.status(Status.NOT_FOUND).build(); 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 92e300dec5..62591227d7 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 @@ -43,7 +43,7 @@ public class ProfileService { @Produces("application/json") public Response view(@Context HttpServletRequest request) { Principal userPrincipal = request.getUserPrincipal(); - List albums = this.entityManager.createQuery("from Album where userId = '" + userPrincipal.getName() + "'").getResultList(); + List albums = this.entityManager.createQuery("from Album where userId = :id").setParameter("id", userPrincipal.getName()).getResultList(); return Response.ok(new Profile(userPrincipal.getName(), albums.size())).build(); } diff --git a/examples/authz/photoz/pom.xml b/examples/authz/photoz/pom.xml index a863cd21d7..cbaeb243ec 100755 --- a/examples/authz/photoz/pom.xml +++ b/examples/authz/photoz/pom.xml @@ -6,7 +6,7 @@ org.keycloak keycloak-authz-example-parent - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../pom.xml diff --git a/examples/authz/pom.xml b/examples/authz/pom.xml index 03012e716c..06adb3ca98 100755 --- a/examples/authz/pom.xml +++ b/examples/authz/pom.xml @@ -6,7 +6,7 @@ keycloak-examples-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../pom.xml diff --git a/examples/authz/servlet-authz/pom.xml b/examples/authz/servlet-authz/pom.xml index 68e672cb1a..ffcf7f2085 100755 --- a/examples/authz/servlet-authz/pom.xml +++ b/examples/authz/servlet-authz/pom.xml @@ -6,7 +6,7 @@ org.keycloak keycloak-authz-example-parent - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../pom.xml diff --git a/examples/basic-auth/pom.xml b/examples/basic-auth/pom.xml index 0c12d2ba92..af19e13ef0 100755 --- a/examples/basic-auth/pom.xml +++ b/examples/basic-auth/pom.xml @@ -23,7 +23,7 @@ keycloak-examples-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT Keycloak Examples - Basic Auth diff --git a/examples/broker/facebook-authentication/pom.xml b/examples/broker/facebook-authentication/pom.xml index c0ef110d74..0fb71b4a4e 100755 --- a/examples/broker/facebook-authentication/pom.xml +++ b/examples/broker/facebook-authentication/pom.xml @@ -23,7 +23,7 @@ keycloak-examples-broker-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT Keycloak Broker Examples - Facebook Authentication diff --git a/examples/broker/google-authentication/pom.xml b/examples/broker/google-authentication/pom.xml index d29e41fddb..e89fbc560f 100755 --- a/examples/broker/google-authentication/pom.xml +++ b/examples/broker/google-authentication/pom.xml @@ -23,7 +23,7 @@ keycloak-examples-broker-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT Keycloak Broker Examples - Google Authentication diff --git a/examples/broker/pom.xml b/examples/broker/pom.xml index aed134df5d..5797474cc6 100755 --- a/examples/broker/pom.xml +++ b/examples/broker/pom.xml @@ -20,7 +20,7 @@ keycloak-examples-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT Broker Examples diff --git a/examples/broker/saml-broker-authentication/pom.xml b/examples/broker/saml-broker-authentication/pom.xml index eda4dd75c0..3226fbc353 100755 --- a/examples/broker/saml-broker-authentication/pom.xml +++ b/examples/broker/saml-broker-authentication/pom.xml @@ -23,7 +23,7 @@ keycloak-examples-broker-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT Keycloak Broker Examples - SAML Identity Provider Brokering diff --git a/examples/broker/twitter-authentication/pom.xml b/examples/broker/twitter-authentication/pom.xml index aa54475306..188035d0e8 100755 --- a/examples/broker/twitter-authentication/pom.xml +++ b/examples/broker/twitter-authentication/pom.xml @@ -23,7 +23,7 @@ keycloak-examples-broker-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT Keycloak Broker Examples - Twitter Authentication diff --git a/examples/cors/angular-product-app/pom.xml b/examples/cors/angular-product-app/pom.xml index 1a2a669b83..28e65fcf6b 100755 --- a/examples/cors/angular-product-app/pom.xml +++ b/examples/cors/angular-product-app/pom.xml @@ -21,7 +21,7 @@ keycloak-examples-cors-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT 4.0.0 diff --git a/examples/cors/database-service/pom.xml b/examples/cors/database-service/pom.xml index 8e9298708c..80ccf115b2 100755 --- a/examples/cors/database-service/pom.xml +++ b/examples/cors/database-service/pom.xml @@ -21,7 +21,7 @@ keycloak-examples-cors-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT 4.0.0 diff --git a/examples/cors/pom.xml b/examples/cors/pom.xml index 40b2ac83e0..3e4d71ad6d 100755 --- a/examples/cors/pom.xml +++ b/examples/cors/pom.xml @@ -21,7 +21,7 @@ keycloak-examples-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT Keycloak Examples - CORS diff --git a/examples/demo-template/admin-access-app/pom.xml b/examples/demo-template/admin-access-app/pom.xml index 7e1f543c01..fd2565f340 100755 --- a/examples/demo-template/admin-access-app/pom.xml +++ b/examples/demo-template/admin-access-app/pom.xml @@ -21,7 +21,7 @@ keycloak-examples-demo-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT 4.0.0 diff --git a/examples/demo-template/angular-product-app/pom.xml b/examples/demo-template/angular-product-app/pom.xml index b02bddfddc..06c4b2ed08 100755 --- a/examples/demo-template/angular-product-app/pom.xml +++ b/examples/demo-template/angular-product-app/pom.xml @@ -21,7 +21,7 @@ keycloak-examples-demo-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT 4.0.0 diff --git a/examples/demo-template/customer-app-cli/pom.xml b/examples/demo-template/customer-app-cli/pom.xml index eeebf5b8c0..f58edd5749 100755 --- a/examples/demo-template/customer-app-cli/pom.xml +++ b/examples/demo-template/customer-app-cli/pom.xml @@ -21,7 +21,7 @@ keycloak-examples-demo-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT 4.0.0 diff --git a/examples/demo-template/customer-app-filter/pom.xml b/examples/demo-template/customer-app-filter/pom.xml index 444395a687..3b8146fe15 100755 --- a/examples/demo-template/customer-app-filter/pom.xml +++ b/examples/demo-template/customer-app-filter/pom.xml @@ -21,7 +21,7 @@ keycloak-examples-demo-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT 4.0.0 diff --git a/examples/demo-template/customer-app-js/pom.xml b/examples/demo-template/customer-app-js/pom.xml index 7e252c810a..e5e6ca9af5 100755 --- a/examples/demo-template/customer-app-js/pom.xml +++ b/examples/demo-template/customer-app-js/pom.xml @@ -21,7 +21,7 @@ keycloak-examples-demo-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT 4.0.0 diff --git a/examples/demo-template/customer-app/pom.xml b/examples/demo-template/customer-app/pom.xml index 51cf448e3c..7c59225647 100755 --- a/examples/demo-template/customer-app/pom.xml +++ b/examples/demo-template/customer-app/pom.xml @@ -21,7 +21,7 @@ keycloak-examples-demo-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT 4.0.0 diff --git a/examples/demo-template/database-service/pom.xml b/examples/demo-template/database-service/pom.xml index 862041b0c0..e4252a4049 100755 --- a/examples/demo-template/database-service/pom.xml +++ b/examples/demo-template/database-service/pom.xml @@ -21,7 +21,7 @@ keycloak-examples-demo-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT 4.0.0 diff --git a/examples/demo-template/example-ear/pom.xml b/examples/demo-template/example-ear/pom.xml index 8a3d4566eb..3c4d3a0f95 100755 --- a/examples/demo-template/example-ear/pom.xml +++ b/examples/demo-template/example-ear/pom.xml @@ -21,7 +21,7 @@ keycloak-examples-demo-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT 4.0.0 diff --git a/examples/demo-template/offline-access-app/pom.xml b/examples/demo-template/offline-access-app/pom.xml index d3f6b23a94..f2af2fb3fa 100755 --- a/examples/demo-template/offline-access-app/pom.xml +++ b/examples/demo-template/offline-access-app/pom.xml @@ -21,7 +21,7 @@ keycloak-examples-demo-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT 4.0.0 diff --git a/examples/demo-template/pom.xml b/examples/demo-template/pom.xml index bd239fb9cf..19f8b9184b 100755 --- a/examples/demo-template/pom.xml +++ b/examples/demo-template/pom.xml @@ -20,7 +20,7 @@ keycloak-examples-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT Demo Examples diff --git a/examples/demo-template/product-app/pom.xml b/examples/demo-template/product-app/pom.xml index 3667ab8432..c565ceefd0 100755 --- a/examples/demo-template/product-app/pom.xml +++ b/examples/demo-template/product-app/pom.xml @@ -21,7 +21,7 @@ keycloak-examples-demo-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT 4.0.0 diff --git a/examples/demo-template/service-account/pom.xml b/examples/demo-template/service-account/pom.xml index ca6152f2a2..3f8526639b 100755 --- a/examples/demo-template/service-account/pom.xml +++ b/examples/demo-template/service-account/pom.xml @@ -21,7 +21,7 @@ keycloak-examples-demo-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT 4.0.0 diff --git a/examples/demo-template/third-party-cdi/pom.xml b/examples/demo-template/third-party-cdi/pom.xml index bcbb86fc68..20a7e7eef1 100755 --- a/examples/demo-template/third-party-cdi/pom.xml +++ b/examples/demo-template/third-party-cdi/pom.xml @@ -21,7 +21,7 @@ keycloak-examples-demo-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT 4.0.0 diff --git a/examples/demo-template/third-party/pom.xml b/examples/demo-template/third-party/pom.xml index faefb5b760..6aad96ccc7 100755 --- a/examples/demo-template/third-party/pom.xml +++ b/examples/demo-template/third-party/pom.xml @@ -21,7 +21,7 @@ keycloak-examples-demo-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT 4.0.0 diff --git a/examples/fuse/camel/pom.xml b/examples/fuse/camel/pom.xml index ec500e8e41..0ca3c4e831 100755 --- a/examples/fuse/camel/pom.xml +++ b/examples/fuse/camel/pom.xml @@ -21,7 +21,7 @@ keycloak-examples-fuse-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT 4.0.0 diff --git a/examples/fuse/customer-app-fuse/pom.xml b/examples/fuse/customer-app-fuse/pom.xml index 38691f2eb7..3c4bfd414d 100755 --- a/examples/fuse/customer-app-fuse/pom.xml +++ b/examples/fuse/customer-app-fuse/pom.xml @@ -21,7 +21,7 @@ keycloak-examples-fuse-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT 4.0.0 diff --git a/examples/fuse/cxf-jaxrs/pom.xml b/examples/fuse/cxf-jaxrs/pom.xml index 31feea3841..9d3faa06c9 100755 --- a/examples/fuse/cxf-jaxrs/pom.xml +++ b/examples/fuse/cxf-jaxrs/pom.xml @@ -21,7 +21,7 @@ keycloak-examples-fuse-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT 4.0.0 diff --git a/examples/fuse/cxf-jaxws/pom.xml b/examples/fuse/cxf-jaxws/pom.xml index 24b2fde358..f53164ca97 100755 --- a/examples/fuse/cxf-jaxws/pom.xml +++ b/examples/fuse/cxf-jaxws/pom.xml @@ -21,7 +21,7 @@ keycloak-examples-fuse-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT 4.0.0 diff --git a/examples/fuse/external-config/pom.xml b/examples/fuse/external-config/pom.xml index 7d79af2e68..3f9f36df60 100755 --- a/examples/fuse/external-config/pom.xml +++ b/examples/fuse/external-config/pom.xml @@ -21,7 +21,7 @@ keycloak-examples-fuse-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT Keycloak Examples - External Config diff --git a/examples/fuse/features/pom.xml b/examples/fuse/features/pom.xml index eb1cb2b78e..a5fe72e3ea 100755 --- a/examples/fuse/features/pom.xml +++ b/examples/fuse/features/pom.xml @@ -21,7 +21,7 @@ keycloak-examples-fuse-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT 4.0.0 diff --git a/examples/fuse/pom.xml b/examples/fuse/pom.xml index cfa9d2cc71..48e01255b1 100755 --- a/examples/fuse/pom.xml +++ b/examples/fuse/pom.xml @@ -20,7 +20,7 @@ keycloak-examples-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT Fuse Examples diff --git a/examples/fuse/product-app-fuse/pom.xml b/examples/fuse/product-app-fuse/pom.xml index a4189258b4..e69578e4df 100755 --- a/examples/fuse/product-app-fuse/pom.xml +++ b/examples/fuse/product-app-fuse/pom.xml @@ -21,7 +21,7 @@ keycloak-examples-fuse-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT 4.0.0 diff --git a/examples/js-console/pom.xml b/examples/js-console/pom.xml index 00e0e6a609..37a64e2c79 100755 --- a/examples/js-console/pom.xml +++ b/examples/js-console/pom.xml @@ -21,7 +21,7 @@ keycloak-examples-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT 4.0.0 diff --git a/examples/kerberos/pom.xml b/examples/kerberos/pom.xml index 8687695e77..6d2d5b1814 100755 --- a/examples/kerberos/pom.xml +++ b/examples/kerberos/pom.xml @@ -22,7 +22,7 @@ keycloak-examples-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT Keycloak Examples - Kerberos Credential Delegation diff --git a/examples/ldap/pom.xml b/examples/ldap/pom.xml index aa506014e5..62c4d1bb67 100644 --- a/examples/ldap/pom.xml +++ b/examples/ldap/pom.xml @@ -22,7 +22,7 @@ keycloak-examples-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT 4.0.0 diff --git a/examples/multi-tenant/pom.xml b/examples/multi-tenant/pom.xml index 697878404b..6c45be0c04 100755 --- a/examples/multi-tenant/pom.xml +++ b/examples/multi-tenant/pom.xml @@ -21,7 +21,7 @@ keycloak-examples-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT Keycloak Examples - Multi Tenant diff --git a/examples/pom.xml b/examples/pom.xml index 6de9216521..0421e888cd 100755 --- a/examples/pom.xml +++ b/examples/pom.xml @@ -20,7 +20,7 @@ keycloak-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT Keycloak Examples diff --git a/examples/providers/authenticator/pom.xml b/examples/providers/authenticator/pom.xml index d3f6ffcf80..a4042c7b42 100755 --- a/examples/providers/authenticator/pom.xml +++ b/examples/providers/authenticator/pom.xml @@ -20,7 +20,7 @@ keycloak-examples-providers-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT Authenticator Example diff --git a/examples/providers/domain-extension/pom.xml b/examples/providers/domain-extension/pom.xml index db892c8714..8203215750 100755 --- a/examples/providers/domain-extension/pom.xml +++ b/examples/providers/domain-extension/pom.xml @@ -20,7 +20,7 @@ keycloak-examples-providers-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT Domain Extension Example diff --git a/examples/providers/event-listener-sysout/pom.xml b/examples/providers/event-listener-sysout/pom.xml index b2d919e7a1..6448f1ef48 100755 --- a/examples/providers/event-listener-sysout/pom.xml +++ b/examples/providers/event-listener-sysout/pom.xml @@ -20,7 +20,7 @@ keycloak-examples-providers-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT Event Listener System.out Example diff --git a/examples/providers/event-store-mem/pom.xml b/examples/providers/event-store-mem/pom.xml index 379da38763..ab431451b9 100755 --- a/examples/providers/event-store-mem/pom.xml +++ b/examples/providers/event-store-mem/pom.xml @@ -20,7 +20,7 @@ keycloak-examples-providers-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT Event Store In-Mem Example diff --git a/examples/providers/pom.xml b/examples/providers/pom.xml index 0ea9100760..a55f520678 100755 --- a/examples/providers/pom.xml +++ b/examples/providers/pom.xml @@ -20,7 +20,7 @@ keycloak-examples-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT Provider Examples diff --git a/examples/providers/rest/pom.xml b/examples/providers/rest/pom.xml index 379954f061..9570a740f7 100755 --- a/examples/providers/rest/pom.xml +++ b/examples/providers/rest/pom.xml @@ -20,7 +20,7 @@ keycloak-examples-providers-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT REST Example diff --git a/examples/providers/user-storage-jpa/pom.xml b/examples/providers/user-storage-jpa/pom.xml index 3bfc8a36a8..a1a5637ed5 100755 --- a/examples/providers/user-storage-jpa/pom.xml +++ b/examples/providers/user-storage-jpa/pom.xml @@ -20,7 +20,7 @@ keycloak-examples-providers-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT User Storage JPA Provider Exapmle diff --git a/examples/providers/user-storage-simple/pom.xml b/examples/providers/user-storage-simple/pom.xml index 065f21b5eb..36a41cb173 100755 --- a/examples/providers/user-storage-simple/pom.xml +++ b/examples/providers/user-storage-simple/pom.xml @@ -20,7 +20,7 @@ keycloak-examples-providers-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT UserStorageProvider Simple Example diff --git a/examples/saml/pom.xml b/examples/saml/pom.xml index 9aaf1261a3..e69c6df1c5 100755 --- a/examples/saml/pom.xml +++ b/examples/saml/pom.xml @@ -20,7 +20,7 @@ keycloak-examples-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT SAML Examples diff --git a/examples/saml/post-with-encryption/pom.xml b/examples/saml/post-with-encryption/pom.xml index 947083d962..d4bed04efa 100755 --- a/examples/saml/post-with-encryption/pom.xml +++ b/examples/saml/post-with-encryption/pom.xml @@ -22,7 +22,7 @@ keycloak-examples-saml-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT saml-post-encryption diff --git a/examples/saml/post-with-signature/pom.xml b/examples/saml/post-with-signature/pom.xml index 1378997f37..02713e6f6e 100755 --- a/examples/saml/post-with-signature/pom.xml +++ b/examples/saml/post-with-signature/pom.xml @@ -22,7 +22,7 @@ keycloak-examples-saml-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT sales-post-sig diff --git a/examples/saml/redirect-with-signature/pom.xml b/examples/saml/redirect-with-signature/pom.xml index 41ae3efcaf..9f42085afb 100755 --- a/examples/saml/redirect-with-signature/pom.xml +++ b/examples/saml/redirect-with-signature/pom.xml @@ -22,7 +22,7 @@ keycloak-examples-saml-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT saml-redirect-signatures diff --git a/examples/saml/servlet-filter/pom.xml b/examples/saml/servlet-filter/pom.xml index cdfcb25002..e586f3e586 100755 --- a/examples/saml/servlet-filter/pom.xml +++ b/examples/saml/servlet-filter/pom.xml @@ -22,7 +22,7 @@ keycloak-examples-saml-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT saml-servlet-filter diff --git a/examples/themes/pom.xml b/examples/themes/pom.xml index 8282f60590..7d18fdf4ce 100755 --- a/examples/themes/pom.xml +++ b/examples/themes/pom.xml @@ -20,7 +20,7 @@ keycloak-examples-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT Themes Examples diff --git a/examples/themes/src/main/resources/theme/logo-example/admin/theme.properties b/examples/themes/src/main/resources/theme/logo-example/admin/theme.properties index 3541fb4bed..7c933cfaca 100755 --- a/examples/themes/src/main/resources/theme/logo-example/admin/theme.properties +++ b/examples/themes/src/main/resources/theme/logo-example/admin/theme.properties @@ -17,4 +17,4 @@ parent=keycloak import=common/keycloak -styles=lib/patternfly/css/patternfly.css lib/select2-3.4.1/select2.css css/styles.css css/logo.css \ No newline at end of file +styles=lib/patternfly/css/patternfly.css node_modules/select2/select2.css css/styles.css css/logo.css \ No newline at end of file diff --git a/federation/kerberos/pom.xml b/federation/kerberos/pom.xml index 5d25b4acea..6b026eb08c 100755 --- a/federation/kerberos/pom.xml +++ b/federation/kerberos/pom.xml @@ -20,7 +20,7 @@ keycloak-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../../pom.xml 4.0.0 diff --git a/federation/ldap/pom.xml b/federation/ldap/pom.xml index da55bf062e..4618792225 100755 --- a/federation/ldap/pom.xml +++ b/federation/ldap/pom.xml @@ -20,7 +20,7 @@ keycloak-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../../pom.xml 4.0.0 diff --git a/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/LDAPTransaction.java b/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/LDAPTransaction.java index 1f2473baab..3cf91b9485 100644 --- a/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/LDAPTransaction.java +++ b/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/LDAPTransaction.java @@ -18,6 +18,7 @@ package org.keycloak.storage.ldap.mappers; import org.jboss.logging.Logger; +import org.keycloak.models.AbstractKeycloakTransaction; import org.keycloak.models.KeycloakTransaction; import org.keycloak.storage.ldap.LDAPStorageProvider; import org.keycloak.storage.ldap.idm.model.LDAPObject; @@ -25,12 +26,10 @@ import org.keycloak.storage.ldap.idm.model.LDAPObject; /** * @author Marek Posolda */ -public class LDAPTransaction implements KeycloakTransaction { +public class LDAPTransaction extends AbstractKeycloakTransaction { public static final Logger logger = Logger.getLogger(LDAPTransaction.class); - protected TransactionState state = TransactionState.NOT_STARTED; - private final LDAPStorageProvider ldapProvider; private final LDAPObject ldapUser; @@ -39,57 +38,21 @@ public class LDAPTransaction implements KeycloakTransaction { this.ldapUser = ldapUser; } - @Override - public void begin() { - if (state != TransactionState.NOT_STARTED) { - throw new IllegalStateException("Transaction already started"); - } - - state = TransactionState.STARTED; - } @Override - public void commit() { - if (state != TransactionState.STARTED) { - throw new IllegalStateException("Transaction in illegal state for commit: " + state); - } - + protected void commitImpl() { if (logger.isTraceEnabled()) { logger.trace("Transaction commit! Updating LDAP attributes for object " + ldapUser.getDn().toString() + ", attributes: " + ldapUser.getAttributes()); } ldapProvider.getLdapIdentityStore().update(ldapUser); - state = TransactionState.FINISHED; } - @Override - public void rollback() { - if (state != TransactionState.STARTED && state != TransactionState.ROLLBACK_ONLY) { - throw new IllegalStateException("Transaction in illegal state for rollback: " + state); - } + @Override + protected void rollbackImpl() { logger.warn("Transaction rollback! Ignoring LDAP updates for object " + ldapUser.getDn().toString()); - state = TransactionState.FINISHED; } - @Override - public void setRollbackOnly() { - state = TransactionState.ROLLBACK_ONLY; - } - - @Override - public boolean getRollbackOnly() { - return state == TransactionState.ROLLBACK_ONLY; - } - - @Override - public boolean isActive() { - return state == TransactionState.STARTED || state == TransactionState.ROLLBACK_ONLY; - } - - - protected enum TransactionState { - NOT_STARTED, STARTED, ROLLBACK_ONLY, FINISHED - } } diff --git a/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/TxAwareLDAPUserModelDelegate.java b/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/TxAwareLDAPUserModelDelegate.java index 2bf88f23dd..09f4051e36 100644 --- a/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/TxAwareLDAPUserModelDelegate.java +++ b/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/TxAwareLDAPUserModelDelegate.java @@ -41,7 +41,7 @@ public abstract class TxAwareLDAPUserModelDelegate extends UserModelDelegate { protected void ensureTransactionStarted() { LDAPTransaction transaction = provider.getUserManager().getTransaction(getId()); - if (transaction.state == LDAPTransaction.TransactionState.NOT_STARTED) { + if (transaction.getState() == LDAPTransaction.TransactionState.NOT_STARTED) { if (logger.isTraceEnabled()) { logger.trace("Starting and enlisting transaction for object " + ldapUser.getDn().toString()); } diff --git a/federation/pom.xml b/federation/pom.xml index dce02fbe7d..9136832004 100755 --- a/federation/pom.xml +++ b/federation/pom.xml @@ -22,7 +22,7 @@ keycloak-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../pom.xml 4.0.0 diff --git a/federation/sssd/pom.xml b/federation/sssd/pom.xml index 55e028ced3..1e40781d8a 100644 --- a/federation/sssd/pom.xml +++ b/federation/sssd/pom.xml @@ -4,7 +4,7 @@ keycloak-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../../pom.xml 4.0.0 diff --git a/integration/admin-client/pom.xml b/integration/admin-client/pom.xml index d59b23ed2a..ee006ef079 100755 --- a/integration/admin-client/pom.xml +++ b/integration/admin-client/pom.xml @@ -22,7 +22,7 @@ keycloak-integration-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT 4.0.0 diff --git a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/RealmResource.java b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/RealmResource.java index cba7eb3d7e..c6a1edb57b 100644 --- a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/RealmResource.java +++ b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/RealmResource.java @@ -38,6 +38,7 @@ import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; +import java.io.IOException; import java.util.List; import java.util.Map; @@ -184,6 +185,12 @@ public interface RealmResource { @QueryParam("bindDn") String bindDn, @QueryParam("bindCredential") String bindCredential, @QueryParam("useTruststoreSpi") String useTruststoreSpi, @QueryParam("connectionTimeout") String connectionTimeout); + @Path("testSMTPConnection/{config}") + @POST + @NoCache + @Consumes(MediaType.APPLICATION_JSON) + Response testSMTPConnection(final @PathParam("config") String config) throws Exception; + @Path("clear-realm-cache") @POST void clearRealmCache(); diff --git a/integration/client-cli/admin-cli/pom.xml b/integration/client-cli/admin-cli/pom.xml index 733ba06ff1..10d629bed7 100755 --- a/integration/client-cli/admin-cli/pom.xml +++ b/integration/client-cli/admin-cli/pom.xml @@ -21,7 +21,7 @@ keycloak-client-cli-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT 4.0.0 diff --git a/integration/client-cli/client-cli-dist/pom.xml b/integration/client-cli/client-cli-dist/pom.xml index 32572ce19f..38a1d5631f 100755 --- a/integration/client-cli/client-cli-dist/pom.xml +++ b/integration/client-cli/client-cli-dist/pom.xml @@ -21,7 +21,7 @@ keycloak-client-cli-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT keycloak-client-cli-dist diff --git a/integration/client-cli/client-registration-cli/pom.xml b/integration/client-cli/client-registration-cli/pom.xml index c194ddedc5..6a76bf0397 100755 --- a/integration/client-cli/client-registration-cli/pom.xml +++ b/integration/client-cli/client-registration-cli/pom.xml @@ -21,7 +21,7 @@ keycloak-client-cli-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT 4.0.0 diff --git a/integration/client-cli/pom.xml b/integration/client-cli/pom.xml index e4bf08723c..1ffbf10e33 100644 --- a/integration/client-cli/pom.xml +++ b/integration/client-cli/pom.xml @@ -20,7 +20,7 @@ keycloak-integration-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT Keycloak Client CLI diff --git a/integration/client-registration/pom.xml b/integration/client-registration/pom.xml index 979b329211..c8d2d222a8 100755 --- a/integration/client-registration/pom.xml +++ b/integration/client-registration/pom.xml @@ -21,7 +21,7 @@ keycloak-integration-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT 4.0.0 diff --git a/integration/pom.xml b/integration/pom.xml index 11266cd9f8..2c829800dd 100755 --- a/integration/pom.xml +++ b/integration/pom.xml @@ -20,7 +20,7 @@ keycloak-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../pom.xml Keycloak Integration diff --git a/misc/Testsuite.md b/misc/Testsuite.md index cb77ad7a59..7f5e036d62 100644 --- a/misc/Testsuite.md +++ b/misc/Testsuite.md @@ -29,6 +29,15 @@ When starting the server it can also import a realm from a json file: mvn exec:java -Pkeycloak-server -Dimport=testrealm.json +When starting the server, https transport can be set up by setting keystore containing the server certificate +and https port, optionally setting the truststore. + + mvn exec:java -Pkeycloak-server \ + -Djavax.net.ssl.trustStore=/path/to/truststore.jks \ + -Djavax.net.ssl.keyStore=/path/to/keystore.jks \ + -Djavax.net.ssl.keyStorePassword=CHANGEME \ + -Dkeycloak.port.https=8443 + ### Live edit of html and styles The Keycloak test server can load resources directly from the filesystem instead of the classpath. This allows editing html, styles and updating images without restarting the server. To make the server use resources from the filesystem start with: diff --git a/misc/keycloak-test-helper/pom.xml b/misc/keycloak-test-helper/pom.xml index 5a9983c092..8747ec19d9 100644 --- a/misc/keycloak-test-helper/pom.xml +++ b/misc/keycloak-test-helper/pom.xml @@ -6,7 +6,7 @@ keycloak-misc-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT org.keycloak keycloak-test-helper diff --git a/misc/pom.xml b/misc/pom.xml index b9c3b588fc..d65bbe7b96 100644 --- a/misc/pom.xml +++ b/misc/pom.xml @@ -3,7 +3,7 @@ keycloak-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT Keycloak Misc diff --git a/misc/spring-boot-starter/keycloak-spring-boot-starter/pom.xml b/misc/spring-boot-starter/keycloak-spring-boot-starter/pom.xml index f48407f504..2f55a9ac55 100644 --- a/misc/spring-boot-starter/keycloak-spring-boot-starter/pom.xml +++ b/misc/spring-boot-starter/keycloak-spring-boot-starter/pom.xml @@ -4,7 +4,7 @@ org.keycloak keycloak-spring-boot-starter-parent - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT keycloak-spring-boot-starter Keycloak :: Spring :: Boot :: Default :: Starter diff --git a/misc/spring-boot-starter/pom.xml b/misc/spring-boot-starter/pom.xml index cbea8390fc..70f7daf336 100644 --- a/misc/spring-boot-starter/pom.xml +++ b/misc/spring-boot-starter/pom.xml @@ -5,7 +5,7 @@ keycloak-misc-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT org.keycloak keycloak-spring-boot-starter-parent @@ -20,7 +20,7 @@ org.keycloak.bom keycloak-adapter-bom - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT pom import diff --git a/model/infinispan/pom.xml b/model/infinispan/pom.xml index 8917daa415..7d7dfe1584 100755 --- a/model/infinispan/pom.xml +++ b/model/infinispan/pom.xml @@ -21,7 +21,7 @@ keycloak-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../../pom.xml 4.0.0 diff --git a/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/CrossDCAwareCacheFactory.java b/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/CrossDCAwareCacheFactory.java index 17795ca213..65ca09d98e 100644 --- a/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/CrossDCAwareCacheFactory.java +++ b/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/CrossDCAwareCacheFactory.java @@ -52,6 +52,12 @@ abstract class CrossDCAwareCacheFactory { // For cross-DC scenario, we need to return underlying remoteCache for atomic operations to work properly RemoteStore remoteStore = remoteStores.iterator().next(); RemoteCache remoteCache = remoteStore.getRemoteCache(); + + if (remoteCache == null) { + String cacheName = remoteStore.getConfiguration().remoteCacheName(); + throw new IllegalStateException("Remote cache '" + cacheName + "' is not available."); + } + return new RemoteCacheWrapperFactory(remoteCache); } } diff --git a/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/InfinispanClusterProvider.java b/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/InfinispanClusterProvider.java index 5a4bdb744b..bd23e90133 100644 --- a/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/InfinispanClusterProvider.java +++ b/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/InfinispanClusterProvider.java @@ -25,6 +25,11 @@ import org.keycloak.cluster.ExecutionResult; import org.keycloak.common.util.Time; import java.util.concurrent.Callable; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.FutureTask; import java.util.concurrent.TimeUnit; /** @@ -43,11 +48,14 @@ public class InfinispanClusterProvider implements ClusterProvider { private final CrossDCAwareCacheFactory crossDCAwareCacheFactory; private final InfinispanNotificationsManager notificationsManager; // Just to extract notifications related stuff to separate class - public InfinispanClusterProvider(int clusterStartupTime, String myAddress, CrossDCAwareCacheFactory crossDCAwareCacheFactory, InfinispanNotificationsManager notificationsManager) { + private final ExecutorService localExecutor; + + public InfinispanClusterProvider(int clusterStartupTime, String myAddress, CrossDCAwareCacheFactory crossDCAwareCacheFactory, InfinispanNotificationsManager notificationsManager, ExecutorService localExecutor) { this.myAddress = myAddress; this.clusterStartupTime = clusterStartupTime; this.crossDCAwareCacheFactory = crossDCAwareCacheFactory; this.notificationsManager = notificationsManager; + this.localExecutor = localExecutor; } @@ -85,6 +93,34 @@ public class InfinispanClusterProvider implements ClusterProvider { } + @Override + public Future executeIfNotExecutedAsync(String taskKey, int taskTimeoutInSeconds, Callable task) { + TaskCallback newCallback = new TaskCallback(); + TaskCallback callback = this.notificationsManager.registerTaskCallback(TASK_KEY_PREFIX + taskKey, newCallback); + + // We successfully submitted our task + if (newCallback == callback) { + Callable wrappedTask = () -> { + boolean executed = executeIfNotExecuted(taskKey, taskTimeoutInSeconds, task).isExecuted(); + + if (!executed) { + logger.infof("Task already in progress on other cluster node. Will wait until it's finished"); + } + + callback.getTaskCompletedLatch().await(taskTimeoutInSeconds, TimeUnit.SECONDS); + return callback.isSuccess(); + }; + + Future future = localExecutor.submit(wrappedTask); + callback.setFuture(future); + } else { + logger.infof("Task already in progress on this cluster node. Will wait until it's finished"); + } + + return callback.getFuture(); + } + + @Override public void registerListener(String taskKey, ClusterListener task) { this.notificationsManager.registerListener(taskKey, task); @@ -92,11 +128,10 @@ public class InfinispanClusterProvider implements ClusterProvider { @Override - public void notify(String taskKey, ClusterEvent event, boolean ignoreSender) { - this.notificationsManager.notify(taskKey, event, ignoreSender); + public void notify(String taskKey, ClusterEvent event, boolean ignoreSender, DCNotify dcNotify) { + this.notificationsManager.notify(taskKey, event, ignoreSender, dcNotify); } - private LockEntry createLockEntry() { LockEntry lock = new LockEntry(); lock.setNode(myAddress); diff --git a/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/InfinispanClusterProviderFactory.java b/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/InfinispanClusterProviderFactory.java index a96621d7b2..330de4fd62 100644 --- a/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/InfinispanClusterProviderFactory.java +++ b/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/InfinispanClusterProviderFactory.java @@ -35,12 +35,15 @@ import org.keycloak.common.util.Time; import org.keycloak.connections.infinispan.InfinispanConnectionProvider; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.models.sessions.infinispan.util.InfinispanUtil; import java.io.Serializable; import java.util.Collection; import java.util.Iterator; import java.util.Map; import java.util.Set; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Collectors; @@ -62,17 +65,18 @@ public class InfinispanClusterProviderFactory implements ClusterProviderFactory // Ensure that atomic operations (like putIfAbsent) must work correctly in any of: non-clustered, clustered or cross-Data-Center (cross-DC) setups private CrossDCAwareCacheFactory crossDCAwareCacheFactory; - private String myAddress; - private int clusterStartupTime; // Just to extract notifications related stuff to separate class private InfinispanNotificationsManager notificationsManager; + private ExecutorService localExecutor = Executors.newCachedThreadPool(); + @Override public ClusterProvider create(KeycloakSession session) { lazyInit(session); - return new InfinispanClusterProvider(clusterStartupTime, myAddress, crossDCAwareCacheFactory, notificationsManager); + String myAddress = InfinispanUtil.getMyAddress(session); + return new InfinispanClusterProvider(clusterStartupTime, myAddress, crossDCAwareCacheFactory, notificationsManager, localExecutor); } private void lazyInit(KeycloakSession session) { @@ -83,33 +87,23 @@ public class InfinispanClusterProviderFactory implements ClusterProviderFactory workCache = ispnConnections.getCache(InfinispanConnectionProvider.WORK_CACHE_NAME); workCache.getCacheManager().addListener(new ViewChangeListener()); - initMyAddress(); - Set remoteStores = getRemoteStores(); + // See if we have RemoteStore (external JDG) configured for cross-Data-Center scenario + Set remoteStores = InfinispanUtil.getRemoteStores(workCache); crossDCAwareCacheFactory = CrossDCAwareCacheFactory.getFactory(workCache, remoteStores); clusterStartupTime = initClusterStartupTime(session); - notificationsManager = InfinispanNotificationsManager.create(workCache, myAddress, remoteStores); + String myAddress = InfinispanUtil.getMyAddress(session); + String mySite = InfinispanUtil.getMySite(session); + + notificationsManager = InfinispanNotificationsManager.create(workCache, myAddress, mySite, remoteStores); } } } } - // See if we have RemoteStore (external JDG) configured for cross-Data-Center scenario - private Set getRemoteStores() { - return workCache.getAdvancedCache().getComponentRegistry().getComponent(PersistenceManager.class).getStores(RemoteStore.class); - } - - - protected void initMyAddress() { - Transport transport = workCache.getCacheManager().getTransport(); - this.myAddress = transport == null ? HostUtils.getHostName() + "-" + workCache.hashCode() : transport.getAddress().toString(); - logger.debugf("My address: %s", this.myAddress); - } - - protected int initClusterStartupTime(KeycloakSession session) { Integer existingClusterStartTime = (Integer) crossDCAwareCacheFactory.getCache().get(InfinispanClusterProvider.CLUSTER_STARTUP_TIME_KEY); if (existingClusterStartTime != null) { @@ -201,6 +195,10 @@ public class InfinispanClusterProviderFactory implements ClusterProviderFactory if (logger.isTraceEnabled()) { logger.tracef("Removing task %s due it's node left cluster", rem); } + + // If we have task in progress, it needs to be notified + notificationsManager.taskFinished(rem, false); + cache.remove(rem); } } diff --git a/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/InfinispanNotificationsManager.java b/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/InfinispanNotificationsManager.java index fa73420ebb..0c5e6e92bc 100644 --- a/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/InfinispanNotificationsManager.java +++ b/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/InfinispanNotificationsManager.java @@ -20,32 +20,35 @@ package org.keycloak.cluster.infinispan; import java.io.Serializable; import java.util.List; import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; import java.util.concurrent.TimeUnit; import org.infinispan.Cache; import org.infinispan.client.hotrod.RemoteCache; import org.infinispan.client.hotrod.annotation.ClientCacheEntryCreated; import org.infinispan.client.hotrod.annotation.ClientCacheEntryModified; +import org.infinispan.client.hotrod.annotation.ClientCacheEntryRemoved; import org.infinispan.client.hotrod.annotation.ClientListener; import org.infinispan.client.hotrod.event.ClientCacheEntryCreatedEvent; import org.infinispan.client.hotrod.event.ClientCacheEntryModifiedEvent; -import org.infinispan.client.hotrod.event.ClientEvent; +import org.infinispan.client.hotrod.event.ClientCacheEntryRemovedEvent; import org.infinispan.context.Flag; -import org.infinispan.marshall.core.MarshalledEntry; import org.infinispan.notifications.Listener; import org.infinispan.notifications.cachelistener.annotation.CacheEntryCreated; import org.infinispan.notifications.cachelistener.annotation.CacheEntryModified; +import org.infinispan.notifications.cachelistener.annotation.CacheEntryRemoved; import org.infinispan.notifications.cachelistener.event.CacheEntryCreatedEvent; import org.infinispan.notifications.cachelistener.event.CacheEntryModifiedEvent; -import org.infinispan.persistence.manager.PersistenceManager; +import org.infinispan.notifications.cachelistener.event.CacheEntryRemovedEvent; import org.infinispan.persistence.remote.RemoteStore; -import org.infinispan.remoting.transport.Transport; import org.jboss.logging.Logger; import org.keycloak.cluster.ClusterEvent; import org.keycloak.cluster.ClusterListener; import org.keycloak.cluster.ClusterProvider; -import org.keycloak.common.util.HostUtils; -import org.keycloak.common.util.MultivaluedHashMap; +import org.keycloak.common.util.ConcurrentMultivaluedHashMap; +import org.keycloak.models.sessions.infinispan.util.InfinispanUtil; /** * Impl for sending infinispan messages across cluster and listening to them @@ -56,37 +59,52 @@ public class InfinispanNotificationsManager { protected static final Logger logger = Logger.getLogger(InfinispanNotificationsManager.class); - private final MultivaluedHashMap listeners = new MultivaluedHashMap<>(); + private final ConcurrentMultivaluedHashMap listeners = new ConcurrentMultivaluedHashMap<>(); + + private final ConcurrentMap taskCallbacks = new ConcurrentHashMap<>(); private final Cache workCache; + private final RemoteCache workRemoteCache; + private final String myAddress; + private final String mySite; - protected InfinispanNotificationsManager(Cache workCache, String myAddress) { + + protected InfinispanNotificationsManager(Cache workCache, RemoteCache workRemoteCache, String myAddress, String mySite) { this.workCache = workCache; + this.workRemoteCache = workRemoteCache; this.myAddress = myAddress; + this.mySite = mySite; } // Create and init manager including all listeners etc - public static InfinispanNotificationsManager create(Cache workCache, String myAddress, Set remoteStores) { - InfinispanNotificationsManager manager = new InfinispanNotificationsManager(workCache, myAddress); + public static InfinispanNotificationsManager create(Cache workCache, String myAddress, String mySite, Set remoteStores) { + RemoteCache workRemoteCache = null; - // We need CacheEntryListener just if we don't have remoteStore. With remoteStore will be all cluster nodes notified anyway from HotRod listener - if (remoteStores.isEmpty()) { - workCache.addListener(manager.new CacheEntryListener()); + if (!remoteStores.isEmpty()) { + RemoteStore remoteStore = remoteStores.iterator().next(); + workRemoteCache = remoteStore.getRemoteCache(); - logger.debugf("Added listener for infinispan cache: %s", workCache.getName()); - } else { - for (RemoteStore remoteStore : remoteStores) { - RemoteCache remoteCache = remoteStore.getRemoteCache(); - remoteCache.addClientListener(manager.new HotRodListener(remoteCache)); - - logger.debugf("Added listener for HotRod remoteStore cache: %s", remoteCache.getName()); + if (mySite == null) { + throw new IllegalStateException("Multiple datacenters available, but site name is not configured! Check your configuration"); } } + InfinispanNotificationsManager manager = new InfinispanNotificationsManager(workCache, workRemoteCache, myAddress, mySite); + + // We need CacheEntryListener for communication within current DC + workCache.addListener(manager.new CacheEntryListener()); + logger.debugf("Added listener for infinispan cache: %s", workCache.getName()); + + // Added listener for remoteCache to notify other DCs + if (workRemoteCache != null) { + workRemoteCache.addClientListener(manager.new HotRodListener(workRemoteCache)); + logger.debugf("Added listener for HotRod remoteStore cache: %s", workRemoteCache.getName()); + } + return manager; } @@ -96,19 +114,41 @@ public class InfinispanNotificationsManager { } - void notify(String taskKey, ClusterEvent event, boolean ignoreSender) { + TaskCallback registerTaskCallback(String taskKey, TaskCallback callback) { + TaskCallback existing = taskCallbacks.putIfAbsent(taskKey, callback); + + if (existing != null) { + return existing; + } else { + return callback; + } + } + + + void notify(String taskKey, ClusterEvent event, boolean ignoreSender, ClusterProvider.DCNotify dcNotify) { WrapperClusterEvent wrappedEvent = new WrapperClusterEvent(); + wrappedEvent.setEventKey(taskKey); wrappedEvent.setDelegateEvent(event); wrappedEvent.setIgnoreSender(ignoreSender); + wrappedEvent.setIgnoreSenderSite(dcNotify == ClusterProvider.DCNotify.ALL_BUT_LOCAL_DC); wrappedEvent.setSender(myAddress); + wrappedEvent.setSenderSite(mySite); + + String eventKey = UUID.randomUUID().toString(); if (logger.isTraceEnabled()) { - logger.tracef("Sending event %s: %s", taskKey, event); + logger.tracef("Sending event with key %s: %s", eventKey, event); } - // Put the value to the cache to notify listeners on all the nodes - workCache.getAdvancedCache().withFlags(Flag.IGNORE_RETURN_VALUES) - .put(taskKey, wrappedEvent, 120, TimeUnit.SECONDS); + if (dcNotify == ClusterProvider.DCNotify.LOCAL_DC_ONLY || workRemoteCache == null) { + // Just put it to workCache, but skip notifying remoteCache + workCache.getAdvancedCache().withFlags(Flag.IGNORE_RETURN_VALUES, Flag.SKIP_CACHE_STORE) + .put(eventKey, wrappedEvent, 120, TimeUnit.SECONDS); + } else { + // Add directly to remoteCache. Will notify remote listeners on all nodes in all DCs + RemoteCache remoteCache = InfinispanUtil.getRemoteCache(workCache); + remoteCache.put(eventKey, wrappedEvent, 120, TimeUnit.SECONDS); + } } @@ -124,6 +164,12 @@ public class InfinispanNotificationsManager { public void cacheEntryModified(CacheEntryModifiedEvent event) { eventReceived(event.getKey(), event.getValue()); } + + @CacheEntryRemoved + public void cacheEntryRemoved(CacheEntryRemovedEvent event) { + taskFinished(event.getKey(), true); + } + } @@ -150,6 +196,14 @@ public class InfinispanNotificationsManager { hotrodEventReceived(key); } + + @ClientCacheEntryRemoved + public void removed(ClientCacheEntryRemovedEvent event) { + String key = event.getKey().toString(); + taskFinished(key, true); + } + + private void hotrodEventReceived(String key) { // TODO: Look at CacheEventConverter stuff to possibly include value in the event and avoid additional remoteCache request Object value = workCache.get(key); @@ -160,6 +214,9 @@ public class InfinispanNotificationsManager { private void eventReceived(String key, Serializable obj) { if (!(obj instanceof WrapperClusterEvent)) { + if (obj == null) { + logger.warnf("Event object wasn't available in remote cache after event was received. Event key: %s", key); + } return; } @@ -171,24 +228,39 @@ public class InfinispanNotificationsManager { } } + if (event.isIgnoreSenderSite()) { + if (this.mySite == null || this.mySite.equals(event.getSenderSite())) { + return; + } + } + + String eventKey = event.getEventKey(); + if (logger.isTraceEnabled()) { - logger.tracef("Received event %s: %s", key, event); + logger.tracef("Received event: %s", event); } ClusterEvent wrappedEvent = event.getDelegateEvent(); - List myListeners = listeners.get(key); - if (myListeners != null) { - for (ClusterListener listener : myListeners) { - listener.eventReceived(wrappedEvent); - } - } - - myListeners = listeners.get(ClusterProvider.ALL); + List myListeners = listeners.get(eventKey); if (myListeners != null) { for (ClusterListener listener : myListeners) { listener.eventReceived(wrappedEvent); } } } + + + void taskFinished(String taskKey, boolean success) { + TaskCallback callback = taskCallbacks.remove(taskKey); + + if (callback != null) { + if (logger.isDebugEnabled()) { + logger.debugf("Finished task '%s' with '%b'", taskKey, success); + } + callback.setSuccess(success); + callback.getTaskCompletedLatch().countDown(); + } + + } } diff --git a/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/TaskCallback.java b/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/TaskCallback.java new file mode 100644 index 0000000000..028d743276 --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/TaskCallback.java @@ -0,0 +1,72 @@ +/* + * Copyright 2016 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.cluster.infinispan; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; + +import org.jboss.logging.Logger; + +/** + * @author Marek Posolda + */ +class TaskCallback { + + protected static final Logger logger = Logger.getLogger(TaskCallback.class); + + static final int LATCH_TIMEOUT_MS = 10000; + + private volatile boolean success; + + private volatile Future future; + + private final CountDownLatch taskCompletedLatch = new CountDownLatch(1); + private final CountDownLatch futureAvailableLatch = new CountDownLatch(1); + + + public void setSuccess(boolean success) { + this.success = success; + } + + public boolean isSuccess() { + return success; + } + + public void setFuture(Future future) { + this.future = future; + this.futureAvailableLatch.countDown(); + } + + + public Future getFuture() { + try { + this.futureAvailableLatch.await(LATCH_TIMEOUT_MS, TimeUnit.MILLISECONDS); + } catch (InterruptedException ie) { + logger.error("Interrupted thread!"); + Thread.currentThread().interrupt(); + } + + return future; + } + + + public CountDownLatch getTaskCompletedLatch() { + return taskCompletedLatch; + } +} diff --git a/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/WrapperClusterEvent.java b/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/WrapperClusterEvent.java index b03dd70c0a..0e58275bcc 100644 --- a/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/WrapperClusterEvent.java +++ b/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/WrapperClusterEvent.java @@ -24,10 +24,21 @@ import org.keycloak.cluster.ClusterEvent; */ public class WrapperClusterEvent implements ClusterEvent { - private String sender; // will be null in non-clustered environment + private String eventKey; + private String sender; + private String senderSite; private boolean ignoreSender; + private boolean ignoreSenderSite; private ClusterEvent delegateEvent; + public String getEventKey() { + return eventKey; + } + + public void setEventKey(String eventKey) { + this.eventKey = eventKey; + } + public String getSender() { return sender; } @@ -36,6 +47,14 @@ public class WrapperClusterEvent implements ClusterEvent { this.sender = sender; } + public String getSenderSite() { + return senderSite; + } + + public void setSenderSite(String senderSite) { + this.senderSite = senderSite; + } + public boolean isIgnoreSender() { return ignoreSender; } @@ -44,6 +63,14 @@ public class WrapperClusterEvent implements ClusterEvent { this.ignoreSender = ignoreSender; } + public boolean isIgnoreSenderSite() { + return ignoreSenderSite; + } + + public void setIgnoreSenderSite(boolean ignoreSenderSite) { + this.ignoreSenderSite = ignoreSenderSite; + } + public ClusterEvent getDelegateEvent() { return delegateEvent; } @@ -54,6 +81,6 @@ public class WrapperClusterEvent implements ClusterEvent { @Override public String toString() { - return String.format("WrapperClusterEvent [ sender=%s, delegateEvent=%s ]", sender, delegateEvent.toString()); + return String.format("WrapperClusterEvent [ eventKey=%s, sender=%s, senderSite=%s, delegateEvent=%s ]", eventKey, sender, senderSite, delegateEvent.toString()); } } diff --git a/model/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProvider.java b/model/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProvider.java index 71a2ebaf94..d95e4a4bb1 100644 --- a/model/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProvider.java +++ b/model/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProvider.java @@ -26,9 +26,13 @@ import org.infinispan.manager.EmbeddedCacheManager; public class DefaultInfinispanConnectionProvider implements InfinispanConnectionProvider { private EmbeddedCacheManager cacheManager; + private final String siteName; + private final String nodeName; - public DefaultInfinispanConnectionProvider(EmbeddedCacheManager cacheManager) { + public DefaultInfinispanConnectionProvider(EmbeddedCacheManager cacheManager, String nodeName, String siteName) { this.cacheManager = cacheManager; + this.nodeName = nodeName; + this.siteName = siteName; } @Override @@ -36,6 +40,16 @@ public class DefaultInfinispanConnectionProvider implements InfinispanConnection return cacheManager.getCache(name); } + @Override + public String getNodeName() { + return nodeName; + } + + @Override + public String getSiteName() { + return siteName; + } + @Override public void close() { } diff --git a/model/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProviderFactory.java b/model/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProviderFactory.java index a9df0471c1..86f607456f 100755 --- a/model/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProviderFactory.java +++ b/model/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProviderFactory.java @@ -17,6 +17,7 @@ package org.keycloak.connections.infinispan; +import java.security.SecureRandom; import java.util.concurrent.TimeUnit; import org.infinispan.commons.util.FileLookup; @@ -30,6 +31,7 @@ import org.infinispan.eviction.EvictionType; import org.infinispan.manager.DefaultCacheManager; import org.infinispan.manager.EmbeddedCacheManager; import org.infinispan.persistence.remote.configuration.RemoteStoreConfigurationBuilder; +import org.infinispan.remoting.transport.Transport; import org.infinispan.remoting.transport.jgroups.JGroupsTransport; import org.infinispan.transaction.LockingMode; import org.infinispan.transaction.TransactionMode; @@ -38,8 +40,12 @@ import org.jboss.logging.Logger; import org.jgroups.JChannel; import org.keycloak.Config; import org.keycloak.cluster.infinispan.KeycloakHotRodMarshallerFactory; +import org.keycloak.common.util.HostUtils; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.models.sessions.infinispan.remotestore.KcRemoteStoreConfigurationBuilder; +import org.keycloak.models.sessions.infinispan.util.InfinispanUtil; +import org.keycloak.models.utils.KeycloakModelUtils; import javax.naming.InitialContext; @@ -56,11 +62,15 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon protected boolean containerManaged; + private String nodeName; + + private String siteName; + @Override public InfinispanConnectionProvider create(KeycloakSession session) { lazyInit(); - return new DefaultInfinispanConnectionProvider(cacheManager); + return new DefaultInfinispanConnectionProvider(cacheManager, nodeName, siteName); } @Override @@ -96,6 +106,8 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon } else { initEmbedded(); } + + logger.infof("Node name: %s, Site name: %s", nodeName, siteName); } } } @@ -134,7 +146,20 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon cacheManager.defineConfiguration(InfinispanConnectionProvider.AUTHORIZATION_REVISIONS_CACHE_NAME, getRevisionCacheConfig(authzRevisionsMaxEntries)); cacheManager.getCache(InfinispanConnectionProvider.AUTHORIZATION_REVISIONS_CACHE_NAME, true); - + Transport transport = cacheManager.getTransport(); + if (transport != null) { + this.nodeName = transport.getAddress().toString(); + this.siteName = cacheManager.getCacheManagerConfiguration().transport().siteId(); + if (this.siteName == null) { + this.siteName = System.getProperty(InfinispanConnectionProvider.JBOSS_SITE_NAME); + } + } else { + this.nodeName = System.getProperty(InfinispanConnectionProvider.JBOSS_NODE_NAME); + this.siteName = System.getProperty(InfinispanConnectionProvider.JBOSS_SITE_NAME); + } + if (this.nodeName == null || this.nodeName.equals("localhost")) { + this.nodeName = generateNodeName(); + } logger.debugv("Using container managed Infinispan cache container, lookup={1}", cacheContainerLookup); } catch (Exception e) { @@ -152,13 +177,27 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon boolean async = config.getBoolean("async", false); boolean allowDuplicateJMXDomains = config.getBoolean("allowDuplicateJMXDomains", true); + this.nodeName = config.get("nodeName", System.getProperty(InfinispanConnectionProvider.JBOSS_NODE_NAME)); + if (this.nodeName != null && this.nodeName.isEmpty()) { + this.nodeName = null; + } + + this.siteName = config.get("siteName", System.getProperty(InfinispanConnectionProvider.JBOSS_SITE_NAME)); + if (this.siteName != null && this.siteName.isEmpty()) { + this.siteName = null; + } + if (clustered) { - String nodeName = config.get("nodeName", System.getProperty(InfinispanConnectionProvider.JBOSS_NODE_NAME)); String jgroupsUdpMcastAddr = config.get("jgroupsUdpMcastAddr", System.getProperty(InfinispanConnectionProvider.JGROUPS_UDP_MCAST_ADDR)); - configureTransport(gcb, nodeName, jgroupsUdpMcastAddr); + configureTransport(gcb, nodeName, siteName, jgroupsUdpMcastAddr); gcb.globalJmxStatistics() .jmxDomain(InfinispanConnectionProvider.JMX_DOMAIN + "-" + nodeName); + } else { + if (nodeName == null) { + nodeName = generateNodeName(); + } } + gcb.globalJmxStatistics() .allowDuplicateDomains(allowDuplicateJMXDomains) .enable(); @@ -166,6 +205,10 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon cacheManager = new DefaultCacheManager(gcb.build()); containerManaged = false; + if (cacheManager.getTransport() != null) { + nodeName = cacheManager.getTransport().getAddress().toString(); + } + logger.debug("Started embedded Infinispan cache container"); ConfigurationBuilder modelCacheConfigBuilder = new ConfigurationBuilder(); @@ -198,11 +241,29 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon .build(); } + // Base configuration doesn't contain any remote stores + Configuration sessionCacheConfigurationBase = sessionConfigBuilder.build(); + + boolean jdgEnabled = config.getBoolean("remoteStoreEnabled", false); + + if (jdgEnabled) { + sessionConfigBuilder = new ConfigurationBuilder(); + sessionConfigBuilder.read(sessionCacheConfigurationBase); + configureRemoteCacheStore(sessionConfigBuilder, async, InfinispanConnectionProvider.SESSION_CACHE_NAME, KcRemoteStoreConfigurationBuilder.class); + } Configuration sessionCacheConfiguration = sessionConfigBuilder.build(); cacheManager.defineConfiguration(InfinispanConnectionProvider.SESSION_CACHE_NAME, sessionCacheConfiguration); + + if (jdgEnabled) { + sessionConfigBuilder = new ConfigurationBuilder(); + sessionConfigBuilder.read(sessionCacheConfigurationBase); + configureRemoteCacheStore(sessionConfigBuilder, async, InfinispanConnectionProvider.OFFLINE_SESSION_CACHE_NAME, KcRemoteStoreConfigurationBuilder.class); + } + sessionCacheConfiguration = sessionConfigBuilder.build(); cacheManager.defineConfiguration(InfinispanConnectionProvider.OFFLINE_SESSION_CACHE_NAME, sessionCacheConfiguration); - cacheManager.defineConfiguration(InfinispanConnectionProvider.LOGIN_FAILURE_CACHE_NAME, sessionCacheConfiguration); - cacheManager.defineConfiguration(InfinispanConnectionProvider.AUTHENTICATION_SESSIONS_CACHE_NAME, sessionCacheConfiguration); + + cacheManager.defineConfiguration(InfinispanConnectionProvider.LOGIN_FAILURE_CACHE_NAME, sessionCacheConfigurationBase); + cacheManager.defineConfiguration(InfinispanConnectionProvider.AUTHENTICATION_SESSIONS_CACHE_NAME, sessionCacheConfigurationBase); // Retrieve caches to enforce rebalance cacheManager.getCache(InfinispanConnectionProvider.SESSION_CACHE_NAME, true); @@ -215,9 +276,8 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon replicationConfigBuilder.clustering().cacheMode(async ? CacheMode.REPL_ASYNC : CacheMode.REPL_SYNC); } - boolean jdgEnabled = config.getBoolean("remoteStoreEnabled", false); if (jdgEnabled) { - configureRemoteCacheStore(replicationConfigBuilder, async); + configureRemoteCacheStore(replicationConfigBuilder, async, InfinispanConnectionProvider.WORK_CACHE_NAME, RemoteStoreConfigurationBuilder.class); } Configuration replicationEvictionCacheConfiguration = replicationConfigBuilder.build(); @@ -267,6 +327,10 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon cacheManager.getCache(InfinispanConnectionProvider.AUTHORIZATION_REVISIONS_CACHE_NAME, true); } + protected String generateNodeName() { + return InfinispanConnectionProvider.NODE_PREFIX + new SecureRandom().nextInt(1000000); + } + private Configuration getRevisionCacheConfig(long maxEntries) { ConfigurationBuilder cb = new ConfigurationBuilder(); cb.invocationBatching().enable().transaction().transactionMode(TransactionMode.TRANSACTIONAL); @@ -281,19 +345,19 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon } // Used for cross-data centers scenario. Usually integration with external JDG server, which itself handles communication between DCs. - private void configureRemoteCacheStore(ConfigurationBuilder builder, boolean async) { + private void configureRemoteCacheStore(ConfigurationBuilder builder, boolean async, String cacheName, Class configBuilderClass) { String jdgServer = config.get("remoteStoreServer", "localhost"); Integer jdgPort = config.getInt("remoteStorePort", 11222); builder.persistence() .passivation(false) - .addStore(RemoteStoreConfigurationBuilder.class) + .addStore(configBuilderClass) .fetchPersistentState(false) .ignoreModifications(false) .purgeOnStartup(false) .preload(false) .shared(true) - .remoteCacheName(InfinispanConnectionProvider.WORK_CACHE_NAME) + .remoteCacheName(cacheName) .rawValues(true) .forceReturnValues(false) .marshaller(KeycloakHotRodMarshallerFactory.class.getName()) @@ -355,7 +419,7 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon private static final Object CHANNEL_INIT_SYNCHRONIZER = new Object(); - protected void configureTransport(GlobalConfigurationBuilder gcb, String nodeName, String jgroupsUdpMcastAddr) { + protected void configureTransport(GlobalConfigurationBuilder gcb, String nodeName, String siteName, String jgroupsUdpMcastAddr) { if (nodeName == null) { gcb.transport().defaultTransport(); } else { @@ -376,6 +440,7 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon gcb.transport() .nodeName(nodeName) + .siteId(siteName) .transport(transport) .globalJmxStatistics() .jmxDomain(InfinispanConnectionProvider.JMX_DOMAIN + "-" + nodeName) diff --git a/model/infinispan/src/main/java/org/keycloak/connections/infinispan/InfinispanConnectionProvider.java b/model/infinispan/src/main/java/org/keycloak/connections/infinispan/InfinispanConnectionProvider.java index e8cdbf6885..9c3d437de9 100755 --- a/model/infinispan/src/main/java/org/keycloak/connections/infinispan/InfinispanConnectionProvider.java +++ b/model/infinispan/src/main/java/org/keycloak/connections/infinispan/InfinispanConnectionProvider.java @@ -55,8 +55,25 @@ public interface InfinispanConnectionProvider extends Provider { String JBOSS_NODE_NAME = "jboss.node.name"; String JGROUPS_UDP_MCAST_ADDR = "jgroups.udp.mcast_addr"; + // TODO This property is not in Wildfly. Check if corresponding property in Wildfly exists + String JBOSS_SITE_NAME = "jboss.site.name"; + String JMX_DOMAIN = "jboss.datagrid-infinispan"; + // Constant used as the prefix of the current node if "jboss.node.name" is not configured + String NODE_PREFIX = "node_"; + Cache getCache(String name); + /** + * @return Address of current node in cluster. In non-cluster environment, it returns some other non-null value (eg. hostname with some random value like "host-123456" ) + */ + String getNodeName(); + + /** + * + * @return siteName or null if we're not in environment with multiple sites (data centers) + */ + String getSiteName(); + } diff --git a/model/infinispan/src/main/java/org/keycloak/keys/infinispan/InfinispanPublicKeyStorageProvider.java b/model/infinispan/src/main/java/org/keycloak/keys/infinispan/InfinispanPublicKeyStorageProvider.java index b5f48cd5bb..52a509ec04 100644 --- a/model/infinispan/src/main/java/org/keycloak/keys/infinispan/InfinispanPublicKeyStorageProvider.java +++ b/model/infinispan/src/main/java/org/keycloak/keys/infinispan/InfinispanPublicKeyStorageProvider.java @@ -69,7 +69,7 @@ public class InfinispanPublicKeyStorageProvider implements PublicKeyStorageProvi public void clearCache() { keys.clear(); ClusterProvider cluster = session.getProvider(ClusterProvider.class); - cluster.notify(InfinispanPublicKeyStorageProviderFactory.KEYS_CLEAR_CACHE_EVENTS, new ClearCacheEvent(), true); + cluster.notify(InfinispanPublicKeyStorageProviderFactory.KEYS_CLEAR_CACHE_EVENTS, new ClearCacheEvent(), true, ClusterProvider.DCNotify.ALL_DCS); } @@ -122,7 +122,7 @@ public class InfinispanPublicKeyStorageProvider implements PublicKeyStorageProvi for (String cacheKey : invalidations) { keys.remove(cacheKey); - cluster.notify(cacheKey, PublicKeyStorageInvalidationEvent.create(cacheKey), true); + cluster.notify(InfinispanPublicKeyStorageProviderFactory.PUBLIC_KEY_STORAGE_INVALIDATION_EVENT, PublicKeyStorageInvalidationEvent.create(cacheKey), true, ClusterProvider.DCNotify.ALL_DCS); } } diff --git a/model/infinispan/src/main/java/org/keycloak/keys/infinispan/InfinispanPublicKeyStorageProviderFactory.java b/model/infinispan/src/main/java/org/keycloak/keys/infinispan/InfinispanPublicKeyStorageProviderFactory.java index 42a73fca17..e8872a7f3e 100644 --- a/model/infinispan/src/main/java/org/keycloak/keys/infinispan/InfinispanPublicKeyStorageProviderFactory.java +++ b/model/infinispan/src/main/java/org/keycloak/keys/infinispan/InfinispanPublicKeyStorageProviderFactory.java @@ -50,6 +50,8 @@ public class InfinispanPublicKeyStorageProviderFactory implements PublicKeyStora public static final String KEYS_CLEAR_CACHE_EVENTS = "KEYS_CLEAR_CACHE_EVENTS"; + public static final String PUBLIC_KEY_STORAGE_INVALIDATION_EVENT = "PUBLIC_KEY_STORAGE_INVALIDATION_EVENT"; + private volatile Cache keysCache; private final Map> tasksInProgress = new ConcurrentHashMap<>(); @@ -69,12 +71,10 @@ public class InfinispanPublicKeyStorageProviderFactory implements PublicKeyStora this.keysCache = session.getProvider(InfinispanConnectionProvider.class).getCache(InfinispanConnectionProvider.KEYS_CACHE_NAME); ClusterProvider cluster = session.getProvider(ClusterProvider.class); - cluster.registerListener(ClusterProvider.ALL, (ClusterEvent event) -> { + cluster.registerListener(PUBLIC_KEY_STORAGE_INVALIDATION_EVENT, (ClusterEvent event) -> { - if (event instanceof PublicKeyStorageInvalidationEvent) { - PublicKeyStorageInvalidationEvent invalidationEvent = (PublicKeyStorageInvalidationEvent) event; - keysCache.remove(invalidationEvent.getCacheKey()); - } + PublicKeyStorageInvalidationEvent invalidationEvent = (PublicKeyStorageInvalidationEvent) event; + keysCache.remove(invalidationEvent.getCacheKey()); }); diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/CacheManager.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/CacheManager.java index 4480f7af9a..9a0839fe4f 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/CacheManager.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/CacheManager.java @@ -198,22 +198,15 @@ public abstract class CacheManager { } - public void sendInvalidationEvents(KeycloakSession session, Collection invalidationEvents) { + public void sendInvalidationEvents(KeycloakSession session, Collection invalidationEvents, String eventKey) { ClusterProvider clusterProvider = session.getProvider(ClusterProvider.class); // Maybe add InvalidationEvent, which will be collection of all invalidationEvents? That will reduce cluster traffic even more. for (InvalidationEvent event : invalidationEvents) { - clusterProvider.notify(generateEventId(event), event, true); + clusterProvider.notify(eventKey, event, true, ClusterProvider.DCNotify.ALL_DCS); } } - protected String generateEventId(InvalidationEvent event) { - return new StringBuilder(event.getId()) - .append("_") - .append(event.hashCode()) - .toString(); - } - public void invalidationEventReceived(InvalidationEvent event) { Set invalidations = new HashSet<>(); diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/InfinispanCacheRealmProviderFactory.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/InfinispanCacheRealmProviderFactory.java index c2ad8cef40..ef2ce2b0f8 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/InfinispanCacheRealmProviderFactory.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/InfinispanCacheRealmProviderFactory.java @@ -38,6 +38,7 @@ public class InfinispanCacheRealmProviderFactory implements CacheRealmProviderFa private static final Logger log = Logger.getLogger(InfinispanCacheRealmProviderFactory.class); public static final String REALM_CLEAR_CACHE_EVENTS = "REALM_CLEAR_CACHE_EVENTS"; + public static final String REALM_INVALIDATION_EVENTS = "REALM_INVALIDATION_EVENTS"; protected volatile RealmCacheManager realmCache; @@ -56,12 +57,11 @@ public class InfinispanCacheRealmProviderFactory implements CacheRealmProviderFa realmCache = new RealmCacheManager(cache, revisions); ClusterProvider cluster = session.getProvider(ClusterProvider.class); - cluster.registerListener(ClusterProvider.ALL, (ClusterEvent event) -> { + cluster.registerListener(REALM_INVALIDATION_EVENTS, (ClusterEvent event) -> { + + InvalidationEvent invalidationEvent = (InvalidationEvent) event; + realmCache.invalidationEventReceived(invalidationEvent); - if (event instanceof InvalidationEvent) { - InvalidationEvent invalidationEvent = (InvalidationEvent) event; - realmCache.invalidationEventReceived(invalidationEvent); - } }); cluster.registerListener(REALM_CLEAR_CACHE_EVENTS, (ClusterEvent event) -> { diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/InfinispanUserCacheProviderFactory.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/InfinispanUserCacheProviderFactory.java index e8c2ba14fb..4d0f445dea 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/InfinispanUserCacheProviderFactory.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/InfinispanUserCacheProviderFactory.java @@ -37,6 +37,7 @@ public class InfinispanUserCacheProviderFactory implements UserCacheProviderFact private static final Logger log = Logger.getLogger(InfinispanUserCacheProviderFactory.class); public static final String USER_CLEAR_CACHE_EVENTS = "USER_CLEAR_CACHE_EVENTS"; + public static final String USER_INVALIDATION_EVENTS = "USER_INVALIDATION_EVENTS"; protected volatile UserCacheManager userCache; @@ -58,12 +59,10 @@ public class InfinispanUserCacheProviderFactory implements UserCacheProviderFact ClusterProvider cluster = session.getProvider(ClusterProvider.class); - cluster.registerListener(ClusterProvider.ALL, (ClusterEvent event) -> { + cluster.registerListener(USER_INVALIDATION_EVENTS, (ClusterEvent event) -> { - if (event instanceof InvalidationEvent) { - InvalidationEvent invalidationEvent = (InvalidationEvent) event; - userCache.invalidationEventReceived(invalidationEvent); - } + InvalidationEvent invalidationEvent = (InvalidationEvent) event; + userCache.invalidationEventReceived(invalidationEvent); }); diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java index 53effda840..af7159cad6 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java @@ -1018,6 +1018,18 @@ public class RealmAdapter implements CachedRealmModel { updated.setClientAuthenticationFlow(flow); } + @Override + public AuthenticationFlowModel getDockerAuthenticationFlow() { + if (isUpdated()) return updated.getDockerAuthenticationFlow(); + return cached.getDockerAuthenticationFlow(); + } + + @Override + public void setDockerAuthenticationFlow(final AuthenticationFlowModel flow) { + getDelegateForUpdate(); + updated.setDockerAuthenticationFlow(flow); + } + @Override public List getAuthenticationFlows() { if (isUpdated()) return updated.getAuthenticationFlows(); diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheManager.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheManager.java index b01dbabf31..ed5db84720 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheManager.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheManager.java @@ -95,11 +95,9 @@ public class RealmCacheManager extends CacheManager { @Override protected void addInvalidationsFromEvent(InvalidationEvent event, Set invalidations) { - if (event instanceof RealmCacheInvalidationEvent) { - invalidations.add(event.getId()); + invalidations.add(event.getId()); - ((RealmCacheInvalidationEvent) event).addInvalidations(this, invalidations); - } + ((RealmCacheInvalidationEvent) event).addInvalidations(this, invalidations); } } diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheSession.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheSession.java index f9bf0d7e50..77f8981ed1 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheSession.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheSession.java @@ -132,7 +132,7 @@ public class RealmCacheSession implements CacheRealmProvider { @Override public void clear() { ClusterProvider cluster = session.getProvider(ClusterProvider.class); - cluster.notify(InfinispanCacheRealmProviderFactory.REALM_CLEAR_CACHE_EVENTS, new ClearCacheEvent(), false); + cluster.notify(InfinispanCacheRealmProviderFactory.REALM_CLEAR_CACHE_EVENTS, new ClearCacheEvent(), false, ClusterProvider.DCNotify.ALL_DCS); } @Override @@ -264,7 +264,7 @@ public class RealmCacheSession implements CacheRealmProvider { cache.invalidateObject(id); } - cache.sendInvalidationEvents(session, invalidationEvents); + cache.sendInvalidationEvents(session, invalidationEvents, InfinispanCacheRealmProviderFactory.REALM_INVALIDATION_EVENTS); } private KeycloakTransaction getPrepareTransaction() { diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheManager.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheManager.java index e9493144e1..9126b2f499 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheManager.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheManager.java @@ -95,9 +95,7 @@ public class UserCacheManager extends CacheManager { @Override protected void addInvalidationsFromEvent(InvalidationEvent event, Set invalidations) { - if (event instanceof UserCacheInvalidationEvent) { - ((UserCacheInvalidationEvent) event).addInvalidations(this, invalidations); - } + ((UserCacheInvalidationEvent) event).addInvalidations(this, invalidations); } public void invalidateRealmUsers(String realm, Set invalidations) { diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheSession.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheSession.java index ef19b84b4b..0d971f70b9 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheSession.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheSession.java @@ -90,7 +90,7 @@ public class UserCacheSession implements UserCache { public void clear() { cache.clear(); ClusterProvider cluster = session.getProvider(ClusterProvider.class); - cluster.notify(InfinispanUserCacheProviderFactory.USER_CLEAR_CACHE_EVENTS, new ClearCacheEvent(), true); + cluster.notify(InfinispanUserCacheProviderFactory.USER_CLEAR_CACHE_EVENTS, new ClearCacheEvent(), true, ClusterProvider.DCNotify.ALL_DCS); } public UserProvider getDelegate() { @@ -129,7 +129,7 @@ public class UserCacheSession implements UserCache { cache.invalidateObject(invalidation); } - cache.sendInvalidationEvents(session, invalidationEvents); + cache.sendInvalidationEvents(session, invalidationEvents, InfinispanUserCacheProviderFactory.USER_INVALIDATION_EVENTS); } private KeycloakTransaction getTransaction() { diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/InfinispanCacheStoreFactoryProviderFactory.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/InfinispanCacheStoreFactoryProviderFactory.java index c74e4fedf7..8a9dd059f9 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/InfinispanCacheStoreFactoryProviderFactory.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/InfinispanCacheStoreFactoryProviderFactory.java @@ -41,6 +41,7 @@ public class InfinispanCacheStoreFactoryProviderFactory implements CachedStorePr private static final Logger log = Logger.getLogger(InfinispanCacheStoreFactoryProviderFactory.class); public static final String AUTHORIZATION_CLEAR_CACHE_EVENTS = "AUTHORIZATION_CLEAR_CACHE_EVENTS"; + public static final String AUTHORIZATION_INVALIDATION_EVENTS = "AUTHORIZATION_INVALIDATION_EVENTS"; protected volatile StoreFactoryCacheManager storeCache; @@ -59,11 +60,11 @@ public class InfinispanCacheStoreFactoryProviderFactory implements CachedStorePr storeCache = new StoreFactoryCacheManager(cache, revisions); ClusterProvider cluster = session.getProvider(ClusterProvider.class); - cluster.registerListener(ClusterProvider.ALL, (ClusterEvent event) -> { - if (event instanceof InvalidationEvent) { - InvalidationEvent invalidationEvent = (InvalidationEvent) event; - storeCache.invalidationEventReceived(invalidationEvent); - } + cluster.registerListener(AUTHORIZATION_INVALIDATION_EVENTS, (ClusterEvent event) -> { + + InvalidationEvent invalidationEvent = (InvalidationEvent) event; + storeCache.invalidationEventReceived(invalidationEvent); + }); cluster.registerListener(AUTHORIZATION_CLEAR_CACHE_EVENTS, (ClusterEvent event) -> storeCache.clear()); diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/StoreFactoryCacheSession.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/StoreFactoryCacheSession.java index 10be78d971..a169235643 100644 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/StoreFactoryCacheSession.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/StoreFactoryCacheSession.java @@ -216,7 +216,7 @@ public class StoreFactoryCacheSession implements CachedStoreFactoryProvider { cache.invalidateObject(id); } - cache.sendInvalidationEvents(session, invalidationEvents); + cache.sendInvalidationEvents(session, invalidationEvents, InfinispanCacheStoreFactoryProviderFactory.AUTHORIZATION_INVALIDATION_EVENTS); } diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedRealm.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedRealm.java index 3668d9740e..160fee56b6 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedRealm.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedRealm.java @@ -117,6 +117,7 @@ public class CachedRealm extends AbstractExtendableRevisioned { protected AuthenticationFlowModel directGrantFlow; protected AuthenticationFlowModel resetCredentialsFlow; protected AuthenticationFlowModel clientAuthenticationFlow; + protected AuthenticationFlowModel dockerAuthenticationFlow; protected boolean eventsEnabled; protected long eventsExpiration; @@ -252,6 +253,7 @@ public class CachedRealm extends AbstractExtendableRevisioned { directGrantFlow = model.getDirectGrantFlow(); resetCredentialsFlow = model.getResetCredentialsFlow(); clientAuthenticationFlow = model.getClientAuthenticationFlow(); + dockerAuthenticationFlow = model.getDockerAuthenticationFlow(); for (ComponentModel component : model.getComponents()) { componentsByParentAndType.add(component.getParentId() + component.getProviderType(), component); @@ -547,6 +549,10 @@ public class CachedRealm extends AbstractExtendableRevisioned { return clientAuthenticationFlow; } + public AuthenticationFlowModel getDockerAuthenticationFlow() { + return dockerAuthenticationFlow; + } + public List getDefaultGroups() { return defaultGroups; } diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/AuthenticatedClientSessionAdapter.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/AuthenticatedClientSessionAdapter.java index 7772bc20f2..be086b8eb0 100644 --- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/AuthenticatedClientSessionAdapter.java +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/AuthenticatedClientSessionAdapter.java @@ -22,13 +22,17 @@ import java.util.HashMap; import java.util.Map; import java.util.Set; -import org.infinispan.Cache; import org.keycloak.models.AuthenticatedClientSessionModel; import org.keycloak.models.ClientModel; +import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserSessionModel; +import org.keycloak.models.sessions.infinispan.changes.InfinispanChangelogBasedTransaction; +import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper; +import org.keycloak.models.sessions.infinispan.changes.SessionUpdateTask; +import org.keycloak.models.sessions.infinispan.changes.UserSessionClientSessionUpdateTask; +import org.keycloak.models.sessions.infinispan.changes.UserSessionUpdateTask; import org.keycloak.models.sessions.infinispan.entities.AuthenticatedClientSessionEntity; -import org.keycloak.models.sessions.infinispan.entities.SessionEntity; import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity; /** @@ -39,19 +43,20 @@ public class AuthenticatedClientSessionAdapter implements AuthenticatedClientSes private final AuthenticatedClientSessionEntity entity; private final ClientModel client; private final InfinispanUserSessionProvider provider; - private final Cache cache; + private final InfinispanChangelogBasedTransaction updateTx; private UserSessionAdapter userSession; - public AuthenticatedClientSessionAdapter(AuthenticatedClientSessionEntity entity, ClientModel client, UserSessionAdapter userSession, InfinispanUserSessionProvider provider, Cache cache) { + public AuthenticatedClientSessionAdapter(AuthenticatedClientSessionEntity entity, ClientModel client, UserSessionAdapter userSession, + InfinispanUserSessionProvider provider, InfinispanChangelogBasedTransaction updateTx) { this.provider = provider; this.entity = entity; this.client = client; - this.cache = cache; + this.updateTx = updateTx; this.userSession = userSession; } - private void update() { - provider.getTx().replace(cache, userSession.getEntity().getId(), userSession.getEntity()); + private void update(UserSessionUpdateTask task) { + updateTx.addTask(userSession.getId(), task); } @@ -62,15 +67,27 @@ public class AuthenticatedClientSessionAdapter implements AuthenticatedClientSes // Dettach userSession if (userSession == null) { - if (sessionEntity.getAuthenticatedClientSessions() != null) { - sessionEntity.getAuthenticatedClientSessions().remove(clientUUID); - update(); - this.userSession = null; - } + UserSessionUpdateTask task = new UserSessionUpdateTask() { + + @Override + public void runUpdate(UserSessionEntity sessionEntity) { + sessionEntity.getAuthenticatedClientSessions().remove(clientUUID); + } + + }; + update(task); + this.userSession = null; } else { this.userSession = (UserSessionAdapter) userSession; - sessionEntity.getAuthenticatedClientSessions().put(clientUUID, entity); - update(); + UserSessionUpdateTask task = new UserSessionUpdateTask() { + + @Override + public void runUpdate(UserSessionEntity sessionEntity) { + sessionEntity.getAuthenticatedClientSessions().put(clientUUID, entity); + } + + }; + update(task); } } @@ -86,8 +103,16 @@ public class AuthenticatedClientSessionAdapter implements AuthenticatedClientSes @Override public void setRedirectUri(String uri) { - entity.setRedirectUri(uri); - update(); + UserSessionClientSessionUpdateTask task = new UserSessionClientSessionUpdateTask(client.getId()) { + + @Override + protected void runClientSessionUpdate(AuthenticatedClientSessionEntity entity) { + entity.setRedirectUri(uri); + } + + }; + + update(task); } @Override @@ -112,8 +137,22 @@ public class AuthenticatedClientSessionAdapter implements AuthenticatedClientSes @Override public void setTimestamp(int timestamp) { - entity.setTimestamp(timestamp); - update(); + UserSessionClientSessionUpdateTask task = new UserSessionClientSessionUpdateTask(client.getId()) { + + @Override + protected void runClientSessionUpdate(AuthenticatedClientSessionEntity entity) { + entity.setTimestamp(timestamp); + } + + @Override + public CrossDCMessageStatus getCrossDCMessageStatus(SessionEntityWrapper sessionWrapper) { + // We usually update lastSessionRefresh at the same time. That would handle it. + return CrossDCMessageStatus.NOT_NEEDED; + } + + }; + + update(task); } @Override @@ -123,8 +162,16 @@ public class AuthenticatedClientSessionAdapter implements AuthenticatedClientSes @Override public void setAction(String action) { - entity.setAction(action); - update(); + UserSessionClientSessionUpdateTask task = new UserSessionClientSessionUpdateTask(client.getId()) { + + @Override + protected void runClientSessionUpdate(AuthenticatedClientSessionEntity entity) { + entity.setAction(action); + } + + }; + + update(task); } @Override @@ -134,8 +181,16 @@ public class AuthenticatedClientSessionAdapter implements AuthenticatedClientSes @Override public void setProtocol(String method) { - entity.setAuthMethod(method); - update(); + UserSessionClientSessionUpdateTask task = new UserSessionClientSessionUpdateTask(client.getId()) { + + @Override + protected void runClientSessionUpdate(AuthenticatedClientSessionEntity entity) { + entity.setAuthMethod(method); + } + + }; + + update(task); } @Override @@ -145,8 +200,16 @@ public class AuthenticatedClientSessionAdapter implements AuthenticatedClientSes @Override public void setRoles(Set roles) { - entity.setRoles(roles); - update(); + UserSessionClientSessionUpdateTask task = new UserSessionClientSessionUpdateTask(client.getId()) { + + @Override + protected void runClientSessionUpdate(AuthenticatedClientSessionEntity entity) { + entity.setRoles(roles); // TODO not thread-safe. But we will remove setRoles anyway...? + } + + }; + + update(task); } @Override @@ -156,35 +219,54 @@ public class AuthenticatedClientSessionAdapter implements AuthenticatedClientSes @Override public void setProtocolMappers(Set protocolMappers) { - entity.setProtocolMappers(protocolMappers); - update(); + UserSessionClientSessionUpdateTask task = new UserSessionClientSessionUpdateTask(client.getId()) { + + @Override + protected void runClientSessionUpdate(AuthenticatedClientSessionEntity entity) { + entity.setProtocolMappers(protocolMappers); // TODO not thread-safe. But we will remove setProtocolMappers anyway...? + } + + }; + + update(task); } @Override public String getNote(String name) { - return entity.getNotes()==null ? null : entity.getNotes().get(name); + return entity.getNotes().get(name); } @Override public void setNote(String name, String value) { - if (entity.getNotes() == null) { - entity.setNotes(new HashMap<>()); - } - entity.getNotes().put(name, value); - update(); + UserSessionClientSessionUpdateTask task = new UserSessionClientSessionUpdateTask(client.getId()) { + + @Override + protected void runClientSessionUpdate(AuthenticatedClientSessionEntity entity) { + entity.getNotes().put(name, value); + } + + }; + + update(task); } @Override public void removeNote(String name) { - if (entity.getNotes() != null) { - entity.getNotes().remove(name); - update(); - } + UserSessionClientSessionUpdateTask task = new UserSessionClientSessionUpdateTask(client.getId()) { + + @Override + protected void runClientSessionUpdate(AuthenticatedClientSessionEntity entity) { + entity.getNotes().remove(name); + } + + }; + + update(task); } @Override public Map getNotes() { - if (entity.getNotes() == null || entity.getNotes().isEmpty()) return Collections.emptyMap(); + if (entity.getNotes().isEmpty()) return Collections.emptyMap(); Map copy = new HashMap<>(); copy.putAll(entity.getNotes()); return copy; diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/mapreduce/LargestResultReducer.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/CacheDecorators.java similarity index 54% rename from model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/mapreduce/LargestResultReducer.java rename to model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/CacheDecorators.java index dbaf924f05..e9b3288103 100644 --- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/mapreduce/LargestResultReducer.java +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/CacheDecorators.java @@ -15,27 +15,24 @@ * limitations under the License. */ -package org.keycloak.models.sessions.infinispan.mapreduce; +package org.keycloak.models.sessions.infinispan; -import org.infinispan.distexec.mapreduce.Reducer; - -import java.util.Iterator; +import org.infinispan.AdvancedCache; +import org.infinispan.Cache; +import org.infinispan.context.Flag; /** - * @author Stian Thorgersen + * @author Marek Posolda */ -public class LargestResultReducer implements Reducer { +public class CacheDecorators { - @Override - public Integer reduce(String reducedKey, Iterator itr) { - Integer largest = itr.next(); - while (itr.hasNext()) { - Integer next = itr.next(); - if (next > largest) { - largest = next; - } - } - return largest; + public static AdvancedCache localCache(Cache cache) { + return cache.getAdvancedCache().withFlags(Flag.CACHE_MODE_LOCAL); } + public static AdvancedCache skipCacheLoaders(Cache cache) { + return cache.getAdvancedCache().withFlags(Flag.SKIP_CACHE_LOAD, Flag.SKIP_CACHE_STORE); + } + + } diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanActionTokenStoreProvider.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanActionTokenStoreProvider.java index b4689aa743..192c9647c7 100644 --- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanActionTokenStoreProvider.java +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanActionTokenStoreProvider.java @@ -61,10 +61,6 @@ public class InfinispanActionTokenStoreProvider implements ActionTokenStoreProvi this.tx.put(actionKeyCache, tokenKey, tokenValue, key.getExpiration() - Time.currentTime(), TimeUnit.SECONDS); } - private static String generateActionTokenEventId() { - return InfinispanActionTokenStoreProviderFactory.ACTION_TOKEN_EVENTS + "/" + UUID.randomUUID(); - } - @Override public ActionTokenValueModel get(ActionTokenKeyModel actionTokenKey) { if (actionTokenKey == null || actionTokenKey.getUserId() == null || actionTokenKey.getActionId() == null) { @@ -98,6 +94,6 @@ public class InfinispanActionTokenStoreProvider implements ActionTokenStoreProvi } ClusterProvider cluster = session.getProvider(ClusterProvider.class); - this.tx.notify(cluster, generateActionTokenEventId(), new RemoveActionTokensSpecificEvent(userId, actionId), false); + this.tx.notify(cluster, InfinispanActionTokenStoreProviderFactory.ACTION_TOKEN_EVENTS, new RemoveActionTokensSpecificEvent(userId, actionId), false); } } diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanActionTokenStoreProviderFactory.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanActionTokenStoreProviderFactory.java index 95ee903507..e4f3bd0c08 100644 --- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanActionTokenStoreProviderFactory.java +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanActionTokenStoreProviderFactory.java @@ -70,24 +70,24 @@ public class InfinispanActionTokenStoreProviderFactory implements ActionTokenSto ClusterProvider cluster = session.getProvider(ClusterProvider.class); - cluster.registerListener(ClusterProvider.ALL, event -> { - if (event instanceof RemoveActionTokensSpecificEvent) { - RemoveActionTokensSpecificEvent e = (RemoveActionTokensSpecificEvent) event; + cluster.registerListener(ACTION_TOKEN_EVENTS, event -> { - LOG.debugf("[%s] Removing token invalidation for user+action: userId=%s, actionId=%s", cacheAddress, e.getUserId(), e.getActionId()); + RemoveActionTokensSpecificEvent e = (RemoveActionTokensSpecificEvent) event; - AdvancedCache localCache = cache - .getAdvancedCache() - .withFlags(Flag.CACHE_MODE_LOCAL, Flag.SKIP_CACHE_LOAD); + LOG.debugf("[%s] Removing token invalidation for user+action: userId=%s, actionId=%s", cacheAddress, e.getUserId(), e.getActionId()); - List toRemove = localCache - .keySet() - .stream() - .filter(k -> Objects.equals(k.getUserId(), e.getUserId()) && Objects.equals(k.getActionId(), e.getActionId())) - .collect(Collectors.toList()); + AdvancedCache localCache = cache + .getAdvancedCache() + .withFlags(Flag.CACHE_MODE_LOCAL, Flag.SKIP_CACHE_LOAD); + + List toRemove = localCache + .keySet() + .stream() + .filter(k -> Objects.equals(k.getUserId(), e.getUserId()) && Objects.equals(k.getActionId(), e.getActionId())) + .collect(Collectors.toList()); + + toRemove.forEach(localCache::remove); - toRemove.forEach(localCache::remove); - } }); LOG.debugf("[%s] Registered cluster listeners", cacheAddress); diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanAuthenticationSessionProvider.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanAuthenticationSessionProvider.java index 5991f98944..15a37ccc3d 100644 --- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanAuthenticationSessionProvider.java +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanAuthenticationSessionProvider.java @@ -30,6 +30,9 @@ import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.cache.infinispan.events.AuthenticationSessionAuthNoteUpdateEvent; import org.keycloak.models.sessions.infinispan.entities.AuthenticationSessionEntity; +import org.keycloak.models.sessions.infinispan.events.ClientRemovedSessionEvent; +import org.keycloak.models.sessions.infinispan.events.RealmRemovedSessionEvent; +import org.keycloak.models.sessions.infinispan.events.SessionEventsSenderTransaction; import org.keycloak.models.sessions.infinispan.stream.AuthenticationSessionPredicate; import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.models.utils.RealmInfoUtil; @@ -46,13 +49,17 @@ public class InfinispanAuthenticationSessionProvider implements AuthenticationSe private final KeycloakSession session; private final Cache cache; protected final InfinispanKeycloakTransaction tx; + protected final SessionEventsSenderTransaction clusterEventsSenderTx; public InfinispanAuthenticationSessionProvider(KeycloakSession session, Cache cache) { this.session = session; this.cache = cache; this.tx = new InfinispanKeycloakTransaction(); + this.clusterEventsSenderTx = new SessionEventsSenderTransaction(session); + session.getTransactionManager().enlistAfterCompletion(tx); + session.getTransactionManager().enlistAfterCompletion(clusterEventsSenderTx); } @Override @@ -109,37 +116,67 @@ public class InfinispanAuthenticationSessionProvider implements AuthenticationSe // Each cluster node cleanups just local sessions, which are those owned by himself (+ few more taking l1 cache into account) - Iterator> itr = cache.getAdvancedCache().withFlags(Flag.CACHE_MODE_LOCAL) - .entrySet().stream().filter(AuthenticationSessionPredicate.create(realm.getId()).expired(expired)).iterator(); + Iterator> itr = CacheDecorators.localCache(cache) + .entrySet() + .stream() + .filter(AuthenticationSessionPredicate.create(realm.getId()).expired(expired)) + .iterator(); int counter = 0; while (itr.hasNext()) { counter++; AuthenticationSessionEntity entity = itr.next().getValue(); - tx.remove(cache, entity.getId()); + tx.remove(CacheDecorators.localCache(cache), entity.getId()); } - log.debugf("Removed %d expired user sessions for realm '%s'", counter, realm.getName()); + log.debugf("Removed %d expired authentication sessions for realm '%s'", counter, realm.getName()); } - // TODO: Should likely listen to "RealmRemovedEvent" received from cluster and clean just local sessions + @Override public void onRealmRemoved(RealmModel realm) { - Iterator> itr = cache.entrySet().stream().filter(AuthenticationSessionPredicate.create(realm.getId())).iterator(); + // Send message to all DCs. The remoteCache will notify client listeners on all DCs for remove authentication sessions + clusterEventsSenderTx.addEvent( + RealmRemovedSessionEvent.createEvent(RealmRemovedSessionEvent.class, InfinispanAuthenticationSessionProviderFactory.REALM_REMOVED_AUTHSESSION_EVENT, session, realm.getId(), false), + ClusterProvider.DCNotify.ALL_DCS); + } + + protected void onRealmRemovedEvent(String realmId) { + Iterator> itr = CacheDecorators.localCache(cache) + .entrySet() + .stream() + .filter(AuthenticationSessionPredicate.create(realmId)) + .iterator(); + while (itr.hasNext()) { - cache.remove(itr.next().getKey()); + CacheDecorators.localCache(cache) + .remove(itr.next().getKey()); } } - // TODO: Should likely listen to "ClientRemovedEvent" received from cluster and clean just local sessions + @Override public void onClientRemoved(RealmModel realm, ClientModel client) { - Iterator> itr = cache.entrySet().stream().filter(AuthenticationSessionPredicate.create(realm.getId()).client(client.getId())).iterator(); + // Send message to all DCs. The remoteCache will notify client listeners on all DCs for remove authentication sessions of this client + clusterEventsSenderTx.addEvent( + ClientRemovedSessionEvent.create(session, InfinispanAuthenticationSessionProviderFactory.CLIENT_REMOVED_AUTHSESSION_EVENT, realm.getId(), false, client.getId()), + ClusterProvider.DCNotify.ALL_DCS); + } + + protected void onClientRemovedEvent(String realmId, String clientUuid) { + Iterator> itr = CacheDecorators.localCache(cache) + .entrySet() + .stream() + .filter(AuthenticationSessionPredicate.create(realmId).client(clientUuid)) + .iterator(); + while (itr.hasNext()) { - cache.remove(itr.next().getKey()); + CacheDecorators.localCache(cache) + .remove(itr.next().getKey()); } } + @Override public void updateNonlocalSessionAuthNotes(String authSessionId, Map authNotesFragment) { if (authSessionId == null) { @@ -150,7 +187,8 @@ public class InfinispanAuthenticationSessionProvider implements AuthenticationSe cluster.notify( InfinispanAuthenticationSessionProviderFactory.AUTHENTICATION_SESSION_EVENTS, AuthenticationSessionAuthNoteUpdateEvent.create(authSessionId, authNotesFragment), - true + true, + ClusterProvider.DCNotify.ALL_BUT_LOCAL_DC ); } @@ -159,4 +197,7 @@ public class InfinispanAuthenticationSessionProvider implements AuthenticationSe } + public Cache getCache() { + return cache; + } } diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanAuthenticationSessionProviderFactory.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanAuthenticationSessionProviderFactory.java index a9589ccca5..04e1dc8c18 100644 --- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanAuthenticationSessionProviderFactory.java +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanAuthenticationSessionProviderFactory.java @@ -26,6 +26,12 @@ import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.cache.infinispan.events.AuthenticationSessionAuthNoteUpdateEvent; import org.keycloak.models.sessions.infinispan.entities.AuthenticationSessionEntity; +import org.keycloak.models.sessions.infinispan.events.AbstractAuthSessionClusterListener; +import org.keycloak.models.sessions.infinispan.events.ClientRemovedSessionEvent; +import org.keycloak.models.sessions.infinispan.events.RealmRemovedSessionEvent; +import org.keycloak.models.utils.PostMigrationEvent; +import org.keycloak.provider.ProviderEvent; +import org.keycloak.provider.ProviderEventListener; import org.keycloak.sessions.AuthenticationSessionProvider; import org.keycloak.sessions.AuthenticationSessionProviderFactory; import java.util.Map; @@ -42,13 +48,59 @@ public class InfinispanAuthenticationSessionProviderFactory implements Authentic private volatile Cache authSessionsCache; + public static final String PROVIDER_ID = "infinispan"; + public static final String AUTHENTICATION_SESSION_EVENTS = "AUTHENTICATION_SESSION_EVENTS"; + public static final String REALM_REMOVED_AUTHSESSION_EVENT = "REALM_REMOVED_EVENT_AUTHSESSIONS"; + + public static final String CLIENT_REMOVED_AUTHSESSION_EVENT = "CLIENT_REMOVED_SESSION_AUTHSESSIONS"; + @Override public void init(Config.Scope config) { } + + @Override + public void postInit(KeycloakSessionFactory factory) { + factory.register(new ProviderEventListener() { + + @Override + public void onEvent(ProviderEvent event) { + if (event instanceof PostMigrationEvent) { + registerClusterListeners(((PostMigrationEvent) event).getSession()); + } + } + }); + } + + + protected void registerClusterListeners(KeycloakSession session) { + KeycloakSessionFactory sessionFactory = session.getKeycloakSessionFactory(); + ClusterProvider cluster = session.getProvider(ClusterProvider.class); + + cluster.registerListener(REALM_REMOVED_AUTHSESSION_EVENT, new AbstractAuthSessionClusterListener(sessionFactory) { + + @Override + protected void eventReceived(KeycloakSession session, InfinispanAuthenticationSessionProvider provider, RealmRemovedSessionEvent sessionEvent) { + provider.onRealmRemovedEvent(sessionEvent.getRealmId()); + } + + }); + + cluster.registerListener(CLIENT_REMOVED_AUTHSESSION_EVENT, new AbstractAuthSessionClusterListener(sessionFactory) { + + @Override + protected void eventReceived(KeycloakSession session, InfinispanAuthenticationSessionProvider provider, ClientRemovedSessionEvent sessionEvent) { + provider.onClientRemovedEvent(sessionEvent.getRealmId(), sessionEvent.getClientUuid()); + } + }); + + log.debug("Registered cluster listeners"); + } + + @Override public AuthenticationSessionProvider create(KeycloakSession session) { lazyInit(session); @@ -98,16 +150,12 @@ public class InfinispanAuthenticationSessionProviderFactory implements Authentic } } - @Override - public void postInit(KeycloakSessionFactory factory) { - } - @Override public void close() { } @Override public String getId() { - return "infinispan"; + return PROVIDER_ID; } } diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanKeycloakTransaction.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanKeycloakTransaction.java index 5471184da9..959223c7a2 100644 --- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanKeycloakTransaction.java +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanKeycloakTransaction.java @@ -155,7 +155,7 @@ public class InfinispanKeycloakTransaction implements KeycloakTransaction { theTaskKey = taskKey + "-" + (i++); } - tasks.put(taskKey, () -> clusterProvider.notify(taskKey, event, ignoreSender)); + tasks.put(taskKey, () -> clusterProvider.notify(taskKey, event, ignoreSender, ClusterProvider.DCNotify.ALL_DCS)); } public void remove(Cache cache, K key) { @@ -168,7 +168,7 @@ public class InfinispanKeycloakTransaction implements KeycloakTransaction { // This is for possibility to lookup for session by id, which was created in this transaction public V get(Cache cache, K key) { Object taskKey = getTaskKey(cache, key); - CacheTask current = tasks.get(taskKey); + CacheTask current = tasks.get(taskKey); if (current != null) { if (current instanceof CacheTaskWithValue) { return ((CacheTaskWithValue) current).getValue(); @@ -190,11 +190,11 @@ public class InfinispanKeycloakTransaction implements KeycloakTransaction { } } - public interface CacheTask { + public interface CacheTask { void execute(); } - public abstract class CacheTaskWithValue implements CacheTask { + public abstract class CacheTaskWithValue implements CacheTask { protected V value; public CacheTaskWithValue(V value) { diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanStickySessionEncoderProviderFactory.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanStickySessionEncoderProviderFactory.java index b8e6a7131d..2477b69a53 100644 --- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanStickySessionEncoderProviderFactory.java +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanStickySessionEncoderProviderFactory.java @@ -21,6 +21,7 @@ import org.keycloak.Config; import org.keycloak.connections.infinispan.InfinispanConnectionProvider; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.models.sessions.infinispan.util.InfinispanUtil; import org.keycloak.sessions.StickySessionEncoderProvider; import org.keycloak.sessions.StickySessionEncoderProviderFactory; @@ -29,16 +30,22 @@ import org.keycloak.sessions.StickySessionEncoderProviderFactory; */ public class InfinispanStickySessionEncoderProviderFactory implements StickySessionEncoderProviderFactory { - private String myNodeName; @Override public StickySessionEncoderProvider create(KeycloakSession session) { + String myNodeName = InfinispanUtil.getMyAddress(session); + + if (myNodeName != null && myNodeName.startsWith(InfinispanConnectionProvider.NODE_PREFIX)) { + + // Node name was randomly generated. We won't use anything for sticky sessions in this case + myNodeName = null; + } + return new InfinispanStickySessionEncoderProvider(session, myNodeName); } @Override public void init(Config.Scope config) { - myNodeName = config.get("nodeName", System.getProperty(InfinispanConnectionProvider.JBOSS_NODE_NAME)); } @Override diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java index 202a051f14..c1a8ed688b 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java @@ -18,10 +18,12 @@ package org.keycloak.models.sessions.infinispan; import org.infinispan.Cache; -import org.infinispan.CacheStream; +import org.infinispan.client.hotrod.RemoteCache; import org.infinispan.context.Flag; import org.jboss.logging.Logger; +import org.keycloak.cluster.ClusterProvider; import org.keycloak.common.util.Time; +import org.keycloak.connections.infinispan.InfinispanConnectionProvider; import org.keycloak.models.AuthenticatedClientSessionModel; import org.keycloak.models.ClientModel; import org.keycloak.models.KeycloakSession; @@ -31,19 +33,27 @@ import org.keycloak.models.UserModel; import org.keycloak.models.UserSessionModel; import org.keycloak.models.UserSessionProvider; import org.keycloak.models.session.UserSessionPersisterProvider; +import org.keycloak.models.sessions.infinispan.changes.sessions.LastSessionRefreshStore; +import org.keycloak.models.sessions.infinispan.remotestore.RemoteCacheInvoker; +import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper; +import org.keycloak.models.sessions.infinispan.changes.InfinispanChangelogBasedTransaction; +import org.keycloak.models.sessions.infinispan.changes.SessionUpdateTask; import org.keycloak.models.sessions.infinispan.entities.AuthenticatedClientSessionEntity; import org.keycloak.models.sessions.infinispan.entities.LoginFailureEntity; import org.keycloak.models.sessions.infinispan.entities.LoginFailureKey; -import org.keycloak.models.sessions.infinispan.entities.SessionEntity; import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity; +import org.keycloak.models.sessions.infinispan.events.ClientRemovedSessionEvent; +import org.keycloak.models.sessions.infinispan.events.RealmRemovedSessionEvent; +import org.keycloak.models.sessions.infinispan.events.RemoveAllUserLoginFailuresEvent; +import org.keycloak.models.sessions.infinispan.events.RemoveUserSessionsEvent; +import org.keycloak.models.sessions.infinispan.events.SessionEventsSenderTransaction; import org.keycloak.models.sessions.infinispan.stream.Comparators; import org.keycloak.models.sessions.infinispan.stream.Mappers; import org.keycloak.models.sessions.infinispan.stream.SessionPredicate; import org.keycloak.models.sessions.infinispan.stream.UserLoginFailurePredicate; import org.keycloak.models.sessions.infinispan.stream.UserSessionPredicate; +import org.keycloak.models.sessions.infinispan.util.InfinispanUtil; -import java.util.Collection; -import java.util.Collections; import java.util.Iterator; import java.util.LinkedList; import java.util.List; @@ -51,7 +61,6 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Consumer; import java.util.function.Predicate; -import java.util.stream.Collectors; import java.util.stream.Stream; /** @@ -62,31 +71,71 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { private static final Logger log = Logger.getLogger(InfinispanUserSessionProvider.class); protected final KeycloakSession session; - protected final Cache sessionCache; - protected final Cache offlineSessionCache; + + protected final Cache> sessionCache; + protected final Cache> offlineSessionCache; protected final Cache loginFailureCache; + + protected final InfinispanChangelogBasedTransaction sessionTx; + protected final InfinispanChangelogBasedTransaction offlineSessionTx; protected final InfinispanKeycloakTransaction tx; - public InfinispanUserSessionProvider(KeycloakSession session, Cache sessionCache, Cache offlineSessionCache, + protected final SessionEventsSenderTransaction clusterEventsSenderTx; + + protected final LastSessionRefreshStore lastSessionRefreshStore; + protected final LastSessionRefreshStore offlineLastSessionRefreshStore; + + public InfinispanUserSessionProvider(KeycloakSession session, + RemoteCacheInvoker remoteCacheInvoker, + LastSessionRefreshStore lastSessionRefreshStore, + LastSessionRefreshStore offlineLastSessionRefreshStore, + Cache> sessionCache, + Cache> offlineSessionCache, Cache loginFailureCache) { this.session = session; + this.sessionCache = sessionCache; this.offlineSessionCache = offlineSessionCache; this.loginFailureCache = loginFailureCache; + + this.sessionTx = new InfinispanChangelogBasedTransaction<>(session, InfinispanConnectionProvider.SESSION_CACHE_NAME, sessionCache, remoteCacheInvoker); + this.offlineSessionTx = new InfinispanChangelogBasedTransaction<>(session, InfinispanConnectionProvider.OFFLINE_SESSION_CACHE_NAME, offlineSessionCache, remoteCacheInvoker); + this.tx = new InfinispanKeycloakTransaction(); + this.clusterEventsSenderTx = new SessionEventsSenderTransaction(session); + + this.lastSessionRefreshStore = lastSessionRefreshStore; + this.offlineLastSessionRefreshStore = offlineLastSessionRefreshStore; + session.getTransactionManager().enlistAfterCompletion(tx); + session.getTransactionManager().enlistAfterCompletion(clusterEventsSenderTx); + session.getTransactionManager().enlistAfterCompletion(sessionTx); + session.getTransactionManager().enlistAfterCompletion(offlineSessionTx); } - protected Cache getCache(boolean offline) { + protected Cache> getCache(boolean offline) { return offline ? offlineSessionCache : sessionCache; } + protected InfinispanChangelogBasedTransaction getTransaction(boolean offline) { + return offline ? offlineSessionTx : sessionTx; + } + + protected LastSessionRefreshStore getLastSessionRefreshStore() { + return lastSessionRefreshStore; + } + + protected LastSessionRefreshStore getOfflineLastSessionRefreshStore() { + return offlineLastSessionRefreshStore; + } + @Override public AuthenticatedClientSessionModel createClientSession(RealmModel realm, ClientModel client, UserSessionModel userSession) { AuthenticatedClientSessionEntity entity = new AuthenticatedClientSessionEntity(); - AuthenticatedClientSessionAdapter adapter = new AuthenticatedClientSessionAdapter(entity, client, (UserSessionAdapter) userSession, this, sessionCache); + InfinispanChangelogBasedTransaction updateTx = getTransaction(false); + AuthenticatedClientSessionAdapter adapter = new AuthenticatedClientSessionAdapter(entity, client, (UserSessionAdapter) userSession, this, updateTx); adapter.setUserSession(userSession); return adapter; } @@ -95,10 +144,28 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { public UserSessionModel createUserSession(String id, RealmModel realm, UserModel user, String loginUsername, String ipAddress, String authMethod, boolean rememberMe, String brokerSessionId, String brokerUserId) { UserSessionEntity entity = new UserSessionEntity(); entity.setId(id); - updateSessionEntity(entity, realm, user, loginUsername, ipAddress, authMethod, rememberMe, brokerSessionId, brokerUserId); - tx.putIfAbsent(sessionCache, id, entity); + SessionUpdateTask createSessionTask = new SessionUpdateTask() { + + @Override + public void runUpdate(UserSessionEntity session) { + + } + + @Override + public CacheOperation getOperation(UserSessionEntity session) { + return CacheOperation.ADD_IF_ABSENT; + } + + @Override + public CrossDCMessageStatus getCrossDCMessageStatus(SessionEntityWrapper sessionWrapper) { + return CrossDCMessageStatus.SYNC; + } + + }; + + sessionTx.addTask(id, createSessionTask, entity); return wrap(realm, entity, false); } @@ -121,31 +188,43 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { } + @Override public UserSessionModel getUserSession(RealmModel realm, String id) { return getUserSession(realm, id, false); } protected UserSessionAdapter getUserSession(RealmModel realm, String id, boolean offline) { - Cache cache = getCache(offline); - UserSessionEntity entity = (UserSessionEntity) tx.get(cache, id); // Chance created in this transaction - - if (entity == null) { - entity = (UserSessionEntity) cache.get(id); - } - + UserSessionEntity entity = getUserSessionEntity(id, offline); return wrap(realm, entity, offline); } - protected List getUserSessions(RealmModel realm, Predicate> predicate, boolean offline) { - CacheStream> cacheStream = getCache(offline).entrySet().stream(); - Iterator> itr = cacheStream.filter(predicate).iterator(); - List sessions = new LinkedList<>(); + private UserSessionEntity getUserSessionEntity(String id, boolean offline) { + InfinispanChangelogBasedTransaction tx = getTransaction(offline); + SessionEntityWrapper entityWrapper = tx.get(id); + return entityWrapper==null ? null : entityWrapper.getEntity(); + } + + + protected List getUserSessions(RealmModel realm, Predicate>> predicate, boolean offline) { + Cache> cache = getCache(offline); + + cache = CacheDecorators.skipCacheLoaders(cache); + + Stream>> cacheStream = cache.entrySet().stream(); + + List resultSessions = new LinkedList<>(); + + Iterator itr = cacheStream.filter(predicate) + .map(Mappers.userSessionEntity()) + .iterator(); + while (itr.hasNext()) { - UserSessionEntity e = (UserSessionEntity) itr.next().getValue(); - sessions.add(wrap(realm, e, offline)); + UserSessionEntity userSessionEntity = itr.next(); + resultSessions.add(wrap(realm, userSessionEntity, offline)); } - return sessions; + + return resultSessions; } @Override @@ -175,65 +254,107 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { } protected List getUserSessions(final RealmModel realm, ClientModel client, int firstResult, int maxResults, final boolean offline) { - final Cache cache = getCache(offline); + Cache> cache = getCache(offline); + + cache = CacheDecorators.skipCacheLoaders(cache); Stream stream = cache.entrySet().stream() .filter(UserSessionPredicate.create(realm.getId()).client(client.getId())) .map(Mappers.userSessionEntity()) .sorted(Comparators.userSessionLastSessionRefresh()); - // Doesn't work due to ISPN-6575 . TODO Fix once infinispan upgraded to 8.2.2.Final or 9.0 -// if (firstResult > 0) { -// stream = stream.skip(firstResult); -// } -// -// if (maxResults > 0) { -// stream = stream.limit(maxResults); -// } -// -// List entities = stream.collect(Collectors.toList()); - - - // Workaround for ISPN-6575 TODO Fix once infinispan upgraded to 8.2.2.Final or 9.0 and replace with the more effective code above - if (firstResult < 0) { - firstResult = 0; - } - if (maxResults < 0) { - maxResults = Integer.MAX_VALUE; + if (firstResult > 0) { + stream = stream.skip(firstResult); } - int count = firstResult + maxResults; - if (count > 0) { - stream = stream.limit(count); + if (maxResults > 0) { + stream = stream.limit(maxResults); } - List entities = stream.collect(Collectors.toList()); - - if (firstResult > entities.size()) { - return Collections.emptyList(); - } - - maxResults = Math.min(maxResults, entities.size() - firstResult); - entities = entities.subList(firstResult, firstResult + maxResults); - final List sessions = new LinkedList<>(); - entities.stream().forEach(new Consumer() { - @Override - public void accept(UserSessionEntity userSessionEntity) { - sessions.add(wrap(realm, userSessionEntity, offline)); - } - }); + Iterator itr = stream.iterator(); + + while (itr.hasNext()) { + UserSessionEntity userSessionEntity = itr.next(); + sessions.add(wrap(realm, userSessionEntity, offline)); + } + return sessions; } + + @Override + public UserSessionModel getUserSessionWithPredicate(RealmModel realm, String id, boolean offline, Predicate predicate) { + UserSessionModel userSession = getUserSession(realm, id, offline); + if (userSession == null) { + return null; + } + + // We have userSession, which passes predicate. No need for remote lookup. + if (predicate.test(userSession)) { + log.debugf("getUserSessionWithPredicate(%s): found in local cache", id); + return userSession; + } + + // Try lookup userSession from remoteCache + Cache> cache = getCache(offline); + RemoteCache remoteCache = InfinispanUtil.getRemoteCache(cache); + + if (remoteCache != null) { + UserSessionEntity remoteSessionEntity = (UserSessionEntity) remoteCache.get(id); + if (remoteSessionEntity != null) { + log.debugf("getUserSessionWithPredicate(%s): remote cache contains session entity %s", id, remoteSessionEntity); + + UserSessionModel remoteSessionAdapter = wrap(realm, remoteSessionEntity, offline); + if (predicate.test(remoteSessionAdapter)) { + + InfinispanChangelogBasedTransaction tx = getTransaction(offline); + + // Remote entity contains our predicate. Update local cache with the remote entity + SessionEntityWrapper sessionWrapper = remoteSessionEntity.mergeRemoteEntityWithLocalEntity(tx.get(id)); + + // Replace entity just in ispn cache. Skip remoteStore + cache.getAdvancedCache().withFlags(Flag.SKIP_CACHE_STORE, Flag.SKIP_CACHE_LOAD, Flag.IGNORE_RETURN_VALUES) + .replace(id, sessionWrapper); + + tx.reloadEntityInCurrentTransaction(realm, id, sessionWrapper); + + // Recursion. We should have it locally now + return getUserSessionWithPredicate(realm, id, offline, predicate); + } else { + log.debugf("getUserSessionWithPredicate(%s): found, but predicate doesn't pass", id); + + return null; + } + } else { + log.debugf("getUserSessionWithPredicate(%s): not found", id); + + // Session not available on remoteCache. Was already removed there. So removing locally too. + // TODO: Can be optimized to skip calling remoteCache.remove + removeUserSession(realm, userSession); + + return null; + } + } else { + + log.debugf("getUserSessionWithPredicate(%s): remote cache not available", id); + + return null; + } + } + + @Override public long getActiveUserSessions(RealmModel realm, ClientModel client) { return getUserSessionsCount(realm, client, false); } protected long getUserSessionsCount(RealmModel realm, ClientModel client, boolean offline) { - return getCache(offline).entrySet().stream() + Cache> cache = getCache(offline); + cache = CacheDecorators.skipCacheLoaders(cache); + + return cache.entrySet().stream() .filter(UserSessionPredicate.create(realm.getId()).client(client.getId())) .count(); } @@ -242,7 +363,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { public void removeUserSession(RealmModel realm, UserSessionModel session) { UserSessionEntity entity = getUserSessionEntity(session, false); if (entity != null) { - removeUserSession(realm, entity, false); + removeUserSession(entity, false); } } @@ -252,12 +373,15 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { } protected void removeUserSessions(RealmModel realm, UserModel user, boolean offline) { - Cache cache = getCache(offline); + Cache> cache = getCache(offline); + + cache = CacheDecorators.skipCacheLoaders(cache); + + Iterator itr = cache.entrySet().stream().filter(UserSessionPredicate.create(realm.getId()).user(user.getId())).map(Mappers.userSessionEntity()).iterator(); - Iterator itr = cache.entrySet().stream().filter(UserSessionPredicate.create(realm.getId()).user(user.getId())).map(Mappers.sessionEntity()).iterator(); while (itr.hasNext()) { - UserSessionEntity userSessionEntity = (UserSessionEntity) itr.next(); - removeUserSession(realm, userSessionEntity, offline); + UserSessionEntity userSessionEntity = itr.next(); + removeUserSession(userSessionEntity, offline); } } @@ -273,17 +397,30 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { int expiredRefresh = Time.currentTime() - realm.getSsoSessionIdleTimeout(); // Each cluster node cleanups just local sessions, which are those owned by himself (+ few more taking l1 cache into account) - Iterator> itr = sessionCache.getAdvancedCache().withFlags(Flag.CACHE_MODE_LOCAL) - .entrySet().stream().filter(UserSessionPredicate.create(realm.getId()).expired(expired, expiredRefresh)).iterator(); + Cache> localCache = CacheDecorators.localCache(sessionCache); - int counter = 0; - while (itr.hasNext()) { - counter++; - UserSessionEntity entity = (UserSessionEntity) itr.next().getValue(); - tx.remove(sessionCache, entity.getId()); - } + int[] counter = { 0 }; - log.debugf("Removed %d expired user sessions for realm '%s'", counter, realm.getName()); + Cache> localCacheStoreIgnore = CacheDecorators.skipCacheLoaders(localCache); + + // Ignore remoteStore for stream iteration. But we will invoke remoteStore for userSession removal propagate + localCacheStoreIgnore + .entrySet() + .stream() + .filter(UserSessionPredicate.create(realm.getId()).expired(expired, expiredRefresh)) + .map(Mappers.sessionId()) + .forEach(new Consumer() { + + @Override + public void accept(String sessionId) { + counter[0]++; + tx.remove(localCache, sessionId); + } + + }); + + + log.debugf("Removed %d expired user sessions for realm '%s'", counter[0], realm.getName()); } private void removeExpiredOfflineUserSessions(RealmModel realm) { @@ -291,38 +428,72 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { int expiredOffline = Time.currentTime() - realm.getOfflineSessionIdleTimeout(); // Each cluster node cleanups just local sessions, which are those owned by himself (+ few more taking l1 cache into account) + Cache> localCache = CacheDecorators.localCache(offlineSessionCache); + UserSessionPredicate predicate = UserSessionPredicate.create(realm.getId()).expired(null, expiredOffline); - Iterator> itr = offlineSessionCache.getAdvancedCache().withFlags(Flag.CACHE_MODE_LOCAL) - .entrySet().stream().filter(predicate).iterator(); - int counter = 0; - while (itr.hasNext()) { - counter++; - UserSessionEntity entity = (UserSessionEntity) itr.next().getValue(); - tx.remove(offlineSessionCache, entity.getId()); + final int[] counter = { 0 }; - persister.removeUserSession(entity.getId(), true); + Cache> localCacheStoreIgnore = CacheDecorators.skipCacheLoaders(localCache); - for (String clientUUID : entity.getAuthenticatedClientSessions().keySet()) { - persister.removeClientSession(entity.getId(), clientUUID, true); - } - } + // Ignore remoteStore for stream iteration. But we will invoke remoteStore for userSession removal propagate + localCacheStoreIgnore + .entrySet() + .stream() + .filter(predicate) + .map(Mappers.userSessionEntity()) + .forEach(new Consumer() { + + @Override + public void accept(UserSessionEntity userSessionEntity) { + counter[0]++; + tx.remove(localCache, userSessionEntity.getId()); + + // TODO:mposolda can be likely optimized to delete all expired at one step + persister.removeUserSession( userSessionEntity.getId(), true); + + // TODO can be likely optimized to delete all at one step + for (String clientUUID : userSessionEntity.getAuthenticatedClientSessions().keySet()) { + persister.removeClientSession(userSessionEntity.getId(), clientUUID, true); + } + } + }); log.debugf("Removed %d expired offline user sessions for realm '%s'", counter, realm.getName()); } @Override public void removeUserSessions(RealmModel realm) { - removeUserSessions(realm, false); + // Don't send message to all DCs, just to all cluster nodes in current DC. The remoteCache will notify client listeners for removed userSessions. + clusterEventsSenderTx.addEvent( + RemoveUserSessionsEvent.createEvent(RemoveUserSessionsEvent.class, InfinispanUserSessionProviderFactory.REMOVE_USER_SESSIONS_EVENT, session, realm.getId(), true), + ClusterProvider.DCNotify.LOCAL_DC_ONLY); } - protected void removeUserSessions(RealmModel realm, boolean offline) { - Cache cache = getCache(offline); + protected void onRemoveUserSessionsEvent(String realmId) { + removeLocalUserSessions(realmId, false); + } - Iterator itr = cache.entrySet().stream().filter(SessionPredicate.create(realm.getId())).map(Mappers.sessionId()).iterator(); - while (itr.hasNext()) { - cache.remove(itr.next()); - } + private void removeLocalUserSessions(String realmId, boolean offline) { + Cache> cache = getCache(offline); + Cache> localCache = CacheDecorators.localCache(cache); + + Cache> localCacheStoreIgnore = CacheDecorators.skipCacheLoaders(localCache); + + localCacheStoreIgnore + .entrySet() + .stream() + .filter(SessionPredicate.create(realmId)) + .map(Mappers.sessionId()) + .forEach(new Consumer() { + + @Override + public void accept(String sessionId) { + // Remove session from remoteCache too + localCache.remove(sessionId); + } + + }); } @Override @@ -348,22 +519,53 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { @Override public void removeAllUserLoginFailures(RealmModel realm) { - Iterator itr = loginFailureCache.entrySet().stream().filter(UserLoginFailurePredicate.create(realm.getId())).map(Mappers.loginFailureId()).iterator(); - while (itr.hasNext()) { - LoginFailureKey key = itr.next(); - tx.remove(loginFailureCache, key); - } + clusterEventsSenderTx.addEvent( + RemoveAllUserLoginFailuresEvent.createEvent(RemoveAllUserLoginFailuresEvent.class, InfinispanUserSessionProviderFactory.REMOVE_ALL_LOGIN_FAILURES_EVENT, session, realm.getId(), true), + ClusterProvider.DCNotify.LOCAL_DC_ONLY); + } + + protected void onRemoveAllUserLoginFailuresEvent(String realmId) { + removeAllLocalUserLoginFailuresEvent(realmId); + } + + private void removeAllLocalUserLoginFailuresEvent(String realmId) { + Cache localCache = CacheDecorators.localCache(loginFailureCache); + + Cache localCacheStoreIgnore = CacheDecorators.skipCacheLoaders(localCache); + + localCacheStoreIgnore + .entrySet() + .stream() + .filter(UserLoginFailurePredicate.create(realmId)) + .map(Mappers.loginFailureId()) + .forEach(loginFailureKey -> { + // Remove loginFailure from remoteCache too + localCache.remove(loginFailureKey); + }); } @Override public void onRealmRemoved(RealmModel realm) { - removeUserSessions(realm, true); - removeUserSessions(realm, false); - removeAllUserLoginFailures(realm); + // Don't send message to all DCs, just to all cluster nodes in current DC. The remoteCache will notify client listeners for removed userSessions. + clusterEventsSenderTx.addEvent( + RealmRemovedSessionEvent.createEvent(RealmRemovedSessionEvent.class, InfinispanUserSessionProviderFactory.REALM_REMOVED_SESSION_EVENT, session, realm.getId(), true), + ClusterProvider.DCNotify.LOCAL_DC_ONLY); + } + + protected void onRealmRemovedEvent(String realmId) { + removeLocalUserSessions(realmId, true); + removeLocalUserSessions(realmId, false); + removeAllLocalUserLoginFailuresEvent(realmId); } @Override public void onClientRemoved(RealmModel realm, ClientModel client) { +// clusterEventsSenderTx.addEvent( +// ClientRemovedSessionEvent.createEvent(ClientRemovedSessionEvent.class, InfinispanUserSessionProviderFactory.CLIENT_REMOVED_SESSION_EVENT, session, realm.getId(), true), +// ClusterProvider.DCNotify.LOCAL_DC_ONLY); + } + + protected void onClientRemovedEvent(String realmId, String clientUuid) { // Nothing for now. userSession.getAuthenticatedClientSessions() will check lazily if particular client exists and update userSession on-the-fly. } @@ -380,10 +582,29 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { public void close() { } - protected void removeUserSession(RealmModel realm, UserSessionEntity sessionEntity, boolean offline) { - Cache cache = getCache(offline); + protected void removeUserSession(UserSessionEntity sessionEntity, boolean offline) { + InfinispanChangelogBasedTransaction tx = getTransaction(offline); - tx.remove(cache, sessionEntity.getId()); + SessionUpdateTask removeTask = new SessionUpdateTask() { + + @Override + public void runUpdate(UserSessionEntity entity) { + + } + + @Override + public CacheOperation getOperation(UserSessionEntity entity) { + return CacheOperation.REMOVE; + } + + @Override + public CrossDCMessageStatus getCrossDCMessageStatus(SessionEntityWrapper sessionWrapper) { + return CrossDCMessageStatus.SYNC; + } + + }; + + tx.addTask(sessionEntity.getId(), removeTask); } InfinispanKeycloakTransaction getTx() { @@ -391,16 +612,8 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { } UserSessionAdapter wrap(RealmModel realm, UserSessionEntity entity, boolean offline) { - Cache cache = getCache(offline); - return entity != null ? new UserSessionAdapter(session, this, cache, realm, entity, offline) : null; - } - - List wrapUserSessions(RealmModel realm, Collection entities, boolean offline) { - List models = new LinkedList<>(); - for (UserSessionEntity e : entities) { - models.add(wrap(realm, e, offline)); - } - return models; + InfinispanChangelogBasedTransaction tx = getTransaction(offline); + return entity != null ? new UserSessionAdapter(session, this, tx, realm, entity, offline) : null; } UserLoginFailureModel wrap(LoginFailureKey key, LoginFailureEntity entity) { @@ -411,8 +624,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { if (userSession instanceof UserSessionAdapter) { return ((UserSessionAdapter) userSession).getEntity(); } else { - Cache cache = getCache(offline); - return cache != null ? (UserSessionEntity) cache.get(userSession.getId()) : null; + return getUserSessionEntity(userSession.getId(), offline); } } @@ -438,7 +650,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { public void removeOfflineUserSession(RealmModel realm, UserSessionModel userSession) { UserSessionEntity userSessionEntity = getUserSessionEntity(userSession, true); if (userSessionEntity != null) { - removeUserSession(realm, userSessionEntity, true); + removeUserSession(userSessionEntity, true); } } @@ -449,7 +661,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { UserSessionAdapter userSessionAdapter = (offlineUserSession instanceof UserSessionAdapter) ? (UserSessionAdapter) offlineUserSession : getOfflineUserSession(offlineUserSession.getRealm(), offlineUserSession.getId()); - AuthenticatedClientSessionAdapter offlineClientSession = importClientSession(userSessionAdapter, clientSession); + AuthenticatedClientSessionAdapter offlineClientSession = importClientSession(userSessionAdapter, clientSession, getTransaction(true)); // update timestamp to current time offlineClientSession.setTimestamp(Time.currentTime()); @@ -459,12 +671,18 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { @Override public List getOfflineUserSessions(RealmModel realm, UserModel user) { - Iterator> itr = offlineSessionCache.entrySet().stream().filter(UserSessionPredicate.create(realm.getId()).user(user.getId())).iterator(); List userSessions = new LinkedList<>(); - while(itr.hasNext()) { - UserSessionEntity entity = (UserSessionEntity) itr.next().getValue(); - UserSessionModel userSession = wrap(realm, entity, true); + Cache> cache = CacheDecorators.skipCacheLoaders(offlineSessionCache); + + Iterator itr = cache.entrySet().stream() + .filter(UserSessionPredicate.create(realm.getId()).user(user.getId())) + .map(Mappers.userSessionEntity()) + .iterator(); + + while (itr.hasNext()) { + UserSessionEntity userSessionEntity = itr.next(); + UserSessionModel userSession = wrap(realm, userSessionEntity, true); userSessions.add(userSession); } @@ -492,7 +710,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { entity.setBrokerUserId(userSession.getBrokerUserId()); entity.setIpAddress(userSession.getIpAddress()); entity.setLoginUsername(userSession.getLoginUsername()); - entity.setNotes(userSession.getNotes()== null ? new ConcurrentHashMap<>() : userSession.getNotes()); + entity.setNotes(userSession.getNotes() == null ? new ConcurrentHashMap<>() : userSession.getNotes()); entity.setAuthenticatedClientSessions(new ConcurrentHashMap<>()); entity.setRememberMe(userSession.isRememberMe()); entity.setState(userSession.getState()); @@ -502,14 +720,34 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { entity.setLastSessionRefresh(userSession.getLastSessionRefresh()); - Cache cache = getCache(offline); - tx.put(cache, userSession.getId(), entity); + InfinispanChangelogBasedTransaction tx = getTransaction(offline); + + SessionUpdateTask importTask = new SessionUpdateTask() { + + @Override + public void runUpdate(UserSessionEntity session) { + + } + + @Override + public CacheOperation getOperation(UserSessionEntity session) { + return CacheOperation.ADD_IF_ABSENT; + } + + @Override + public CrossDCMessageStatus getCrossDCMessageStatus(SessionEntityWrapper sessionWrapper) { + return CrossDCMessageStatus.SYNC; + } + + }; + tx.addTask(userSession.getId(), importTask, entity); + UserSessionAdapter importedSession = wrap(userSession.getRealm(), entity, offline); // Handle client sessions if (importAuthenticatedClientSessions) { for (AuthenticatedClientSessionModel clientSession : userSession.getAuthenticatedClientSessions().values()) { - importClientSession(importedSession, clientSession); + importClientSession(importedSession, clientSession, tx); } } @@ -517,25 +755,46 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { } - private AuthenticatedClientSessionAdapter importClientSession(UserSessionAdapter importedUserSession, AuthenticatedClientSessionModel clientSession) { + private AuthenticatedClientSessionAdapter importClientSession(UserSessionAdapter importedUserSession, AuthenticatedClientSessionModel clientSession, + InfinispanChangelogBasedTransaction updateTx) { AuthenticatedClientSessionEntity entity = new AuthenticatedClientSessionEntity(); entity.setAction(clientSession.getAction()); entity.setAuthMethod(clientSession.getProtocol()); - entity.setNotes(clientSession.getNotes()); + entity.setNotes(clientSession.getNotes() == null ? new ConcurrentHashMap<>() : clientSession.getNotes()); entity.setProtocolMappers(clientSession.getProtocolMappers()); entity.setRedirectUri(clientSession.getRedirectUri()); entity.setRoles(clientSession.getRoles()); entity.setTimestamp(clientSession.getTimestamp()); + Map clientSessions = importedUserSession.getEntity().getAuthenticatedClientSessions(); clientSessions.put(clientSession.getClient().getId(), entity); - importedUserSession.update(); + SessionUpdateTask importTask = new SessionUpdateTask() { - return new AuthenticatedClientSessionAdapter(entity, clientSession.getClient(), importedUserSession, this, importedUserSession.getCache()); + @Override + public void runUpdate(UserSessionEntity session) { + Map clientSessions = session.getAuthenticatedClientSessions(); + clientSessions.put(clientSession.getClient().getId(), entity); + } + + @Override + public CacheOperation getOperation(UserSessionEntity session) { + return CacheOperation.REPLACE; + } + + @Override + public CrossDCMessageStatus getCrossDCMessageStatus(SessionEntityWrapper sessionWrapper) { + return CrossDCMessageStatus.SYNC; + } + + }; + updateTx.addTask(importedUserSession.getId(), importTask); + + return new AuthenticatedClientSessionAdapter(entity, clientSession.getClient(), importedUserSession, this, updateTx); } } diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProviderFactory.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProviderFactory.java index 663a4b2e7d..110a8124f8 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProviderFactory.java +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProviderFactory.java @@ -18,41 +18,76 @@ package org.keycloak.models.sessions.infinispan; import org.infinispan.Cache; +import org.infinispan.client.hotrod.RemoteCache; +import org.infinispan.persistence.remote.RemoteStore; import org.jboss.logging.Logger; import org.keycloak.Config; +import org.keycloak.cluster.ClusterProvider; import org.keycloak.connections.infinispan.InfinispanConnectionProvider; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.KeycloakSessionTask; +import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.models.UserSessionProvider; import org.keycloak.models.UserSessionProviderFactory; +import org.keycloak.models.sessions.infinispan.changes.sessions.LastSessionRefreshStore; +import org.keycloak.models.sessions.infinispan.changes.sessions.LastSessionRefreshStoreFactory; +import org.keycloak.models.sessions.infinispan.initializer.BaseCacheInitializer; +import org.keycloak.models.sessions.infinispan.initializer.CacheInitializer; +import org.keycloak.models.sessions.infinispan.initializer.DBLockBasedCacheInitializer; +import org.keycloak.models.sessions.infinispan.initializer.SingleWorkerCacheInitializer; +import org.keycloak.models.sessions.infinispan.remotestore.RemoteCacheInvoker; +import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper; import org.keycloak.models.sessions.infinispan.entities.LoginFailureEntity; import org.keycloak.models.sessions.infinispan.entities.LoginFailureKey; -import org.keycloak.models.sessions.infinispan.entities.SessionEntity; -import org.keycloak.models.sessions.infinispan.initializer.InfinispanUserSessionInitializer; -import org.keycloak.models.sessions.infinispan.initializer.OfflineUserSessionLoader; +import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity; +import org.keycloak.models.sessions.infinispan.events.AbstractUserSessionClusterListener; +import org.keycloak.models.sessions.infinispan.events.ClientRemovedSessionEvent; +import org.keycloak.models.sessions.infinispan.events.RealmRemovedSessionEvent; +import org.keycloak.models.sessions.infinispan.events.RemoveAllUserLoginFailuresEvent; +import org.keycloak.models.sessions.infinispan.events.RemoveUserSessionsEvent; +import org.keycloak.models.sessions.infinispan.initializer.InfinispanCacheInitializer; +import org.keycloak.models.sessions.infinispan.initializer.OfflinePersistentUserSessionLoader; +import org.keycloak.models.sessions.infinispan.remotestore.RemoteCacheSessionListener; +import org.keycloak.models.sessions.infinispan.remotestore.RemoteCacheSessionsLoader; +import org.keycloak.models.sessions.infinispan.util.InfinispanUtil; import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.models.utils.PostMigrationEvent; import org.keycloak.provider.ProviderEvent; import org.keycloak.provider.ProviderEventListener; import java.io.Serializable; +import java.util.Set; public class InfinispanUserSessionProviderFactory implements UserSessionProviderFactory { private static final Logger log = Logger.getLogger(InfinispanUserSessionProviderFactory.class); + public static final String PROVIDER_ID = "infinispan"; + + public static final String REALM_REMOVED_SESSION_EVENT = "REALM_REMOVED_EVENT_SESSIONS"; + + public static final String CLIENT_REMOVED_SESSION_EVENT = "CLIENT_REMOVED_SESSION_SESSIONS"; + + public static final String REMOVE_USER_SESSIONS_EVENT = "REMOVE_USER_SESSIONS_EVENT"; + + public static final String REMOVE_ALL_LOGIN_FAILURES_EVENT = "REMOVE_ALL_LOGIN_FAILURES_EVENT"; + private Config.Scope config; + private RemoteCacheInvoker remoteCacheInvoker; + private LastSessionRefreshStore lastSessionRefreshStore; + private LastSessionRefreshStore offlineLastSessionRefreshStore; + @Override public InfinispanUserSessionProvider create(KeycloakSession session) { InfinispanConnectionProvider connections = session.getProvider(InfinispanConnectionProvider.class); - Cache cache = connections.getCache(InfinispanConnectionProvider.SESSION_CACHE_NAME); - Cache offlineSessionsCache = connections.getCache(InfinispanConnectionProvider.OFFLINE_SESSION_CACHE_NAME); + Cache> cache = connections.getCache(InfinispanConnectionProvider.SESSION_CACHE_NAME); + Cache> offlineSessionsCache = connections.getCache(InfinispanConnectionProvider.OFFLINE_SESSION_CACHE_NAME); Cache loginFailures = connections.getCache(InfinispanConnectionProvider.LOGIN_FAILURE_CACHE_NAME); - return new InfinispanUserSessionProvider(session, cache, offlineSessionsCache, loginFailures); + return new InfinispanUserSessionProvider(session, remoteCacheInvoker, lastSessionRefreshStore, offlineLastSessionRefreshStore, cache, offlineSessionsCache, loginFailures); } @Override @@ -62,18 +97,19 @@ public class InfinispanUserSessionProviderFactory implements UserSessionProvider @Override public void postInit(final KeycloakSessionFactory factory) { - // Max count of worker errors. Initialization will end with exception when this number is reached - final int maxErrors = config.getInt("maxErrors", 20); - - // Count of sessions to be computed in each segment - final int sessionsPerSegment = config.getInt("sessionsPerSegment", 100); factory.register(new ProviderEventListener() { @Override public void onEvent(ProviderEvent event) { if (event instanceof PostMigrationEvent) { - loadPersistentSessions(factory, maxErrors, sessionsPerSegment); + KeycloakSession session = ((PostMigrationEvent) event).getSession(); + + checkRemoteCaches(session); + loadPersistentSessions(factory, getMaxErrors(), getSessionsPerSegment()); + registerClusterListeners(session); + loadSessionsFromRemoteCaches(session); + } else if (event instanceof UserModel.UserRemovedEvent) { UserModel.UserRemovedEvent userRemovedEvent = (UserModel.UserRemovedEvent) event; @@ -84,35 +120,169 @@ public class InfinispanUserSessionProviderFactory implements UserSessionProvider }); } + // Max count of worker errors. Initialization will end with exception when this number is reached + private int getMaxErrors() { + return config.getInt("maxErrors", 20); + } + + // Count of sessions to be computed in each segment + private int getSessionsPerSegment() { + return config.getInt("sessionsPerSegment", 100); + } + @Override public void loadPersistentSessions(final KeycloakSessionFactory sessionFactory, final int maxErrors, final int sessionsPerSegment) { - log.debug("Start pre-loading userSessions and clientSessions from persistent storage"); + log.debug("Start pre-loading userSessions from persistent storage"); KeycloakModelUtils.runJobInTransaction(sessionFactory, new KeycloakSessionTask() { @Override public void run(KeycloakSession session) { InfinispanConnectionProvider connections = session.getProvider(InfinispanConnectionProvider.class); - Cache cache = connections.getCache(InfinispanConnectionProvider.WORK_CACHE_NAME); + Cache workCache = connections.getCache(InfinispanConnectionProvider.WORK_CACHE_NAME); + + InfinispanCacheInitializer ispnInitializer = new InfinispanCacheInitializer(sessionFactory, workCache, new OfflinePersistentUserSessionLoader(), "offlineUserSessions", sessionsPerSegment, maxErrors); + + // DB-lock to ensure that persistent sessions are loaded from DB just on one DC. The other DCs will load them from remote cache. + CacheInitializer initializer = new DBLockBasedCacheInitializer(session, ispnInitializer); - InfinispanUserSessionInitializer initializer = new InfinispanUserSessionInitializer(sessionFactory, cache, new OfflineUserSessionLoader(), maxErrors, sessionsPerSegment, "offlineUserSessions"); initializer.initCache(); - initializer.loadPersistentSessions(); + initializer.loadSessions(); } }); - log.debug("Pre-loading userSessions and clientSessions from persistent storage finished"); + log.debug("Pre-loading userSessions from persistent storage finished"); } + + protected void registerClusterListeners(KeycloakSession session) { + KeycloakSessionFactory sessionFactory = session.getKeycloakSessionFactory(); + ClusterProvider cluster = session.getProvider(ClusterProvider.class); + + cluster.registerListener(REALM_REMOVED_SESSION_EVENT, new AbstractUserSessionClusterListener(sessionFactory) { + + @Override + protected void eventReceived(KeycloakSession session, InfinispanUserSessionProvider provider, RealmRemovedSessionEvent sessionEvent) { + provider.onRealmRemovedEvent(sessionEvent.getRealmId()); + } + + }); + + cluster.registerListener(CLIENT_REMOVED_SESSION_EVENT, new AbstractUserSessionClusterListener(sessionFactory) { + + @Override + protected void eventReceived(KeycloakSession session, InfinispanUserSessionProvider provider, ClientRemovedSessionEvent sessionEvent) { + provider.onClientRemovedEvent(sessionEvent.getRealmId(), sessionEvent.getClientUuid()); + } + + }); + + cluster.registerListener(REMOVE_USER_SESSIONS_EVENT, new AbstractUserSessionClusterListener(sessionFactory) { + + @Override + protected void eventReceived(KeycloakSession session, InfinispanUserSessionProvider provider, RemoveUserSessionsEvent sessionEvent) { + provider.onRemoveUserSessionsEvent(sessionEvent.getRealmId()); + } + + }); + + cluster.registerListener(REMOVE_ALL_LOGIN_FAILURES_EVENT, new AbstractUserSessionClusterListener(sessionFactory) { + + @Override + protected void eventReceived(KeycloakSession session, InfinispanUserSessionProvider provider, RemoveAllUserLoginFailuresEvent sessionEvent) { + provider.onRemoveAllUserLoginFailuresEvent(sessionEvent.getRealmId()); + } + + }); + + log.debug("Registered cluster listeners"); + } + + + protected void checkRemoteCaches(KeycloakSession session) { + this.remoteCacheInvoker = new RemoteCacheInvoker(); + + InfinispanConnectionProvider ispn = session.getProvider(InfinispanConnectionProvider.class); + + Cache sessionsCache = ispn.getCache(InfinispanConnectionProvider.SESSION_CACHE_NAME); + boolean sessionsRemoteCache = checkRemoteCache(session, sessionsCache, (RealmModel realm) -> { + return realm.getSsoSessionIdleTimeout() * 1000; + }); + + if (sessionsRemoteCache) { + lastSessionRefreshStore = new LastSessionRefreshStoreFactory().createAndInit(session, sessionsCache, false); + } + + + Cache offlineSessionsCache = ispn.getCache(InfinispanConnectionProvider.OFFLINE_SESSION_CACHE_NAME); + boolean offlineSessionsRemoteCache = checkRemoteCache(session, offlineSessionsCache, (RealmModel realm) -> { + return realm.getOfflineSessionIdleTimeout() * 1000; + }); + + if (offlineSessionsRemoteCache) { + offlineLastSessionRefreshStore = new LastSessionRefreshStoreFactory().createAndInit(session, offlineSessionsCache, true); + } + } + + private boolean checkRemoteCache(KeycloakSession session, Cache ispnCache, RemoteCacheInvoker.MaxIdleTimeLoader maxIdleLoader) { + Set remoteStores = InfinispanUtil.getRemoteStores(ispnCache); + + if (remoteStores.isEmpty()) { + log.debugf("No remote store configured for cache '%s'", ispnCache.getName()); + return false; + } else { + log.infof("Remote store configured for cache '%s'", ispnCache.getName()); + + RemoteCache remoteCache = remoteStores.iterator().next().getRemoteCache(); + + remoteCacheInvoker.addRemoteCache(ispnCache.getName(), remoteCache, maxIdleLoader); + + RemoteCacheSessionListener hotrodListener = RemoteCacheSessionListener.createListener(session, ispnCache, remoteCache); + remoteCache.addClientListener(hotrodListener); + return true; + } + } + + + private void loadSessionsFromRemoteCaches(KeycloakSession session) { + for (String cacheName : remoteCacheInvoker.getRemoteCacheNames()) { + loadSessionsFromRemoteCache(session.getKeycloakSessionFactory(), cacheName, getMaxErrors()); + } + } + + + private void loadSessionsFromRemoteCache(final KeycloakSessionFactory sessionFactory, String cacheName, final int maxErrors) { + log.debugf("Check pre-loading userSessions from remote cache '%s'", cacheName); + + KeycloakModelUtils.runJobInTransaction(sessionFactory, new KeycloakSessionTask() { + + @Override + public void run(KeycloakSession session) { + InfinispanConnectionProvider connections = session.getProvider(InfinispanConnectionProvider.class); + Cache workCache = connections.getCache(InfinispanConnectionProvider.WORK_CACHE_NAME); + + // Use limit for sessionsPerSegment as RemoteCache bulk load doesn't have support for pagination :/ + BaseCacheInitializer initializer = new SingleWorkerCacheInitializer(session, workCache, new RemoteCacheSessionsLoader(cacheName), "remoteCacheLoad::" + cacheName); + + initializer.initCache(); + initializer.loadSessions(); + } + + }); + + log.debugf("Pre-loading userSessions from remote cache '%s' finished", cacheName); + } + + @Override public void close() { } @Override public String getId() { - return "infinispan"; + return PROVIDER_ID; } } diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/UserSessionAdapter.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/UserSessionAdapter.java index f35dea974a..3f09773c11 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/UserSessionAdapter.java +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/UserSessionAdapter.java @@ -17,15 +17,17 @@ package org.keycloak.models.sessions.infinispan; -import org.infinispan.Cache; import org.keycloak.models.AuthenticatedClientSessionModel; import org.keycloak.models.ClientModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.models.UserSessionModel; +import org.keycloak.models.sessions.infinispan.changes.InfinispanChangelogBasedTransaction; +import org.keycloak.models.sessions.infinispan.changes.sessions.LastSessionRefreshChecker; +import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper; +import org.keycloak.models.sessions.infinispan.changes.UserSessionUpdateTask; import org.keycloak.models.sessions.infinispan.entities.AuthenticatedClientSessionEntity; -import org.keycloak.models.sessions.infinispan.entities.SessionEntity; import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity; import java.util.Collections; @@ -33,7 +35,6 @@ import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; /** * @author Stian Thorgersen @@ -44,7 +45,7 @@ public class UserSessionAdapter implements UserSessionModel { private final InfinispanUserSessionProvider provider; - private final Cache cache; + private final InfinispanChangelogBasedTransaction updateTx; private final RealmModel realm; @@ -52,11 +53,11 @@ public class UserSessionAdapter implements UserSessionModel { private final boolean offline; - public UserSessionAdapter(KeycloakSession session, InfinispanUserSessionProvider provider, Cache cache, RealmModel realm, + public UserSessionAdapter(KeycloakSession session, InfinispanUserSessionProvider provider, InfinispanChangelogBasedTransaction updateTx, RealmModel realm, UserSessionEntity entity, boolean offline) { this.session = session; this.provider = provider; - this.cache = cache; + this.updateTx = updateTx; this.realm = realm; this.entity = entity; this.offline = offline; @@ -74,7 +75,7 @@ public class UserSessionAdapter implements UserSessionModel { // Check if client still exists ClientModel client = realm.getClientById(key); if (client != null) { - result.put(key, new AuthenticatedClientSessionAdapter(value, client, this, provider, cache)); + result.put(key, new AuthenticatedClientSessionAdapter(value, client, this, provider, updateTx)); } else { removedClientUUIDS.add(key); } @@ -83,10 +84,18 @@ public class UserSessionAdapter implements UserSessionModel { // Update user session if (!removedClientUUIDS.isEmpty()) { - for (String clientUUID : removedClientUUIDS) { - entity.getAuthenticatedClientSessions().remove(clientUUID); - } - update(); + UserSessionUpdateTask task = new UserSessionUpdateTask() { + + @Override + public void runUpdate(UserSessionEntity entity) { + for (String clientUUID : removedClientUUIDS) { + entity.getAuthenticatedClientSessions().remove(clientUUID); + } + } + + }; + + update(task); } return Collections.unmodifiableMap(result); @@ -114,12 +123,6 @@ public class UserSessionAdapter implements UserSessionModel { return session.users().getUserById(entity.getUser(), realm); } - @Override - public void setUser(UserModel user) { - entity.setUser(user.getId()); - update(); - } - @Override public String getLoginUsername() { return entity.getLoginUsername(); @@ -148,8 +151,26 @@ public class UserSessionAdapter implements UserSessionModel { } public void setLastSessionRefresh(int lastSessionRefresh) { - entity.setLastSessionRefresh(lastSessionRefresh); - update(); + UserSessionUpdateTask task = new UserSessionUpdateTask() { + + @Override + public void runUpdate(UserSessionEntity entity) { + entity.setLastSessionRefresh(lastSessionRefresh); + } + + @Override + public CrossDCMessageStatus getCrossDCMessageStatus(SessionEntityWrapper sessionWrapper) { + return new LastSessionRefreshChecker(provider.getLastSessionRefreshStore(), provider.getOfflineLastSessionRefreshStore()) + .getCrossDCMessageStatus(UserSessionAdapter.this.session, UserSessionAdapter.this.realm, sessionWrapper, offline, lastSessionRefresh); + } + + @Override + public String toString() { + return "setLastSessionRefresh(" + lastSessionRefresh + ')'; + } + }; + + update(task); } @Override @@ -159,22 +180,36 @@ public class UserSessionAdapter implements UserSessionModel { @Override public void setNote(String name, String value) { - if (value == null) { - if (entity.getNotes().containsKey(name)) { - removeNote(name); + UserSessionUpdateTask task = new UserSessionUpdateTask() { + + @Override + public void runUpdate(UserSessionEntity entity) { + if (value == null) { + if (entity.getNotes().containsKey(name)) { + removeNote(name); + } + return; + } + entity.getNotes().put(name, value); } - return; - } - entity.getNotes().put(name, value); - update(); + + }; + + update(task); } @Override public void removeNote(String name) { - if (entity.getNotes() != null) { - entity.getNotes().remove(name); - update(); - } + UserSessionUpdateTask task = new UserSessionUpdateTask() { + + @Override + public void runUpdate(UserSessionEntity entity) { + entity.getNotes().remove(name); + } + + }; + + update(task); } @Override @@ -189,19 +224,34 @@ public class UserSessionAdapter implements UserSessionModel { @Override public void setState(State state) { - entity.setState(state); - update(); + UserSessionUpdateTask task = new UserSessionUpdateTask() { + + @Override + public void runUpdate(UserSessionEntity entity) { + entity.setState(state); + } + + }; + + update(task); } @Override public void restartSession(RealmModel realm, UserModel user, String loginUsername, String ipAddress, String authMethod, boolean rememberMe, String brokerSessionId, String brokerUserId) { - provider.updateSessionEntity(entity, realm, user, loginUsername, ipAddress, authMethod, rememberMe, brokerSessionId, brokerUserId); + UserSessionUpdateTask task = new UserSessionUpdateTask() { - entity.setState(null); - entity.getNotes().clear(); - entity.getAuthenticatedClientSessions().clear(); + @Override + public void runUpdate(UserSessionEntity entity) { + provider.updateSessionEntity(entity, realm, user, loginUsername, ipAddress, authMethod, rememberMe, brokerSessionId, brokerUserId); - update(); + entity.setState(null); + entity.getNotes().clear(); + entity.getAuthenticatedClientSessions().clear(); + } + + }; + + update(task); } @Override @@ -222,11 +272,8 @@ public class UserSessionAdapter implements UserSessionModel { return entity; } - void update() { - provider.getTx().replace(cache, entity.getId(), entity); + void update(UserSessionUpdateTask task) { + updateTx.addTask(getId(), task); } - Cache getCache() { - return cache; - } } diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/InfinispanChangelogBasedTransaction.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/InfinispanChangelogBasedTransaction.java new file mode 100644 index 0000000000..43bb2b3929 --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/InfinispanChangelogBasedTransaction.java @@ -0,0 +1,242 @@ +/* + * 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.models.sessions.infinispan.changes; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import org.infinispan.Cache; +import org.infinispan.context.Flag; +import org.jboss.logging.Logger; +import org.keycloak.models.AbstractKeycloakTransaction; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.models.sessions.infinispan.entities.SessionEntity; +import org.keycloak.models.sessions.infinispan.remotestore.RemoteCacheInvoker; + +/** + * @author Marek Posolda + */ +public class InfinispanChangelogBasedTransaction extends AbstractKeycloakTransaction { + + public static final Logger logger = Logger.getLogger(InfinispanChangelogBasedTransaction.class); + + private final KeycloakSession kcSession; + private final String cacheName; + private final Cache> cache; + private final RemoteCacheInvoker remoteCacheInvoker; + + private final Map> updates = new HashMap<>(); + + public InfinispanChangelogBasedTransaction(KeycloakSession kcSession, String cacheName, Cache> cache, RemoteCacheInvoker remoteCacheInvoker) { + this.kcSession = kcSession; + this.cacheName = cacheName; + this.cache = cache; + this.remoteCacheInvoker = remoteCacheInvoker; + } + + + public void addTask(String key, SessionUpdateTask task) { + SessionUpdatesList myUpdates = updates.get(key); + if (myUpdates == null) { + // Lookup entity from cache + SessionEntityWrapper wrappedEntity = cache.get(key); + if (wrappedEntity == null) { + logger.warnf("Not present cache item for key %s", key); + return; + } + + RealmModel realm = kcSession.realms().getRealm(wrappedEntity.getEntity().getRealm()); + + myUpdates = new SessionUpdatesList<>(realm, wrappedEntity); + updates.put(key, myUpdates); + } + + // Run the update now, so reader in same transaction can see it (TODO: Rollback may not work correctly. See if it's an issue..) + task.runUpdate(myUpdates.getEntityWrapper().getEntity()); + myUpdates.add(task); + } + + + // Create entity and new version for it + public void addTask(String key, SessionUpdateTask task, S entity) { + if (entity == null) { + throw new IllegalArgumentException("Null entity not allowed"); + } + + RealmModel realm = kcSession.realms().getRealm(entity.getRealm()); + SessionEntityWrapper wrappedEntity = new SessionEntityWrapper<>(entity); + SessionUpdatesList myUpdates = new SessionUpdatesList<>(realm, wrappedEntity); + updates.put(key, myUpdates); + + // Run the update now, so reader in same transaction can see it + task.runUpdate(entity); + myUpdates.add(task); + } + + + public void reloadEntityInCurrentTransaction(RealmModel realm, String key, SessionEntityWrapper entity) { + if (entity == null) { + throw new IllegalArgumentException("Null entity not allowed"); + } + + SessionEntityWrapper latestEntity = cache.get(key); + if (latestEntity == null) { + return; + } + + SessionUpdatesList newUpdates = new SessionUpdatesList<>(realm, latestEntity); + + SessionUpdatesList existingUpdates = updates.get(key); + if (existingUpdates != null) { + newUpdates.setUpdateTasks(existingUpdates.getUpdateTasks()); + } + + updates.put(key, newUpdates); + } + + + public SessionEntityWrapper get(String key) { + SessionUpdatesList myUpdates = updates.get(key); + if (myUpdates == null) { + SessionEntityWrapper wrappedEntity = cache.get(key); + if (wrappedEntity == null) { + return null; + } + + RealmModel realm = kcSession.realms().getRealm(wrappedEntity.getEntity().getRealm()); + + myUpdates = new SessionUpdatesList<>(realm, wrappedEntity); + updates.put(key, myUpdates); + + return wrappedEntity; + } else { + S entity = myUpdates.getEntityWrapper().getEntity(); + + // If entity is scheduled for remove, we don't return it. + boolean scheduledForRemove = myUpdates.getUpdateTasks().stream().filter((SessionUpdateTask task) -> { + + return task.getOperation(entity) == SessionUpdateTask.CacheOperation.REMOVE; + + }).findFirst().isPresent(); + + return scheduledForRemove ? null : myUpdates.getEntityWrapper(); + } + } + + + @Override + protected void commitImpl() { + for (Map.Entry> entry : updates.entrySet()) { + SessionUpdatesList sessionUpdates = entry.getValue(); + SessionEntityWrapper sessionWrapper = sessionUpdates.getEntityWrapper(); + + RealmModel realm = sessionUpdates.getRealm(); + + MergedUpdate merged = MergedUpdate.computeUpdate(sessionUpdates.getUpdateTasks(), sessionWrapper); + + if (merged != null) { + // Now run the operation in our cluster + runOperationInCluster(entry.getKey(), merged, sessionWrapper); + + // Check if we need to send message to second DC + remoteCacheInvoker.runTask(kcSession, realm, cacheName, entry.getKey(), merged, sessionWrapper); + } + } + } + + + private void runOperationInCluster(String key, MergedUpdate task, SessionEntityWrapper sessionWrapper) { + S session = sessionWrapper.getEntity(); + SessionUpdateTask.CacheOperation operation = task.getOperation(session); + + // Don't need to run update of underlying entity. Local updates were already run + //task.runUpdate(session); + + switch (operation) { + case REMOVE: + // Just remove it + cache + .getAdvancedCache().withFlags(Flag.IGNORE_RETURN_VALUES) + .remove(key); + break; + case ADD: + cache + .getAdvancedCache().withFlags(Flag.IGNORE_RETURN_VALUES) + .put(key, sessionWrapper, task.getLifespanMs(), TimeUnit.MILLISECONDS); + break; + case ADD_IF_ABSENT: + SessionEntityWrapper existing = cache.putIfAbsent(key, sessionWrapper); + if (existing != null) { + throw new IllegalStateException("There is already existing value in cache for key " + key); + } + break; + case REPLACE: + replace(key, task, sessionWrapper); + break; + default: + throw new IllegalStateException("Unsupported state " + operation); + } + + } + + + private void replace(String key, MergedUpdate task, SessionEntityWrapper oldVersionEntity) { + boolean replaced = false; + S session = oldVersionEntity.getEntity(); + + while (!replaced) { + SessionEntityWrapper newVersionEntity = generateNewVersionAndWrapEntity(session, oldVersionEntity.getLocalMetadata()); + + // Atomic cluster-aware replace + replaced = cache.replace(key, oldVersionEntity, newVersionEntity); + + // Replace fail. Need to load latest entity from cache, apply updates again and try to replace in cache again + if (!replaced) { + logger.debugf("Replace failed for entity: %s . Will try again", key); + + oldVersionEntity = cache.get(key); + + if (oldVersionEntity == null) { + logger.debugf("Entity %s not found. Maybe removed in the meantime. Replace task will be ignored", key); + return; + } + + session = oldVersionEntity.getEntity(); + + task.runUpdate(session); + } else { + if (logger.isTraceEnabled()) { + logger.tracef("Replace SUCCESS for entity: %s . old version: %d, new version: %d", key, oldVersionEntity.getVersion(), newVersionEntity.getVersion()); + } + } + } + + } + + + @Override + protected void rollbackImpl() { + } + + private SessionEntityWrapper generateNewVersionAndWrapEntity(S entity, Map localMetadata) { + return new SessionEntityWrapper<>(localMetadata, entity); + } + +} diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/MergedUpdate.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/MergedUpdate.java new file mode 100644 index 0000000000..1f24f84faf --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/MergedUpdate.java @@ -0,0 +1,104 @@ +/* + * Copyright 2016 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.models.sessions.infinispan.changes; + +import java.util.LinkedList; +import java.util.List; + +import org.keycloak.models.sessions.infinispan.entities.SessionEntity; + +/** + * @author Marek Posolda + */ +class MergedUpdate implements SessionUpdateTask { + + private List> childUpdates = new LinkedList<>(); + private CacheOperation operation; + private CrossDCMessageStatus crossDCMessageStatus; + + + public MergedUpdate(CacheOperation operation, CrossDCMessageStatus crossDCMessageStatus) { + this.operation = operation; + this.crossDCMessageStatus = crossDCMessageStatus; + } + + @Override + public void runUpdate(S session) { + for (SessionUpdateTask child : childUpdates) { + child.runUpdate(session); + } + } + + @Override + public CacheOperation getOperation(S session) { + return operation; + } + + @Override + public CrossDCMessageStatus getCrossDCMessageStatus(SessionEntityWrapper sessionWrapper) { + return crossDCMessageStatus; + } + + + public static MergedUpdate computeUpdate(List> childUpdates, SessionEntityWrapper sessionWrapper) { + if (childUpdates == null || childUpdates.isEmpty()) { + return null; + } + + MergedUpdate result = null; + S session = sessionWrapper.getEntity(); + for (SessionUpdateTask child : childUpdates) { + if (result == null) { + result = new MergedUpdate<>(child.getOperation(session), child.getCrossDCMessageStatus(sessionWrapper)); + result.childUpdates.add(child); + } else { + + // Merge the operations. REMOVE is special case as other operations are not needed then. + CacheOperation mergedOp = result.getOperation(session).merge(child.getOperation(session), session); + if (mergedOp == CacheOperation.REMOVE) { + result = new MergedUpdate<>(child.getOperation(session), child.getCrossDCMessageStatus(sessionWrapper)); + result.childUpdates.add(child); + return result; + } + + result.operation = mergedOp; + + // Check if we need to send message to other DCs and how critical it is + CrossDCMessageStatus currentDCStatus = result.getCrossDCMessageStatus(sessionWrapper); + + // Optimization. If we already have SYNC, we don't need to retrieve childDCStatus + if (currentDCStatus != CrossDCMessageStatus.SYNC) { + CrossDCMessageStatus childDCStatus = child.getCrossDCMessageStatus(sessionWrapper); + result.crossDCMessageStatus = currentDCStatus.merge(childDCStatus); + } + + // Finally add another update to the result + result.childUpdates.add(child); + } + } + + return result; + } + + @Override + public String toString() { + return "MergedUpdate" + childUpdates; + } + + +} diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/SessionEntityWrapper.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/SessionEntityWrapper.java new file mode 100644 index 0000000000..ca21487674 --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/SessionEntityWrapper.java @@ -0,0 +1,155 @@ +/* + * 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.models.sessions.infinispan.changes; + +import java.io.IOException; +import java.io.ObjectInput; +import java.io.ObjectOutput; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +import org.infinispan.commons.marshall.Externalizer; +import org.infinispan.commons.marshall.MarshallUtil; +import org.infinispan.commons.marshall.SerializeWith; +import org.keycloak.models.sessions.infinispan.changes.sessions.SessionData; +import org.keycloak.models.sessions.infinispan.entities.SessionEntity; + +/** + * @author Marek Posolda + */ +@SerializeWith(SessionEntityWrapper.ExternalizerImpl.class) +public class SessionEntityWrapper { + + private UUID version; + private final S entity; + private final Map localMetadata; + + + protected SessionEntityWrapper(UUID version, Map localMetadata, S entity) { + if (version == null) { + throw new IllegalArgumentException("Version UUID can't be null"); + } + + this.version = version; + this.localMetadata = localMetadata; + this.entity = entity; + } + + public SessionEntityWrapper(Map localMetadata, S entity) { + this(UUID.randomUUID(),localMetadata, entity); + } + + public SessionEntityWrapper(S entity) { + this(new ConcurrentHashMap<>(), entity); + } + + + public UUID getVersion() { + return version; + } + + public void setVersion(UUID version) { + this.version = version; + } + + + public S getEntity() { + return entity; + } + + public String getLocalMetadataNote(String key) { + return localMetadata.get(key); + } + + public void putLocalMetadataNote(String key, String value) { + localMetadata.put(key, value); + } + + public Integer getLocalMetadataNoteInt(String key) { + String note = getLocalMetadataNote(key); + return note==null ? null : Integer.parseInt(note); + } + + public void putLocalMetadataNoteInt(String key, int value) { + localMetadata.put(key, String.valueOf(value)); + } + + public Map getLocalMetadata() { + return localMetadata; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof SessionEntityWrapper)) return false; + + SessionEntityWrapper that = (SessionEntityWrapper) o; + + if (!Objects.equals(version, that.version)) { + return false; + } + + return Objects.equals(entity, that.entity); + } + + + @Override + public int hashCode() { + return Objects.hashCode(version) * 17 + + Objects.hashCode(entity); + } + + @Override + public String toString() { + return "SessionEntityWrapper{" + "version=" + version + ", entity=" + entity + ", localMetadata=" + localMetadata + '}'; + } + + public static class ExternalizerImpl implements Externalizer { + + + @Override + public void writeObject(ObjectOutput output, SessionEntityWrapper obj) throws IOException { + MarshallUtil.marshallUUID(obj.version, output, false); + MarshallUtil.marshallMap(obj.localMetadata, output); + output.writeObject(obj.getEntity()); + } + + + @Override + public SessionEntityWrapper readObject(ObjectInput input) throws IOException, ClassNotFoundException { + UUID objVersion = MarshallUtil.unmarshallUUID(input, false); + + Map localMetadata = MarshallUtil.unmarshallMap(input, new MarshallUtil.MapBuilder>() { + + @Override + public Map build(int size) { + return new ConcurrentHashMap<>(size); + } + + }); + + SessionEntity entity = (SessionEntity) input.readObject(); + + return new SessionEntityWrapper<>(objVersion, localMetadata, entity); + } + + } +} diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/SessionUpdateTask.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/SessionUpdateTask.java new file mode 100644 index 0000000000..66f88baa05 --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/SessionUpdateTask.java @@ -0,0 +1,87 @@ +/* + * 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.models.sessions.infinispan.changes; + +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.models.sessions.infinispan.entities.SessionEntity; + +/** + * @author Marek Posolda + */ +public interface SessionUpdateTask { + + void runUpdate(S entity); + + CacheOperation getOperation(S entity); + + CrossDCMessageStatus getCrossDCMessageStatus(SessionEntityWrapper sessionWrapper); + + default long getLifespanMs() { + return -1; + } + + + enum CacheOperation { + + ADD, + ADD_IF_ABSENT, // ADD_IF_ABSENT throws an exception if there is existing value + REMOVE, + REPLACE; + + CacheOperation merge(CacheOperation other, SessionEntity entity) { + if (this == REMOVE || other == REMOVE) { + return REMOVE; + } + + if (this == ADD | this == ADD_IF_ABSENT) { + if (other == ADD | other == ADD_IF_ABSENT) { + throw new IllegalStateException("Illegal state. Task already in progress for session " + entity.getId()); + } + + return this; + } + + // Lowest priority + return REPLACE; + } + } + + + enum CrossDCMessageStatus { + SYNC, + //ASYNC, + // QUEUE, + NOT_NEEDED; + + + CrossDCMessageStatus merge(CrossDCMessageStatus other) { + if (this == SYNC || other == SYNC) { + return SYNC; + } + + /*if (this == ASYNC || other == ASYNC) { + return ASYNC; + }*/ + + return NOT_NEEDED; + } + + } + +} diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/SessionUpdatesList.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/SessionUpdatesList.java new file mode 100644 index 0000000000..136df30b2d --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/SessionUpdatesList.java @@ -0,0 +1,64 @@ +/* + * Copyright 2016 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.models.sessions.infinispan.changes; + +import java.util.LinkedList; +import java.util.List; + +import org.keycloak.models.RealmModel; +import org.keycloak.models.sessions.infinispan.entities.SessionEntity; + +/** + * tracks all changes to the underlying session in this transaction + * + * @author Marek Posolda + */ +class SessionUpdatesList { + + private final RealmModel realm; + + private final SessionEntityWrapper entityWrapper; + + private List> updateTasks = new LinkedList<>(); + + public SessionUpdatesList(RealmModel realm, SessionEntityWrapper entityWrapper) { + this.realm = realm; + this.entityWrapper = entityWrapper; + } + + public RealmModel getRealm() { + return realm; + } + + public SessionEntityWrapper getEntityWrapper() { + return entityWrapper; + } + + + public void add(SessionUpdateTask task) { + updateTasks.add(task); + } + + public List> getUpdateTasks() { + return updateTasks; + } + + public void setUpdateTasks(List> updateTasks) { + this.updateTasks = updateTasks; + } +} diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/UserSessionClientSessionUpdateTask.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/UserSessionClientSessionUpdateTask.java new file mode 100644 index 0000000000..56e0403143 --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/UserSessionClientSessionUpdateTask.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.models.sessions.infinispan.changes; + +import org.jboss.logging.Logger; +import org.keycloak.models.sessions.infinispan.entities.AuthenticatedClientSessionEntity; +import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity; + +/** + * Task for create or update AuthenticatedClientSessionEntity within userSession + * + * @author Marek Posolda + */ +public abstract class UserSessionClientSessionUpdateTask extends UserSessionUpdateTask { + + public static final Logger logger = Logger.getLogger(UserSessionClientSessionUpdateTask.class); + + private final String clientUUID; + + public UserSessionClientSessionUpdateTask(String clientUUID) { + this.clientUUID = clientUUID; + } + + @Override + public void runUpdate(UserSessionEntity userSession) { + AuthenticatedClientSessionEntity clientSession = userSession.getAuthenticatedClientSessions().get(clientUUID); + if (clientSession == null) { + logger.warnf("Not found authenticated client session entity for client %s in userSession %s", clientUUID, userSession.getId()); + return; + } + + runClientSessionUpdate(clientSession); + } + + protected abstract void runClientSessionUpdate(AuthenticatedClientSessionEntity entity); +} diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/UserSessionUpdateTask.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/UserSessionUpdateTask.java new file mode 100644 index 0000000000..4fd4bbebf1 --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/UserSessionUpdateTask.java @@ -0,0 +1,38 @@ +/* + * 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.models.sessions.infinispan.changes; + +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity; + +/** + * @author Marek Posolda + */ +public abstract class UserSessionUpdateTask implements SessionUpdateTask { + + @Override + public CacheOperation getOperation(UserSessionEntity session) { + return CacheOperation.REPLACE; + } + + @Override + public CrossDCMessageStatus getCrossDCMessageStatus(SessionEntityWrapper sessionWrapper) { + return CrossDCMessageStatus.SYNC; + } +} diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/sessions/LastSessionRefreshChecker.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/sessions/LastSessionRefreshChecker.java new file mode 100644 index 0000000000..f9adf9b8cb --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/sessions/LastSessionRefreshChecker.java @@ -0,0 +1,81 @@ +/* + * Copyright 2017 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.models.sessions.infinispan.changes.sessions; + +import org.jboss.logging.Logger; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper; +import org.keycloak.models.sessions.infinispan.changes.SessionUpdateTask; +import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity; + +/** + * @author Marek Posolda + */ +public class LastSessionRefreshChecker { + + public static final Logger logger = Logger.getLogger(LastSessionRefreshChecker.class); + + private final LastSessionRefreshStore store; + private final LastSessionRefreshStore offlineStore; + + + public LastSessionRefreshChecker(LastSessionRefreshStore store, LastSessionRefreshStore offlineStore) { + this.store = store; + this.offlineStore = offlineStore; + } + + + public SessionUpdateTask.CrossDCMessageStatus getCrossDCMessageStatus(KeycloakSession kcSession, RealmModel realm, SessionEntityWrapper sessionWrapper, boolean offline, int newLastSessionRefresh) { + // revokeRefreshToken always writes everything to remoteCache immediately + if (realm.isRevokeRefreshToken()) { + return SessionUpdateTask.CrossDCMessageStatus.SYNC; + } + + // We're likely not in cross-dc environment. Doesn't matter what we return + LastSessionRefreshStore storeToUse = offline ? offlineStore : store; + if (storeToUse == null) { + return SessionUpdateTask.CrossDCMessageStatus.SYNC; + } + + Boolean ignoreRemoteCacheUpdate = (Boolean) kcSession.getAttribute(LastSessionRefreshListener.IGNORE_REMOTE_CACHE_UPDATE); + if (ignoreRemoteCacheUpdate != null && ignoreRemoteCacheUpdate) { + return SessionUpdateTask.CrossDCMessageStatus.NOT_NEEDED; + } + + Integer lsrr = sessionWrapper.getLocalMetadataNoteInt(UserSessionEntity.LAST_SESSION_REFRESH_REMOTE); + if (lsrr == null) { + logger.warnf("Not available lsrr note on user session %s.", sessionWrapper.getEntity().getId()); + return SessionUpdateTask.CrossDCMessageStatus.SYNC; + } + + int idleTimeout = offline ? realm.getOfflineSessionIdleTimeout() : realm.getSsoSessionIdleTimeout(); + + if (lsrr + (idleTimeout / 2) <= newLastSessionRefresh) { + logger.debugf("We are going to write remotely. Remote last session refresh: %d, New last session refresh: %d", (int) lsrr, newLastSessionRefresh); + return SessionUpdateTask.CrossDCMessageStatus.SYNC; + } + + logger.debugf("Skip writing last session refresh to the remoteCache. Session %s newLastSessionRefresh %d", sessionWrapper.getEntity().getId(), newLastSessionRefresh); + + storeToUse.putLastSessionRefresh(kcSession, sessionWrapper.getEntity().getId(), realm.getId(), newLastSessionRefresh); + + return SessionUpdateTask.CrossDCMessageStatus.NOT_NEEDED; + } + +} diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/sessions/LastSessionRefreshEvent.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/sessions/LastSessionRefreshEvent.java new file mode 100644 index 0000000000..7d2af5fddf --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/sessions/LastSessionRefreshEvent.java @@ -0,0 +1,73 @@ +/* + * Copyright 2017 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.models.sessions.infinispan.changes.sessions; + +import java.io.IOException; +import java.io.ObjectInput; +import java.io.ObjectOutput; +import java.util.HashMap; +import java.util.Map; + +import org.infinispan.commons.marshall.Externalizer; +import org.infinispan.commons.marshall.MarshallUtil; +import org.infinispan.commons.marshall.SerializeWith; +import org.keycloak.cluster.ClusterEvent; + +/** + * @author Marek Posolda + */ +@SerializeWith(LastSessionRefreshEvent.ExternalizerImpl.class) +public class LastSessionRefreshEvent implements ClusterEvent { + + private final Map lastSessionRefreshes; + + public LastSessionRefreshEvent(Map lastSessionRefreshes) { + this.lastSessionRefreshes = lastSessionRefreshes; + } + + public Map getLastSessionRefreshes() { + return lastSessionRefreshes; + } + + + public static class ExternalizerImpl implements Externalizer { + + + @Override + public void writeObject(ObjectOutput output, LastSessionRefreshEvent obj) throws IOException { + MarshallUtil.marshallMap(obj.lastSessionRefreshes, output); + } + + + @Override + public LastSessionRefreshEvent readObject(ObjectInput input) throws IOException, ClassNotFoundException { + Map map = MarshallUtil.unmarshallMap(input, new MarshallUtil.MapBuilder>() { + + @Override + public Map build(int size) { + return new HashMap<>(size); + } + + }); + + LastSessionRefreshEvent event = new LastSessionRefreshEvent(map); + return event; + } + + } +} diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/sessions/LastSessionRefreshListener.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/sessions/LastSessionRefreshListener.java new file mode 100644 index 0000000000..1bc151fb2a --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/sessions/LastSessionRefreshListener.java @@ -0,0 +1,112 @@ +/* + * Copyright 2017 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.models.sessions.infinispan.changes.sessions; + +import java.util.Map; + +import org.infinispan.Cache; +import org.infinispan.client.hotrod.RemoteCache; +import org.infinispan.client.hotrod.event.ClientEvent; +import org.jboss.logging.Logger; +import org.keycloak.cluster.ClusterEvent; +import org.keycloak.cluster.ClusterListener; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserSessionModel; +import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper; +import org.keycloak.models.sessions.infinispan.util.InfinispanUtil; +import org.keycloak.models.utils.KeycloakModelUtils; + +/** + * @author Marek Posolda + */ +public class LastSessionRefreshListener implements ClusterListener { + + public static final Logger logger = Logger.getLogger(LastSessionRefreshListener.class); + + public static final String IGNORE_REMOTE_CACHE_UPDATE = "IGNORE_REMOTE_CACHE_UPDATE"; + + private final boolean offline; + + private final KeycloakSessionFactory sessionFactory; + private final Cache cache; + private final boolean distributed; + private final String myAddress; + + public LastSessionRefreshListener(KeycloakSession session, Cache cache, boolean offline) { + this.sessionFactory = session.getKeycloakSessionFactory(); + this.cache = cache; + this.offline = offline; + + this.distributed = InfinispanUtil.isDistributedCache(cache); + if (this.distributed) { + this.myAddress = InfinispanUtil.getMyAddress(session); + } else { + this.myAddress = null; + } + } + + @Override + public void eventReceived(ClusterEvent event) { + Map lastSessionRefreshes = ((LastSessionRefreshEvent) event).getLastSessionRefreshes(); + + if (logger.isDebugEnabled()) { + logger.debugf("Received refreshes. Offline %b, refreshes: %s", offline, lastSessionRefreshes); + } + + lastSessionRefreshes.entrySet().stream().forEach((entry) -> { + String sessionId = entry.getKey(); + String realmId = entry.getValue().getRealmId(); + int lastSessionRefresh = entry.getValue().getLastSessionRefresh(); + + // All nodes will receive the message. So ensure that each node updates just lastSessionRefreshes owned by him. + if (shouldUpdateLocalCache(sessionId)) { + KeycloakModelUtils.runJobInTransaction(sessionFactory, (kcSession) -> { + + RealmModel realm = kcSession.realms().getRealm(realmId); + UserSessionModel userSession = kcSession.sessions().getUserSession(realm, sessionId); + if (userSession == null) { + logger.debugf("User session %s not available on node %s", sessionId, myAddress); + } else { + // Update just if lastSessionRefresh from event is bigger than ours + if (lastSessionRefresh > userSession.getLastSessionRefresh()) { + + // Ensure that remoteCache won't be updated due to this + kcSession.setAttribute(IGNORE_REMOTE_CACHE_UPDATE, true); + + userSession.setLastSessionRefresh(lastSessionRefresh); + } + } + }); + } + + }); + } + + + // For distributed caches, ensure that local modification is executed just on owner + protected boolean shouldUpdateLocalCache(String key) { + if (!distributed) { + return true; + } else { + String keyAddress = InfinispanUtil.getKeyPrimaryOwnerAddress(cache, key); + return myAddress.equals(keyAddress); + } + } +} diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/sessions/LastSessionRefreshStore.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/sessions/LastSessionRefreshStore.java new file mode 100644 index 0000000000..c50bcf1951 --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/sessions/LastSessionRefreshStore.java @@ -0,0 +1,101 @@ +/* + * Copyright 2017 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.models.sessions.infinispan.changes.sessions; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.jboss.logging.Logger; +import org.keycloak.cluster.ClusterProvider; +import org.keycloak.common.util.Time; +import org.keycloak.models.KeycloakSession; + +/** + * Tracks the queue of lastSessionRefreshes, which were updated on this host. Those will be sent to the second DC in bulk, so second DC can update + * lastSessionRefreshes on it's side. Message is sent either periodically or if there are lots of stored lastSessionRefreshes. + * + * @author Marek Posolda + */ +public class LastSessionRefreshStore { + + protected static final Logger logger = Logger.getLogger(LastSessionRefreshStore.class); + + private final int maxIntervalBetweenMessagesSeconds; + private final int maxCount; + private final String eventKey; + + private volatile Map lastSessionRefreshes = new ConcurrentHashMap<>(); + + private volatile int lastRun = Time.currentTime(); + + + protected LastSessionRefreshStore(int maxIntervalBetweenMessagesSeconds, int maxCount, String eventKey) { + this.maxIntervalBetweenMessagesSeconds = maxIntervalBetweenMessagesSeconds; + this.maxCount = maxCount; + this.eventKey = eventKey; + } + + + public void putLastSessionRefresh(KeycloakSession kcSession, String sessionId, String realmId, int lastSessionRefresh) { + lastSessionRefreshes.put(sessionId, new SessionData(realmId, lastSessionRefresh)); + + // Assume that lastSessionRefresh is same or close to current time + checkSendingMessage(kcSession, lastSessionRefresh); + } + + + void checkSendingMessage(KeycloakSession kcSession, int currentTime) { + if (lastSessionRefreshes.size() >= maxCount || lastRun + maxIntervalBetweenMessagesSeconds <= currentTime) { + Map refreshesToSend = prepareSendingMessage(currentTime); + + // Sending message doesn't need to be synchronized + if (refreshesToSend != null) { + sendMessage(kcSession, refreshesToSend); + } + } + } + + + // synchronized manipulation with internal object instances. Will return map if message should be sent. Otherwise return null + private synchronized Map prepareSendingMessage(int currentTime) { + if (lastSessionRefreshes.size() >= maxCount || lastRun + maxIntervalBetweenMessagesSeconds <= currentTime) { + // Create new map instance, so that new writers will use that one + Map copiedRefreshesToSend = lastSessionRefreshes; + lastSessionRefreshes = new ConcurrentHashMap<>(); + lastRun = currentTime; + + return copiedRefreshesToSend; + } else { + return null; + } + } + + + protected void sendMessage(KeycloakSession kcSession, Map refreshesToSend) { + LastSessionRefreshEvent event = new LastSessionRefreshEvent(refreshesToSend); + + if (logger.isDebugEnabled()) { + logger.debugf("Sending lastSessionRefreshes for key '%s'. Refreshes: %s", eventKey, event.getLastSessionRefreshes().toString()); + } + + // Don't notify local DC about the lastSessionRefreshes. They were processed here already + ClusterProvider cluster = kcSession.getProvider(ClusterProvider.class); + cluster.notify(eventKey, event, true, ClusterProvider.DCNotify.ALL_BUT_LOCAL_DC); + } + +} diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/sessions/LastSessionRefreshStoreFactory.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/sessions/LastSessionRefreshStoreFactory.java new file mode 100644 index 0000000000..9c38251cf7 --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/sessions/LastSessionRefreshStoreFactory.java @@ -0,0 +1,74 @@ +/* + * Copyright 2017 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.models.sessions.infinispan.changes.sessions; + +import org.infinispan.Cache; +import org.keycloak.cluster.ClusterProvider; +import org.keycloak.common.util.Time; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper; +import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity; +import org.keycloak.timer.TimerProvider; + +/** + * @author Marek Posolda + */ +public class LastSessionRefreshStoreFactory { + + // Timer interval. The store will be checked every 5 seconds whether the message with stored lastSessionRefreshes + public static final long DEFAULT_TIMER_INTERVAL_MS = 5000; + + // Max interval between messages. It means that when message is sent to second DC, then another message will be sent at least after 60 seconds. + public static final int DEFAULT_MAX_INTERVAL_BETWEEN_MESSAGES_SECONDS = 60; + + // Max count of lastSessionRefreshes. It count of lastSessionRefreshes reach this value, the message is sent to second DC + public static final int DEFAULT_MAX_COUNT = 100; + + + public LastSessionRefreshStore createAndInit(KeycloakSession kcSession, Cache cache, boolean offline) { + return createAndInit(kcSession, cache, DEFAULT_TIMER_INTERVAL_MS, DEFAULT_MAX_INTERVAL_BETWEEN_MESSAGES_SECONDS, DEFAULT_MAX_COUNT, offline); + } + + + public LastSessionRefreshStore createAndInit(KeycloakSession kcSession, Cache cache, long timerIntervalMs, int maxIntervalBetweenMessagesSeconds, int maxCount, boolean offline) { + String eventKey = offline ? "lastSessionRefreshes-offline" : "lastSessionRefreshes"; + LastSessionRefreshStore store = createStoreInstance(maxIntervalBetweenMessagesSeconds, maxCount, eventKey); + + // Register listener + ClusterProvider cluster = kcSession.getProvider(ClusterProvider.class); + cluster.registerListener(eventKey, new LastSessionRefreshListener(kcSession, cache, offline)); + + // Setup periodic timer check + TimerProvider timer = kcSession.getProvider(TimerProvider.class); + timer.scheduleTask((KeycloakSession keycloakSession) -> { + + store.checkSendingMessage(keycloakSession, Time.currentTime()); + + }, timerIntervalMs, eventKey); + + return store; + } + + + protected LastSessionRefreshStore createStoreInstance(int maxIntervalBetweenMessagesSeconds, int maxCount, String eventKey) { + return new LastSessionRefreshStore(maxIntervalBetweenMessagesSeconds, maxCount, eventKey); + } + + + +} diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/sessions/SessionData.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/sessions/SessionData.java new file mode 100644 index 0000000000..5f78eda326 --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/sessions/SessionData.java @@ -0,0 +1,74 @@ +/* + * Copyright 2017 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.models.sessions.infinispan.changes.sessions; + +import java.io.IOException; +import java.io.ObjectInput; +import java.io.ObjectOutput; + +import org.infinispan.commons.marshall.Externalizer; +import org.infinispan.commons.marshall.MarshallUtil; +import org.infinispan.commons.marshall.SerializeWith; + +/** + * @author Marek Posolda + */ +@SerializeWith(SessionData.ExternalizerImpl.class) +public class SessionData { + + private final String realmId; + private final int lastSessionRefresh; + + public SessionData(String realmId, int lastSessionRefresh) { + this.realmId = realmId; + this.lastSessionRefresh = lastSessionRefresh; + } + + public String getRealmId() { + return realmId; + } + + public int getLastSessionRefresh() { + return lastSessionRefresh; + } + + @Override + public String toString() { + return String.format("realmId: %s, lastSessionRefresh: %d", realmId, lastSessionRefresh); + } + + public static class ExternalizerImpl implements Externalizer { + + + @Override + public void writeObject(ObjectOutput output, SessionData obj) throws IOException { + MarshallUtil.marshallString(obj.realmId, output); + MarshallUtil.marshallInt(output, obj.lastSessionRefresh); + } + + + @Override + public SessionData readObject(ObjectInput input) throws IOException, ClassNotFoundException { + String realmId = MarshallUtil.unmarshallString(input); + int lastSessionRefresh = MarshallUtil.unmarshallInt(input); + + return new SessionData(realmId, lastSessionRefresh); + } + + } +} diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/AuthenticatedClientSessionEntity.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/AuthenticatedClientSessionEntity.java index 3641d5f171..f67d736195 100644 --- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/AuthenticatedClientSessionEntity.java +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/AuthenticatedClientSessionEntity.java @@ -17,13 +17,24 @@ package org.keycloak.models.sessions.infinispan.entities; +import java.io.IOException; +import java.io.ObjectInput; +import java.io.ObjectOutput; import java.io.Serializable; import java.util.Map; import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import org.infinispan.commons.marshall.Externalizer; +import org.infinispan.commons.marshall.MarshallUtil; +import org.infinispan.commons.marshall.SerializeWith; +import org.keycloak.models.sessions.infinispan.util.KeycloakMarshallUtil; /** + * * @author Marek Posolda */ +@SerializeWith(AuthenticatedClientSessionEntity.ExternalizerImpl.class) public class AuthenticatedClientSessionEntity implements Serializable { private String authMethod; @@ -33,7 +44,7 @@ public class AuthenticatedClientSessionEntity implements Serializable { private Set roles; private Set protocolMappers; - private Map notes; + private Map notes = new ConcurrentHashMap<>(); public String getAuthMethod() { return authMethod; @@ -91,4 +102,46 @@ public class AuthenticatedClientSessionEntity implements Serializable { this.notes = notes; } + + public static class ExternalizerImpl implements Externalizer { + + @Override + public void writeObject(ObjectOutput output, AuthenticatedClientSessionEntity session) throws IOException { + MarshallUtil.marshallString(session.getAuthMethod(), output); + MarshallUtil.marshallString(session.getRedirectUri(), output); + MarshallUtil.marshallInt(output, session.getTimestamp()); + MarshallUtil.marshallString(session.getAction(), output); + + Map notes = session.getNotes(); + KeycloakMarshallUtil.writeMap(notes, KeycloakMarshallUtil.STRING_EXT, KeycloakMarshallUtil.STRING_EXT, output); + + KeycloakMarshallUtil.writeCollection(session.getProtocolMappers(), KeycloakMarshallUtil.STRING_EXT, output); + KeycloakMarshallUtil.writeCollection(session.getRoles(), KeycloakMarshallUtil.STRING_EXT, output); + } + + + @Override + public AuthenticatedClientSessionEntity readObject(ObjectInput input) throws IOException, ClassNotFoundException { + AuthenticatedClientSessionEntity sessionEntity = new AuthenticatedClientSessionEntity(); + + sessionEntity.setAuthMethod(MarshallUtil.unmarshallString(input)); + sessionEntity.setRedirectUri(MarshallUtil.unmarshallString(input)); + sessionEntity.setTimestamp(MarshallUtil.unmarshallInt(input)); + sessionEntity.setAction(MarshallUtil.unmarshallString(input)); + + Map notes = KeycloakMarshallUtil.readMap(input, KeycloakMarshallUtil.STRING_EXT, KeycloakMarshallUtil.STRING_EXT, + new KeycloakMarshallUtil.ConcurrentHashMapBuilder<>()); + sessionEntity.setNotes(notes); + + Set protocolMappers = KeycloakMarshallUtil.readCollection(input, KeycloakMarshallUtil.STRING_EXT, new KeycloakMarshallUtil.HashSetBuilder<>()); + sessionEntity.setProtocolMappers(protocolMappers); + + Set roles = KeycloakMarshallUtil.readCollection(input, KeycloakMarshallUtil.STRING_EXT, new KeycloakMarshallUtil.HashSetBuilder<>()); + sessionEntity.setRoles(roles); + + return sessionEntity; + } + + } + } diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/SessionEntity.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/SessionEntity.java index 8aeb79ea18..25ac2a4efe 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/SessionEntity.java +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/SessionEntity.java @@ -19,10 +19,12 @@ package org.keycloak.models.sessions.infinispan.entities; import java.io.Serializable; +import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper; + /** * @author Stian Thorgersen */ -public class SessionEntity implements Serializable { +public abstract class SessionEntity implements Serializable { private String id; @@ -60,4 +62,10 @@ public class SessionEntity implements Serializable { public int hashCode() { return id != null ? id.hashCode() : 0; } + + + public SessionEntityWrapper mergeRemoteEntityWithLocalEntity(SessionEntityWrapper localEntityWrapper) { + throw new IllegalStateException("Not yet implemented"); + }; + } diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/UserSessionEntity.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/UserSessionEntity.java index 3c4746d846..5d0edb09fe 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/UserSessionEntity.java +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/UserSessionEntity.java @@ -17,18 +17,32 @@ package org.keycloak.models.sessions.infinispan.entities; +import org.infinispan.commons.marshall.Externalizer; +import org.infinispan.commons.marshall.MarshallUtil; +import org.infinispan.commons.marshall.SerializeWith; +import org.jboss.logging.Logger; import org.keycloak.models.UserSessionModel; +import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper; +import org.keycloak.models.sessions.infinispan.util.KeycloakMarshallUtil; +import java.io.IOException; +import java.io.ObjectInput; +import java.io.ObjectOutput; import java.util.Map; -import java.util.Set; +import java.util.TreeSet; import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.CopyOnWriteArraySet; /** * @author Stian Thorgersen */ +@SerializeWith(UserSessionEntity.ExternalizerImpl.class) public class UserSessionEntity extends SessionEntity { + public static final Logger logger = Logger.getLogger(UserSessionEntity.class); + + // Metadata attribute, which contains the lastSessionRefresh available on remoteCache. Used in decide whether we need to write to remoteCache (DC) or not + public static final String LAST_SESSION_REFRESH_REMOTE = "lsrr"; + private String user; private String brokerSessionId; @@ -147,4 +161,120 @@ public class UserSessionEntity extends SessionEntity { public void setBrokerUserId(String brokerUserId) { this.brokerUserId = brokerUserId; } + + @Override + public String toString() { + return String.format("UserSessionEntity [id=%s, realm=%s, lastSessionRefresh=%d, clients=%s]", getId(), getRealm(), getLastSessionRefresh(), + new TreeSet(this.authenticatedClientSessions.keySet())); + } + + @Override + public SessionEntityWrapper mergeRemoteEntityWithLocalEntity(SessionEntityWrapper localEntityWrapper) { + int lsrRemote = getLastSessionRefresh(); + + SessionEntityWrapper entityWrapper; + if (localEntityWrapper == null) { + entityWrapper = new SessionEntityWrapper<>(this); + } else { + UserSessionEntity localUserSession = (UserSessionEntity) localEntityWrapper.getEntity(); + + // local lastSessionRefresh should always contain the bigger + if (lsrRemote < localUserSession.getLastSessionRefresh()) { + setLastSessionRefresh(localUserSession.getLastSessionRefresh()); + } + + entityWrapper = new SessionEntityWrapper<>(localEntityWrapper.getLocalMetadata(), this); + } + + entityWrapper.putLocalMetadataNoteInt(LAST_SESSION_REFRESH_REMOTE, lsrRemote); + + logger.debugf("Updating session entity. lastSessionRefresh=%d, lastSessionRefreshRemote=%d", getLastSessionRefresh(), lsrRemote); + + return entityWrapper; + } + + + public static class ExternalizerImpl implements Externalizer { + + private static final int VERSION_1 = 1; + + @Override + public void writeObject(ObjectOutput output, UserSessionEntity session) throws IOException { + output.writeByte(VERSION_1); + + MarshallUtil.marshallString(session.getAuthMethod(), output); + MarshallUtil.marshallString(session.getBrokerSessionId(), output); + MarshallUtil.marshallString(session.getBrokerUserId(), output); + MarshallUtil.marshallString(session.getId(), output); + MarshallUtil.marshallString(session.getIpAddress(), output); + MarshallUtil.marshallString(session.getLoginUsername(), output); + MarshallUtil.marshallString(session.getRealm(), output); + MarshallUtil.marshallString(session.getUser(), output); + + MarshallUtil.marshallInt(output, session.getLastSessionRefresh()); + MarshallUtil.marshallInt(output, session.getStarted()); + output.writeBoolean(session.isRememberMe()); + + int state = session.getState() == null ? 0 : + ((session.getState() == UserSessionModel.State.LOGGED_IN) ? 1 : (session.getState() == UserSessionModel.State.LOGGED_OUT ? 2 : 3)); + output.writeInt(state); + + Map notes = session.getNotes(); + KeycloakMarshallUtil.writeMap(notes, KeycloakMarshallUtil.STRING_EXT, KeycloakMarshallUtil.STRING_EXT, output); + + Map authSessions = session.getAuthenticatedClientSessions(); + KeycloakMarshallUtil.writeMap(authSessions, KeycloakMarshallUtil.STRING_EXT, new AuthenticatedClientSessionEntity.ExternalizerImpl(), output); + } + + + @Override + public UserSessionEntity readObject(ObjectInput input) throws IOException, ClassNotFoundException { + switch (input.readByte()) { + case VERSION_1: + return readObjectVersion1(input); + default: + throw new IOException("Unknown version"); + } + } + + public UserSessionEntity readObjectVersion1(ObjectInput input) throws IOException, ClassNotFoundException { + UserSessionEntity sessionEntity = new UserSessionEntity(); + + sessionEntity.setAuthMethod(MarshallUtil.unmarshallString(input)); + sessionEntity.setBrokerSessionId(MarshallUtil.unmarshallString(input)); + sessionEntity.setBrokerUserId(MarshallUtil.unmarshallString(input)); + sessionEntity.setId(MarshallUtil.unmarshallString(input)); + sessionEntity.setIpAddress(MarshallUtil.unmarshallString(input)); + sessionEntity.setLoginUsername(MarshallUtil.unmarshallString(input)); + sessionEntity.setRealm(MarshallUtil.unmarshallString(input)); + sessionEntity.setUser(MarshallUtil.unmarshallString(input)); + + sessionEntity.setLastSessionRefresh(MarshallUtil.unmarshallInt(input)); + sessionEntity.setStarted(MarshallUtil.unmarshallInt(input)); + sessionEntity.setRememberMe(input.readBoolean()); + + int state = input.readInt(); + switch(state) { + case 1: sessionEntity.setState(UserSessionModel.State.LOGGED_IN); + break; + case 2: sessionEntity.setState(UserSessionModel.State.LOGGED_OUT); + break; + case 3: sessionEntity.setState(UserSessionModel.State.LOGGING_OUT); + break; + default: + sessionEntity.setState(null); + } + + Map notes = KeycloakMarshallUtil.readMap(input, KeycloakMarshallUtil.STRING_EXT, KeycloakMarshallUtil.STRING_EXT, + new KeycloakMarshallUtil.ConcurrentHashMapBuilder<>()); + sessionEntity.setNotes(notes); + + Map authSessions = KeycloakMarshallUtil.readMap(input, KeycloakMarshallUtil.STRING_EXT, new AuthenticatedClientSessionEntity.ExternalizerImpl(), + new KeycloakMarshallUtil.ConcurrentHashMapBuilder<>()); + sessionEntity.setAuthenticatedClientSessions(authSessions); + + return sessionEntity; + } + + } } diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/events/AbstractAuthSessionClusterListener.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/events/AbstractAuthSessionClusterListener.java new file mode 100644 index 0000000000..18461e0cc4 --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/events/AbstractAuthSessionClusterListener.java @@ -0,0 +1,64 @@ +/* + * Copyright 2016 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.models.sessions.infinispan.events; + +import org.jboss.logging.Logger; +import org.keycloak.cluster.ClusterEvent; +import org.keycloak.cluster.ClusterListener; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.models.sessions.infinispan.InfinispanAuthenticationSessionProvider; +import org.keycloak.models.sessions.infinispan.InfinispanAuthenticationSessionProviderFactory; +import org.keycloak.models.utils.KeycloakModelUtils; +import org.keycloak.sessions.AuthenticationSessionProvider; + +/** + * @author Marek Posolda + */ +public abstract class AbstractAuthSessionClusterListener implements ClusterListener { + + private static final Logger log = Logger.getLogger(AbstractAuthSessionClusterListener.class); + + private final KeycloakSessionFactory sessionFactory; + + public AbstractAuthSessionClusterListener(KeycloakSessionFactory sessionFactory) { + this.sessionFactory = sessionFactory; + } + + + @Override + public void eventReceived(ClusterEvent event) { + KeycloakModelUtils.runJobInTransaction(sessionFactory, (KeycloakSession session) -> { + InfinispanAuthenticationSessionProvider provider = (InfinispanAuthenticationSessionProvider) session.getProvider(AuthenticationSessionProvider.class, + InfinispanAuthenticationSessionProviderFactory.PROVIDER_ID); + SE sessionEvent = (SE) event; + + if (!provider.getCache().getStatus().allowInvocations()) { + log.debugf("Cache in state '%s' doesn't allow invocations", provider.getCache().getStatus()); + return; + } + + log.debugf("Received authentication session event '%s'", sessionEvent.toString()); + + eventReceived(session, provider, sessionEvent); + + }); + } + + protected abstract void eventReceived(KeycloakSession session, InfinispanAuthenticationSessionProvider provider, SE sessionEvent); +} diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/events/AbstractUserSessionClusterListener.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/events/AbstractUserSessionClusterListener.java new file mode 100644 index 0000000000..70d94f0e03 --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/events/AbstractUserSessionClusterListener.java @@ -0,0 +1,82 @@ +/* + * 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.models.sessions.infinispan.events; + +import org.jboss.logging.Logger; +import org.keycloak.cluster.ClusterEvent; +import org.keycloak.cluster.ClusterListener; +import org.keycloak.cluster.ClusterProvider; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserSessionProvider; +import org.keycloak.models.sessions.infinispan.InfinispanUserSessionProvider; +import org.keycloak.models.sessions.infinispan.InfinispanUserSessionProviderFactory; +import org.keycloak.models.sessions.infinispan.util.InfinispanUtil; +import org.keycloak.models.utils.KeycloakModelUtils; + +/** + * @author Marek Posolda + */ +public abstract class AbstractUserSessionClusterListener implements ClusterListener { + + private static final Logger log = Logger.getLogger(AbstractUserSessionClusterListener.class); + + private final KeycloakSessionFactory sessionFactory; + + public AbstractUserSessionClusterListener(KeycloakSessionFactory sessionFactory) { + this.sessionFactory = sessionFactory; + } + + + @Override + public void eventReceived(ClusterEvent event) { + KeycloakModelUtils.runJobInTransaction(sessionFactory, (KeycloakSession session) -> { + InfinispanUserSessionProvider provider = (InfinispanUserSessionProvider) session.getProvider(UserSessionProvider.class, InfinispanUserSessionProviderFactory.PROVIDER_ID); + SE sessionEvent = (SE) event; + + boolean shouldResendEvent = shouldResendEvent(session, sessionEvent); + + if (log.isDebugEnabled()) { + log.debugf("Received user session event '%s'. Should resend event: %b", sessionEvent.toString(), shouldResendEvent); + } + + eventReceived(session, provider, sessionEvent); + + if (shouldResendEvent) { + session.getProvider(ClusterProvider.class).notify(sessionEvent.getEventKey(), event, true, ClusterProvider.DCNotify.ALL_BUT_LOCAL_DC); + } + + }); + } + + protected abstract void eventReceived(KeycloakSession session, InfinispanUserSessionProvider provider, SE sessionEvent); + + + private boolean shouldResendEvent(KeycloakSession session, SessionClusterEvent event) { + if (!event.isResendingEvent()) { + return false; + } + + // Just the initiator will re-send the event after receiving it + String myNode = InfinispanUtil.getMyAddress(session); + String mySite = InfinispanUtil.getMySite(session); + return (event.getNodeId() != null && event.getNodeId().equals(myNode) && event.getSiteId() != null && event.getSiteId().equals(mySite)); + } + +} diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/events/ClientRemovedSessionEvent.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/events/ClientRemovedSessionEvent.java new file mode 100644 index 0000000000..8dbad8cc35 --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/events/ClientRemovedSessionEvent.java @@ -0,0 +1,43 @@ +/* + * 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.models.sessions.infinispan.events; + +import org.keycloak.models.KeycloakSession; + +/** + * @author Marek Posolda + */ +public class ClientRemovedSessionEvent extends SessionClusterEvent { + + private String clientUuid; + + public static ClientRemovedSessionEvent create(KeycloakSession session, String eventKey, String realmId, boolean resendingEvent, String clientUuid) { + ClientRemovedSessionEvent event = ClientRemovedSessionEvent.createEvent(ClientRemovedSessionEvent.class, eventKey, session, realmId, resendingEvent); + event.clientUuid = clientUuid; + return event; + } + + @Override + public String toString() { + return String.format("ClientRemovedSessionEvent [ realmId=%s , clientUuid=%s ]", getRealmId(), clientUuid); + } + + public String getClientUuid() { + return clientUuid; + } +} diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/mapreduce/FirstResultReducer.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/events/RealmRemovedSessionEvent.java similarity index 61% rename from model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/mapreduce/FirstResultReducer.java rename to model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/events/RealmRemovedSessionEvent.java index 0e62689462..76d6aaf920 100644 --- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/mapreduce/FirstResultReducer.java +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/events/RealmRemovedSessionEvent.java @@ -15,21 +15,10 @@ * limitations under the License. */ -package org.keycloak.models.sessions.infinispan.mapreduce; - -import org.infinispan.distexec.mapreduce.Reducer; - -import java.io.Serializable; -import java.util.Iterator; +package org.keycloak.models.sessions.infinispan.events; /** - * @author Stian Thorgersen + * @author Marek Posolda */ -public class FirstResultReducer implements Reducer, Serializable { - - @Override - public Object reduce(Object reducedKey, Iterator itr) { - return itr.next(); - } - +public class RealmRemovedSessionEvent extends SessionClusterEvent { } diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/events/RemoveAllUserLoginFailuresEvent.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/events/RemoveAllUserLoginFailuresEvent.java new file mode 100644 index 0000000000..7b0f3b7432 --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/events/RemoveAllUserLoginFailuresEvent.java @@ -0,0 +1,24 @@ +/* + * 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.models.sessions.infinispan.events; + +/** + * @author Marek Posolda + */ +public class RemoveAllUserLoginFailuresEvent extends SessionClusterEvent { +} diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/events/RemoveUserSessionsEvent.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/events/RemoveUserSessionsEvent.java new file mode 100644 index 0000000000..968ff8619c --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/events/RemoveUserSessionsEvent.java @@ -0,0 +1,24 @@ +/* + * 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.models.sessions.infinispan.events; + +/** + * @author Marek Posolda + */ +public class RemoveUserSessionsEvent extends SessionClusterEvent { +} diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/events/SessionClusterEvent.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/events/SessionClusterEvent.java new file mode 100644 index 0000000000..118eb53526 --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/events/SessionClusterEvent.java @@ -0,0 +1,81 @@ +/* + * 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.models.sessions.infinispan.events; + +import org.keycloak.cluster.ClusterEvent; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.sessions.infinispan.util.InfinispanUtil; + +/** + * @author Marek Posolda + */ +public abstract class SessionClusterEvent implements ClusterEvent { + + private String realmId; + private String eventKey; + private boolean resendingEvent; + private String siteId; + private String nodeId; + + + public static T createEvent(Class eventClass, String eventKey, KeycloakSession session, String realmId, boolean resendingEvent) { + try { + T event = eventClass.newInstance(); + event.setData(session, eventKey, realmId, resendingEvent); + return event; + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + + void setData(KeycloakSession session, String eventKey, String realmId, boolean resendingEvent) { + this.realmId = realmId; + this.eventKey = eventKey; + this.resendingEvent = resendingEvent; + this.siteId = InfinispanUtil.getMySite(session); + this.nodeId = InfinispanUtil.getMyAddress(session); + } + + + public String getRealmId() { + return realmId; + } + + public String getEventKey() { + return eventKey; + } + + public boolean isResendingEvent() { + return resendingEvent; + } + + public String getSiteId() { + return siteId; + } + + public String getNodeId() { + return nodeId; + } + + @Override + public String toString() { + String simpleClassName = getClass().getSimpleName(); + return String.format("%s [ realmId=%s ]", simpleClassName, realmId); + } +} diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/events/SessionEventsSenderTransaction.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/events/SessionEventsSenderTransaction.java new file mode 100644 index 0000000000..f98bbe6518 --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/events/SessionEventsSenderTransaction.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.models.sessions.infinispan.events; + +import java.util.LinkedList; +import java.util.List; + +import org.keycloak.cluster.ClusterProvider; +import org.keycloak.models.AbstractKeycloakTransaction; +import org.keycloak.models.KeycloakSession; + +/** + * Postpone sending notifications of session events to the commit of Keycloak transaction + * + * @author Marek Posolda + */ +public class SessionEventsSenderTransaction extends AbstractKeycloakTransaction { + + private final KeycloakSession session; + + private final List sessionEvents = new LinkedList<>(); + + public SessionEventsSenderTransaction(KeycloakSession session) { + this.session = session; + } + + public void addEvent(SessionClusterEvent event, ClusterProvider.DCNotify dcNotify) { + sessionEvents.add(new DCEventContext(dcNotify, event)); + } + + + @Override + protected void commitImpl() { + ClusterProvider cluster = session.getProvider(ClusterProvider.class); + + // TODO bulk notify (send whole list instead of separate events?) + for (DCEventContext entry : sessionEvents) { + cluster.notify(entry.event.getEventKey(), entry.event, false, entry.dcNotify); + } + } + + + @Override + protected void rollbackImpl() { + + } + + + private class DCEventContext { + private final ClusterProvider.DCNotify dcNotify; + private final SessionClusterEvent event; + + DCEventContext(ClusterProvider.DCNotify dcNotify, SessionClusterEvent event) { + this.dcNotify = dcNotify; + this.event = event; + } + } +} diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/BaseCacheInitializer.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/BaseCacheInitializer.java new file mode 100644 index 0000000000..43788d07fb --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/BaseCacheInitializer.java @@ -0,0 +1,159 @@ +/* + * 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.models.sessions.infinispan.initializer; + +import java.io.Serializable; + +import org.infinispan.Cache; +import org.infinispan.context.Flag; +import org.infinispan.lifecycle.ComponentStatus; +import org.infinispan.remoting.transport.Transport; +import org.jboss.logging.Logger; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.models.KeycloakSessionTask; +import org.keycloak.models.utils.KeycloakModelUtils; + +/** + * @author Marek Posolda + */ +public abstract class BaseCacheInitializer extends CacheInitializer { + + private static final String STATE_KEY_PREFIX = "distributed::"; + + private static final Logger log = Logger.getLogger(BaseCacheInitializer.class); + + protected final KeycloakSessionFactory sessionFactory; + protected final Cache workCache; + protected final SessionLoader sessionLoader; + protected final int sessionsPerSegment; + protected final String stateKey; + + public BaseCacheInitializer(KeycloakSessionFactory sessionFactory, Cache workCache, SessionLoader sessionLoader, String stateKeySuffix, int sessionsPerSegment) { + this.sessionFactory = sessionFactory; + this.workCache = workCache; + this.sessionLoader = sessionLoader; + this.sessionsPerSegment = sessionsPerSegment; + this.stateKey = STATE_KEY_PREFIX + stateKeySuffix; + } + + + @Override + protected boolean isFinished() { + // Check if we should skipLoadingSessions. This can happen if someone else already did the task (For example in cross-dc environment, it was done by different DC) + boolean isFinishedAlready = this.sessionLoader.isFinished(this); + if (isFinishedAlready) { + return true; + } + + InitializerState state = getStateFromCache(); + return state != null && state.isFinished(); + } + + + @Override + protected boolean isCoordinator() { + Transport transport = workCache.getCacheManager().getTransport(); + return transport == null || transport.isCoordinator(); + } + + + protected InitializerState getOrCreateInitializerState() { + InitializerState state = getStateFromCache(); + if (state == null) { + final int[] count = new int[1]; + + // Rather use separate transactions for update and counting + + KeycloakModelUtils.runJobInTransaction(sessionFactory, new KeycloakSessionTask() { + @Override + public void run(KeycloakSession session) { + sessionLoader.init(session); + } + + }); + + KeycloakModelUtils.runJobInTransaction(sessionFactory, new KeycloakSessionTask() { + @Override + public void run(KeycloakSession session) { + count[0] = sessionLoader.getSessionsCount(session); + } + + }); + + state = new InitializerState(); + state.init(count[0], sessionsPerSegment); + saveStateToCache(state); + } + return state; + + } + + + private InitializerState getStateFromCache() { + // We ignore cacheStore for now, so that in Cross-DC scenario (with RemoteStore enabled) is the remoteStore ignored. This means that every DC needs to load offline sessions separately. + return (InitializerState) workCache.getAdvancedCache() + .withFlags(Flag.SKIP_CACHE_STORE, Flag.SKIP_CACHE_LOAD) + .get(stateKey); + } + + + protected void saveStateToCache(final InitializerState state) { + + // 3 attempts to send the message (it may fail if some node fails in the meantime) + retry(3, new Runnable() { + + @Override + public void run() { + + // Save this synchronously to ensure all nodes read correct state + // We ignore cacheStore for now, so that in Cross-DC scenario (with RemoteStore enabled) is the remoteStore ignored. This means that every DC needs to load offline sessions separately. + BaseCacheInitializer.this.workCache.getAdvancedCache(). + withFlags(Flag.IGNORE_RETURN_VALUES, Flag.FORCE_SYNCHRONOUS, Flag.SKIP_CACHE_STORE, Flag.SKIP_CACHE_LOAD) + .put(stateKey, state); + } + + }); + } + + + private void retry(int retry, Runnable runnable) { + while (true) { + try { + runnable.run(); + return; + } catch (RuntimeException e) { + ComponentStatus status = workCache.getStatus(); + if (status.isStopping() || status.isTerminated()) { + log.warn("Failed to put initializerState to the cache. Cache is already terminating"); + log.debug(e.getMessage(), e); + return; + } + retry--; + if (retry == 0) { + throw e; + } + } + } + } + + + public Cache getWorkCache() { + return workCache; + } +} diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/CacheInitializer.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/CacheInitializer.java new file mode 100644 index 0000000000..1932709c72 --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/CacheInitializer.java @@ -0,0 +1,55 @@ +/* + * Copyright 2016 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.models.sessions.infinispan.initializer; + +import org.jboss.logging.Logger; + +/** + * @author Marek Posolda + */ +public abstract class CacheInitializer { + + private static final Logger log = Logger.getLogger(CacheInitializer.class); + + public void initCache() { + } + + public void loadSessions() { + while (!isFinished()) { + if (!isCoordinator()) { + try { + Thread.sleep(1000); + } catch (InterruptedException ie) { + log.error("Interrupted", ie); + } + } else { + startLoading(); + } + } + } + + + protected abstract boolean isFinished(); + + protected abstract boolean isCoordinator(); + + /** + * Just coordinator will run this + */ + protected abstract void startLoading(); +} diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/DBLockBasedCacheInitializer.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/DBLockBasedCacheInitializer.java new file mode 100644 index 0000000000..ecc8c7833e --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/DBLockBasedCacheInitializer.java @@ -0,0 +1,81 @@ +/* + * 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.models.sessions.infinispan.initializer; + +import org.jboss.logging.Logger; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.dblock.DBLockManager; +import org.keycloak.models.dblock.DBLockProvider; + +/** + * Encapsulates preloading of sessions within the DB Lock. This DB-aware lock ensures that "startLoading" is done on single DC and the other DCs need to wait. + * + * @author Marek Posolda + */ +public class DBLockBasedCacheInitializer extends CacheInitializer { + + private static final Logger log = Logger.getLogger(DBLockBasedCacheInitializer.class); + + private final KeycloakSession session; + private final CacheInitializer delegate; + + public DBLockBasedCacheInitializer(KeycloakSession session, CacheInitializer delegate) { + this.session = session; + this.delegate = delegate; + } + + + @Override + public void initCache() { + delegate.initCache(); + } + + + @Override + protected boolean isFinished() { + return delegate.isFinished(); + } + + + @Override + protected boolean isCoordinator() { + return delegate.isCoordinator(); + } + + + /** + * Just coordinator will run this. And there is DB-lock, so the delegate.startLoading() will be permitted just by the single DC + */ + @Override + protected void startLoading() { + DBLockManager dbLockManager = new DBLockManager(session); + dbLockManager.checkForcedUnlock(); + DBLockProvider dbLock = dbLockManager.getDBLock(); + dbLock.waitForLock(); + try { + + if (isFinished()) { + log.infof("Task already finished when DBLock retrieved"); + } else { + delegate.startLoading(); + } + } finally { + dbLock.releaseLock(); + } + } +} diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/InfinispanUserSessionInitializer.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/InfinispanCacheInitializer.java similarity index 54% rename from model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/InfinispanUserSessionInitializer.java rename to model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/InfinispanCacheInitializer.java index c332eea8fc..620a9a8178 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/InfinispanUserSessionInitializer.java +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/InfinispanCacheInitializer.java @@ -18,15 +18,10 @@ package org.keycloak.models.sessions.infinispan.initializer; import org.infinispan.Cache; -import org.infinispan.context.Flag; import org.infinispan.distexec.DefaultExecutorService; -import org.infinispan.lifecycle.ComponentStatus; import org.infinispan.remoting.transport.Transport; import org.jboss.logging.Logger; -import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; -import org.keycloak.models.KeycloakSessionTask; -import org.keycloak.models.utils.KeycloakModelUtils; import java.io.Serializable; import java.util.LinkedList; @@ -37,131 +32,34 @@ import java.util.concurrent.Executors; import java.util.concurrent.Future; /** - * Startup initialization for reading persistent userSessions/clientSessions to be filled into infinispan/memory . In cluster, + * Startup initialization for reading persistent userSessions to be filled into infinispan/memory . In cluster, * the initialization is distributed among all cluster nodes, so the startup time is even faster * * TODO: Move to clusterService. Implementation is already pretty generic and doesn't contain any "userSession" specific stuff. All sessions-specific logic is in the SessionLoader implementation * * @author Marek Posolda */ -public class InfinispanUserSessionInitializer { +public class InfinispanCacheInitializer extends BaseCacheInitializer { - private static final String STATE_KEY_PREFIX = "distributed::"; + private static final Logger log = Logger.getLogger(InfinispanCacheInitializer.class); - private static final Logger log = Logger.getLogger(InfinispanUserSessionInitializer.class); - - private final KeycloakSessionFactory sessionFactory; - private final Cache workCache; - private final SessionLoader sessionLoader; private final int maxErrors; - private final int sessionsPerSegment; - private final String stateKey; - public InfinispanUserSessionInitializer(KeycloakSessionFactory sessionFactory, Cache workCache, SessionLoader sessionLoader, int maxErrors, int sessionsPerSegment, String stateKeySuffix) { - this.sessionFactory = sessionFactory; - this.workCache = workCache; - this.sessionLoader = sessionLoader; + public InfinispanCacheInitializer(KeycloakSessionFactory sessionFactory, Cache workCache, SessionLoader sessionLoader, String stateKeySuffix, int sessionsPerSegment, int maxErrors) { + super(sessionFactory, workCache, sessionLoader, stateKeySuffix, sessionsPerSegment); this.maxErrors = maxErrors; - this.sessionsPerSegment = sessionsPerSegment; - this.stateKey = STATE_KEY_PREFIX + stateKeySuffix; } + @Override public void initCache() { this.workCache.getAdvancedCache().getComponentRegistry().registerComponent(sessionFactory, KeycloakSessionFactory.class); } - public void loadPersistentSessions() { - if (isFinished()) { - return; - } - - while (!isFinished()) { - if (!isCoordinator()) { - try { - Thread.sleep(1000); - } catch (InterruptedException ie) { - log.error("Interrupted", ie); - } - } else { - startLoading(); - } - } - } - - - private boolean isFinished() { - InitializerState state = getStateFromCache(); - return state != null && state.isFinished(); - } - - - private InitializerState getOrCreateInitializerState() { - InitializerState state = getStateFromCache(); - if (state == null) { - final int[] count = new int[1]; - - // Rather use separate transactions for update and counting - - KeycloakModelUtils.runJobInTransaction(sessionFactory, new KeycloakSessionTask() { - @Override - public void run(KeycloakSession session) { - sessionLoader.init(session); - } - - }); - - KeycloakModelUtils.runJobInTransaction(sessionFactory, new KeycloakSessionTask() { - @Override - public void run(KeycloakSession session) { - count[0] = sessionLoader.getSessionsCount(session); - } - - }); - - state = new InitializerState(); - state.init(count[0], sessionsPerSegment); - saveStateToCache(state); - } - return state; - - } - - private InitializerState getStateFromCache() { - // TODO: We ignore cacheStore for now, so that in Cross-DC scenario (with RemoteStore enabled) is the remoteStore ignored. This means that every DC needs to load offline sessions separately. - return (InitializerState) workCache.getAdvancedCache() - .withFlags(Flag.SKIP_CACHE_STORE, Flag.SKIP_CACHE_LOAD) - .get(stateKey); - } - - private void saveStateToCache(final InitializerState state) { - - // 3 attempts to send the message (it may fail if some node fails in the meantime) - retry(3, new Runnable() { - - @Override - public void run() { - - // Save this synchronously to ensure all nodes read correct state - // TODO: We ignore cacheStore for now, so that in Cross-DC scenario (with RemoteStore enabled) is the remoteStore ignored. This means that every DC needs to load offline sessions separately. - InfinispanUserSessionInitializer.this.workCache.getAdvancedCache(). - withFlags(Flag.IGNORE_RETURN_VALUES, Flag.FORCE_SYNCHRONOUS, Flag.SKIP_CACHE_STORE, Flag.SKIP_CACHE_LOAD) - .put(stateKey, state); - } - - }); - } - - - private boolean isCoordinator() { - Transport transport = workCache.getCacheManager().getTransport(); - return transport == null || transport.isCoordinator(); - } - - // Just coordinator will run this - private void startLoading() { + @Override + protected void startLoading() { InitializerState state = getOrCreateInitializerState(); // Assume each worker has same processor's count @@ -230,6 +128,10 @@ public class InfinispanUserSessionInitializer { log.debug("New initializer state pushed. The state is: " + state.printState()); } } + + // Loader callback after the task is finished + this.sessionLoader.afterAllSessionsLoaded(this); + } finally { if (distributed) { executorService.shutdown(); @@ -238,25 +140,6 @@ public class InfinispanUserSessionInitializer { } } - private void retry(int retry, Runnable runnable) { - while (true) { - try { - runnable.run(); - return; - } catch (RuntimeException e) { - ComponentStatus status = workCache.getStatus(); - if (status.isStopping() || status.isTerminated()) { - log.warn("Failed to put initializerState to the cache. Cache is already terminating"); - log.debug(e.getMessage(), e); - return; - } - retry--; - if (retry == 0) { - throw e; - } - } - } - } public static class WorkerResult implements Serializable { diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/OfflineUserSessionLoader.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/OfflinePersistentUserSessionLoader.java similarity index 60% rename from model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/OfflineUserSessionLoader.java rename to model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/OfflinePersistentUserSessionLoader.java index 2b6fb71752..5379e4068b 100644 --- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/OfflineUserSessionLoader.java +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/OfflinePersistentUserSessionLoader.java @@ -17,20 +17,30 @@ package org.keycloak.models.sessions.infinispan.initializer; +import org.infinispan.Cache; +import org.infinispan.context.Flag; import org.jboss.logging.Logger; import org.keycloak.cluster.ClusterProvider; import org.keycloak.models.KeycloakSession; import org.keycloak.models.UserSessionModel; import org.keycloak.models.session.UserSessionPersisterProvider; +import java.io.Serializable; import java.util.List; /** * @author Marek Posolda */ -public class OfflineUserSessionLoader implements SessionLoader { +public class OfflinePersistentUserSessionLoader implements SessionLoader, Serializable { + + private static final Logger log = Logger.getLogger(OfflinePersistentUserSessionLoader.class); + + // Cross-DC aware flag + public static final String PERSISTENT_SESSIONS_LOADED = "PERSISTENT_SESSIONS_LOADED"; + + // Just local-DC aware flag + public static final String PERSISTENT_SESSIONS_LOADED_IN_CURRENT_DC = "PERSISTENT_SESSIONS_LOADED_IN_CURRENT_DC"; - private static final Logger log = Logger.getLogger(OfflineUserSessionLoader.class); @Override public void init(KeycloakSession session) { @@ -45,12 +55,14 @@ public class OfflineUserSessionLoader implements SessionLoader { persister.updateAllTimestamps(clusterStartupTime); } + @Override public int getSessionsCount(KeycloakSession session) { UserSessionPersisterProvider persister = session.getProvider(UserSessionPersisterProvider.class); return persister.getUserSessionsCount(true); } + @Override public boolean loadSessions(KeycloakSession session, int first, int max) { if (log.isTraceEnabled()) { @@ -70,4 +82,37 @@ public class OfflineUserSessionLoader implements SessionLoader { } + @Override + public boolean isFinished(BaseCacheInitializer initializer) { + Cache workCache = initializer.getWorkCache(); + Boolean sessionsLoaded = (Boolean) workCache.get(PERSISTENT_SESSIONS_LOADED); + + if (sessionsLoaded != null && sessionsLoaded) { + log.debugf("Persistent sessions loaded already."); + return true; + } else { + log.debugf("Persistent sessions not yet loaded."); + return false; + } + } + + + @Override + public void afterAllSessionsLoaded(BaseCacheInitializer initializer) { + Cache workCache = initializer.getWorkCache(); + + // Cross-DC aware flag + workCache + .getAdvancedCache().withFlags(Flag.SKIP_REMOTE_LOOKUP) + .put(PERSISTENT_SESSIONS_LOADED, true); + + // Just local-DC aware flag + workCache + .getAdvancedCache().withFlags(Flag.SKIP_REMOTE_LOOKUP, Flag.SKIP_CACHE_LOAD, Flag.SKIP_CACHE_STORE) + .put(PERSISTENT_SESSIONS_LOADED_IN_CURRENT_DC, true); + + + log.debugf("Persistent sessions loaded successfully!"); + } + } diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/SessionInitializerWorker.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/SessionInitializerWorker.java index 4b04d9b752..ec647afdbe 100644 --- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/SessionInitializerWorker.java +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/SessionInitializerWorker.java @@ -31,7 +31,7 @@ import java.util.Set; /** * @author Marek Posolda */ -public class SessionInitializerWorker implements DistributedCallable, Serializable { +public class SessionInitializerWorker implements DistributedCallable, Serializable { private static final Logger log = Logger.getLogger(SessionInitializerWorker.class); @@ -53,7 +53,7 @@ public class SessionInitializerWorker implements DistributedCallableMarek Posolda */ -public interface SessionLoader extends Serializable { +public interface SessionLoader { + /** + * Will be triggered just once on cluster coordinator node to perform some generic initialization tasks (Eg. update DB before starting load). + * + * NOTE: This shouldn't be used for the initialization of loader instance itself! + * + * @param session + */ void init(KeycloakSession session); + + /** + * Will be triggered just once on cluster coordinator node to count the number of sessions + * + * @param session + * @return + */ int getSessionsCount(KeycloakSession session); + + /** + * Will be called on all cluster nodes to load the specified page. + * + * @param session + * @param first + * @param max + * @return + */ boolean loadSessions(KeycloakSession session, int first, int max); + + + /** + * This will be called on nodes to check if loading is finished. It allows loader to notify that loading is finished for some reason. + * + * @param initializer + * @return + */ + boolean isFinished(BaseCacheInitializer initializer); + + + /** + * Callback triggered on cluster coordinator once it recognize that all sessions were successfully loaded + * + * @param initializer + */ + void afterAllSessionsLoaded(BaseCacheInitializer initializer); } diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/SingleWorkerCacheInitializer.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/SingleWorkerCacheInitializer.java new file mode 100644 index 0000000000..a60b4b9ac3 --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/SingleWorkerCacheInitializer.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.models.sessions.infinispan.initializer; + +import java.io.Serializable; + +import org.infinispan.Cache; +import org.keycloak.models.KeycloakSession; + +/** + * This impl is able to run the non-paginatable loader task and hence will be executed just on single node. + * + * @author Marek Posolda + */ +public class SingleWorkerCacheInitializer extends BaseCacheInitializer { + + private final KeycloakSession session; + + public SingleWorkerCacheInitializer(KeycloakSession session, Cache workCache, SessionLoader sessionLoader, String stateKeySuffix) { + super(session.getKeycloakSessionFactory(), workCache, sessionLoader, stateKeySuffix, Integer.MAX_VALUE); + this.session = session; + } + + @Override + protected void startLoading() { + InitializerState state = getOrCreateInitializerState(); + while (!state.isFinished()) { + sessionLoader.loadSessions(session, -1, -1); + state.markSegmentFinished(0); + saveStateToCache(state); + } + + // Loader callback after the task is finished + this.sessionLoader.afterAllSessionsLoaded(this); + } +} diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/mapreduce/SessionMapper.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/mapreduce/SessionMapper.java deleted file mode 100755 index fb7c4cfac5..0000000000 --- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/mapreduce/SessionMapper.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright 2016 Red Hat, Inc. and/or its affiliates - * and other contributors as indicated by the @author tags. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.keycloak.models.sessions.infinispan.mapreduce; - -import org.infinispan.distexec.mapreduce.Collector; -import org.infinispan.distexec.mapreduce.Mapper; -import org.keycloak.models.sessions.infinispan.entities.SessionEntity; - -import java.io.Serializable; - -/** - * @author Stian Thorgersen - */ -public class SessionMapper implements Mapper, Serializable { - - public SessionMapper(String realm) { - this.realm = realm; - } - - private enum EmitValue { - KEY, ENTITY - } - - private String realm; - - private EmitValue emit = EmitValue.ENTITY; - - public static SessionMapper create(String realm) { - return new SessionMapper(realm); - } - - public SessionMapper emitKey() { - emit = EmitValue.KEY; - return this; - } - - @Override - public void map(String key, SessionEntity e, Collector collector) { - if (!realm.equals(e.getRealm())) { - return; - } - - switch (emit) { - case KEY: - collector.emit(key, key); - break; - case ENTITY: - collector.emit(key, e); - break; - } - } - -} diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/mapreduce/UserLoginFailureMapper.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/mapreduce/UserLoginFailureMapper.java deleted file mode 100755 index 5b81f8e65f..0000000000 --- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/mapreduce/UserLoginFailureMapper.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright 2016 Red Hat, Inc. and/or its affiliates - * and other contributors as indicated by the @author tags. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.keycloak.models.sessions.infinispan.mapreduce; - -import org.infinispan.distexec.mapreduce.Collector; -import org.infinispan.distexec.mapreduce.Mapper; -import org.keycloak.models.sessions.infinispan.entities.LoginFailureEntity; -import org.keycloak.models.sessions.infinispan.entities.LoginFailureKey; - -import java.io.Serializable; - -/** - * @author Stian Thorgersen - */ -public class UserLoginFailureMapper implements Mapper, Serializable { - - public UserLoginFailureMapper(String realm) { - this.realm = realm; - } - - private enum EmitValue { - KEY, ENTITY - } - - private String realm; - - private EmitValue emit = EmitValue.ENTITY; - - public static UserLoginFailureMapper create(String realm) { - return new UserLoginFailureMapper(realm); - } - - public UserLoginFailureMapper emitKey() { - emit = EmitValue.KEY; - return this; - } - - @Override - public void map(LoginFailureKey key, LoginFailureEntity e, Collector collector) { - if (!realm.equals(e.getRealm())) { - return; - } - - switch (emit) { - case KEY: - collector.emit(key, key); - break; - case ENTITY: - collector.emit(key, e); - break; - } - } - -} diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/mapreduce/UserSessionMapper.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/mapreduce/UserSessionMapper.java deleted file mode 100755 index 437b92f4e5..0000000000 --- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/mapreduce/UserSessionMapper.java +++ /dev/null @@ -1,120 +0,0 @@ -/* - * Copyright 2016 Red Hat, Inc. and/or its affiliates - * and other contributors as indicated by the @author tags. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.keycloak.models.sessions.infinispan.mapreduce; - -import org.infinispan.distexec.mapreduce.Collector; -import org.infinispan.distexec.mapreduce.Mapper; -import org.keycloak.models.sessions.infinispan.entities.SessionEntity; -import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity; - -import java.io.Serializable; - -/** - * @author Stian Thorgersen - */ -public class UserSessionMapper implements Mapper, Serializable { - - public UserSessionMapper(String realm) { - this.realm = realm; - } - - private enum EmitValue { - KEY, ENTITY - } - - private String realm; - - private EmitValue emit = EmitValue.ENTITY; - - private String user; - - private Integer expired; - - private Integer expiredRefresh; - - private String brokerSessionId; - private String brokerUserId; - - public static UserSessionMapper create(String realm) { - return new UserSessionMapper(realm); - } - - public UserSessionMapper emitKey() { - emit = EmitValue.KEY; - return this; - } - - public UserSessionMapper user(String user) { - this.user = user; - return this; - } - - public UserSessionMapper expired(Integer expired, Integer expiredRefresh) { - this.expired = expired; - this.expiredRefresh = expiredRefresh; - return this; - } - - public UserSessionMapper brokerSessionId(String id) { - this.brokerSessionId = id; - return this; - } - - public UserSessionMapper brokerUserId(String id) { - this.brokerUserId = id; - return this; - } - - @Override - public void map(String key, SessionEntity e, Collector collector) { - if (!(e instanceof UserSessionEntity)) { - return; - } - - UserSessionEntity entity = (UserSessionEntity) e; - - if (!realm.equals(entity.getRealm())) { - return; - } - - if (user != null && !entity.getUser().equals(user)) { - return; - } - - if (brokerSessionId != null && !brokerSessionId.equals(entity.getBrokerSessionId())) return; - if (brokerUserId != null && !brokerUserId.equals(entity.getBrokerUserId())) return; - - if (expired != null && expiredRefresh != null && entity.getStarted() > expired && entity.getLastSessionRefresh() > expiredRefresh) { - return; - } - - if (expired == null && expiredRefresh != null && entity.getLastSessionRefresh() > expiredRefresh) { - return; - } - - switch (emit) { - case KEY: - collector.emit(key, key); - break; - case ENTITY: - collector.emit(key, entity); - break; - } - } - -} diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/mapreduce/UserSessionNoteMapper.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/mapreduce/UserSessionNoteMapper.java deleted file mode 100755 index 4afadf92dc..0000000000 --- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/mapreduce/UserSessionNoteMapper.java +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright 2016 Red Hat, Inc. and/or its affiliates - * and other contributors as indicated by the @author tags. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.keycloak.models.sessions.infinispan.mapreduce; - -import org.infinispan.distexec.mapreduce.Collector; -import org.infinispan.distexec.mapreduce.Mapper; -import org.keycloak.models.sessions.infinispan.entities.SessionEntity; -import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity; - -import java.io.Serializable; -import java.util.Map; - -/** - * @author Stian Thorgersen - */ -public class UserSessionNoteMapper implements Mapper, Serializable { - - public UserSessionNoteMapper(String realm) { - this.realm = realm; - } - - private enum EmitValue { - KEY, ENTITY - } - - private String realm; - - private EmitValue emit = EmitValue.ENTITY; - private Map notes; - - public static UserSessionNoteMapper create(String realm) { - return new UserSessionNoteMapper(realm); - } - - public UserSessionNoteMapper emitKey() { - emit = EmitValue.KEY; - return this; - } - - public UserSessionNoteMapper notes(Map notes) { - this.notes = notes; - return this; - } - - @Override - public void map(String key, SessionEntity e, Collector collector) { - if (!(e instanceof UserSessionEntity)) { - return; - } - - UserSessionEntity entity = (UserSessionEntity) e; - - if (!realm.equals(entity.getRealm())) { - return; - } - - for (Map.Entry entry : notes.entrySet()) { - String note = entity.getNotes().get(entry.getKey()); - if (note == null) return; - if (!note.equals(entry.getValue())) return; - } - - switch (emit) { - case KEY: - collector.emit(key, key); - break; - case ENTITY: - collector.emit(key, entity); - break; - } - } - -} diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/remotestore/KcRemoteStore.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/remotestore/KcRemoteStore.java new file mode 100644 index 0000000000..ba413ae124 --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/remotestore/KcRemoteStore.java @@ -0,0 +1,105 @@ +/* + * 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.models.sessions.infinispan.remotestore; + +import java.util.concurrent.Executor; + +import org.infinispan.client.hotrod.Flag; +import org.infinispan.commons.configuration.ConfiguredBy; +import org.infinispan.filter.KeyFilter; +import org.infinispan.marshall.core.MarshalledEntry; +import org.infinispan.metadata.InternalMetadata; +import org.infinispan.persistence.remote.RemoteStore; +import org.infinispan.persistence.spi.PersistenceException; +import org.jboss.logging.Logger; +import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper; +import org.keycloak.models.sessions.infinispan.entities.SessionEntity; + +/** + * @author Marek Posolda + */ +@ConfiguredBy(KcRemoteStoreConfiguration.class) +public class KcRemoteStore extends RemoteStore { + + protected static final Logger logger = Logger.getLogger(KcRemoteStore.class); + + private String cacheName; + + @Override + public void start() throws PersistenceException { + super.start(); + if (getRemoteCache() == null) { + String cacheName = getConfiguration().remoteCacheName(); + throw new IllegalStateException("Remote cache '" + cacheName + "' is not available."); + } + this.cacheName = getRemoteCache().getName(); + } + + @Override + public MarshalledEntry load(Object key) throws PersistenceException { + logger.debugf("Calling load: '%s' for remote cache '%s'", key, cacheName); + + MarshalledEntry entry = super.load(key); + if (entry == null) { + return null; + } + + // wrap remote entity + SessionEntity entity = (SessionEntity) entry.getValue(); + SessionEntityWrapper entityWrapper = new SessionEntityWrapper(entity); + + MarshalledEntry wrappedEntry = marshalledEntry(entry.getKey(), entityWrapper, entry.getMetadata()); + + logger.debugf("Found entry in load: %s", wrappedEntry.toString()); + + return wrappedEntry; + } + + + // Don't do anything. Iterate over remoteCache.keySet() can have big performance impact. We handle bulk load by ourselves if needed. + @Override + public void process(KeyFilter filter, CacheLoaderTask task, Executor executor, boolean fetchValue, boolean fetchMetadata) { + logger.debugf("Skip calling process with filter '%s' on cache '%s'", filter, cacheName); + // super.process(filter, task, executor, fetchValue, fetchMetadata); + } + + + // Don't do anything. Writes handled by KC itself as we need more flexibility + @Override + public void write(MarshalledEntry entry) throws PersistenceException { + } + + + @Override + public boolean delete(Object key) throws PersistenceException { + logger.debugf("Calling delete for key '%s' on cache '%s'", key, cacheName); + + // Optimization - we don't need to know the previous value. + // TODO: For some usecases (bulk removal of user sessions), it may be better for performance to call removeAsync and wait for all futures to be finished + getRemoteCache().remove(key); + + return true; + } + + protected MarshalledEntry marshalledEntry(Object key, Object value, InternalMetadata metadata) { + return ctx.getMarshalledEntryFactory().newMarshalledEntry(key, value, metadata); + } + + + +} diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/remotestore/KcRemoteStoreConfiguration.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/remotestore/KcRemoteStoreConfiguration.java new file mode 100644 index 0000000000..d786872e24 --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/remotestore/KcRemoteStoreConfiguration.java @@ -0,0 +1,40 @@ +/* + * 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.models.sessions.infinispan.remotestore; + +import org.infinispan.commons.configuration.BuiltBy; +import org.infinispan.commons.configuration.ConfigurationFor; +import org.infinispan.commons.configuration.attributes.AttributeSet; +import org.infinispan.configuration.cache.AsyncStoreConfiguration; +import org.infinispan.configuration.cache.SingletonStoreConfiguration; +import org.infinispan.persistence.remote.configuration.ConnectionPoolConfiguration; +import org.infinispan.persistence.remote.configuration.ExecutorFactoryConfiguration; +import org.infinispan.persistence.remote.configuration.RemoteStoreConfiguration; + +/** + * @author Marek Posolda + */ +@BuiltBy(KcRemoteStoreConfigurationBuilder.class) +@ConfigurationFor(KcRemoteStore.class) +public class KcRemoteStoreConfiguration extends RemoteStoreConfiguration { + + public KcRemoteStoreConfiguration(AttributeSet attributes, AsyncStoreConfiguration async, SingletonStoreConfiguration singletonStore, + ExecutorFactoryConfiguration asyncExecutorFactory, ConnectionPoolConfiguration connectionPool) { + super(attributes, async, singletonStore, asyncExecutorFactory, connectionPool); + } +} diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/remotestore/KcRemoteStoreConfigurationBuilder.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/remotestore/KcRemoteStoreConfigurationBuilder.java new file mode 100644 index 0000000000..9e99967467 --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/remotestore/KcRemoteStoreConfigurationBuilder.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.models.sessions.infinispan.remotestore; + +import org.infinispan.configuration.cache.PersistenceConfigurationBuilder; +import org.infinispan.persistence.remote.configuration.RemoteStoreConfiguration; +import org.infinispan.persistence.remote.configuration.RemoteStoreConfigurationBuilder; + +/** + * @author Marek Posolda + */ +public class KcRemoteStoreConfigurationBuilder extends RemoteStoreConfigurationBuilder { + + public KcRemoteStoreConfigurationBuilder(PersistenceConfigurationBuilder builder) { + super(builder); + } + + @Override + public KcRemoteStoreConfiguration create() { + RemoteStoreConfiguration cfg = super.create(); + KcRemoteStoreConfiguration cfg2 = new KcRemoteStoreConfiguration(cfg.attributes(), cfg.async(), cfg.singletonStore(), cfg.asyncExecutorFactory(), cfg.connectionPool()); + return cfg2; + } +} diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/remotestore/RemoteCacheInvoker.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/remotestore/RemoteCacheInvoker.java new file mode 100644 index 0000000000..89fd215902 --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/remotestore/RemoteCacheInvoker.java @@ -0,0 +1,166 @@ +/* + * 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.models.sessions.infinispan.remotestore; + +import org.keycloak.common.util.Time; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +import org.infinispan.client.hotrod.Flag; +import org.infinispan.client.hotrod.RemoteCache; +import org.infinispan.client.hotrod.VersionedValue; +import org.jboss.logging.Logger; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper; +import org.keycloak.models.sessions.infinispan.changes.SessionUpdateTask; +import org.keycloak.models.sessions.infinispan.entities.SessionEntity; +import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity; + +/** + * @author Marek Posolda + */ +public class RemoteCacheInvoker { + + public static final Logger logger = Logger.getLogger(RemoteCacheInvoker.class); + + private final Map remoteCaches = new HashMap<>(); + + + public void addRemoteCache(String cacheName, RemoteCache remoteCache, MaxIdleTimeLoader maxIdleLoader) { + RemoteCacheContext ctx = new RemoteCacheContext(remoteCache, maxIdleLoader); + remoteCaches.put(cacheName, ctx); + } + + public Set getRemoteCacheNames() { + return Collections.unmodifiableSet(remoteCaches.keySet()); + } + + + public void runTask(KeycloakSession kcSession, RealmModel realm, String cacheName, String key, SessionUpdateTask task, SessionEntityWrapper sessionWrapper) { + RemoteCacheContext context = remoteCaches.get(cacheName); + if (context == null) { + return; + } + + S session = sessionWrapper.getEntity(); + + SessionUpdateTask.CacheOperation operation = task.getOperation(session); + SessionUpdateTask.CrossDCMessageStatus status = task.getCrossDCMessageStatus(sessionWrapper); + + if (status == SessionUpdateTask.CrossDCMessageStatus.NOT_NEEDED) { + logger.debugf("Skip writing to remoteCache for entity '%s' of cache '%s' and operation '%s'", key, cacheName, operation); + return; + } + + long maxIdleTimeMs = context.maxIdleTimeLoader.getMaxIdleTimeMs(realm); + + // Double the timeout to ensure that entry won't expire on remoteCache in case that write of some entities to remoteCache is postponed (eg. userSession.lastSessionRefresh) + maxIdleTimeMs = maxIdleTimeMs * 2; + + logger.debugf("Running task '%s' on remote cache '%s' . Key is '%s'", operation, cacheName, key); + + runOnRemoteCache(context.remoteCache, maxIdleTimeMs, key, task, sessionWrapper); + } + + + private void runOnRemoteCache(RemoteCache remoteCache, long maxIdleMs, String key, SessionUpdateTask task, SessionEntityWrapper sessionWrapper) { + S session = sessionWrapper.getEntity(); + SessionUpdateTask.CacheOperation operation = task.getOperation(session); + + switch (operation) { + case REMOVE: + // REMOVE already handled at remote cache store level + //remoteCache.remove(key); + break; + case ADD: + remoteCache.put(key, session, task.getLifespanMs(), TimeUnit.MILLISECONDS, maxIdleMs, TimeUnit.MILLISECONDS); + break; + case ADD_IF_ABSENT: + final int currentTime = Time.currentTime(); + SessionEntity existing = (SessionEntity) remoteCache + .withFlags(Flag.FORCE_RETURN_VALUE) + .putIfAbsent(key, session, -1, TimeUnit.MILLISECONDS, maxIdleMs, TimeUnit.MILLISECONDS); + if (existing != null) { + throw new IllegalStateException("There is already existing value in cache for key " + key); + } + sessionWrapper.putLocalMetadataNoteInt(UserSessionEntity.LAST_SESSION_REFRESH_REMOTE, currentTime); + break; + case REPLACE: + replace(remoteCache, task.getLifespanMs(), maxIdleMs, key, task); + break; + default: + throw new IllegalStateException("Unsupported state " + operation); + } + } + + + private void replace(RemoteCache remoteCache, long lifespanMs, long maxIdleMs, String key, SessionUpdateTask task) { + boolean replaced = false; + while (!replaced) { + VersionedValue versioned = remoteCache.getVersioned(key); + if (versioned == null) { + logger.warnf("Not found entity to replace for key '%s'", key); + return; + } + + S session = versioned.getValue(); + + // Run task on the remote session + task.runUpdate(session); + + logger.debugf("Before replaceWithVersion. Entity to write version %d: %s", versioned.getVersion(), session); + + replaced = remoteCache.replaceWithVersion(key, session, versioned.getVersion(), lifespanMs, TimeUnit.MILLISECONDS, maxIdleMs, TimeUnit.MILLISECONDS); + + if (!replaced) { + logger.debugf("Failed to replace entity '%s' version %d. Will retry again", key, versioned.getVersion()); + } else { + if (logger.isDebugEnabled()) { + logger.debugf("Replaced entity version %d in remote cache: %s", versioned.getVersion(), session); + } + } + } + } + + + private class RemoteCacheContext { + + private final RemoteCache remoteCache; + private final MaxIdleTimeLoader maxIdleTimeLoader; + + public RemoteCacheContext(RemoteCache remoteCache, MaxIdleTimeLoader maxIdleLoader) { + this.remoteCache = remoteCache; + this.maxIdleTimeLoader = maxIdleLoader; + } + + } + + + @FunctionalInterface + public interface MaxIdleTimeLoader { + + long getMaxIdleTimeMs(RealmModel realm); + + } + + +} diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/remotestore/RemoteCacheSessionListener.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/remotestore/RemoteCacheSessionListener.java new file mode 100644 index 0000000000..d29e2206ed --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/remotestore/RemoteCacheSessionListener.java @@ -0,0 +1,208 @@ +/* + * 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.models.sessions.infinispan.remotestore; + +import org.infinispan.Cache; +import org.infinispan.client.hotrod.RemoteCache; +import org.infinispan.client.hotrod.annotation.ClientCacheEntryCreated; +import org.infinispan.client.hotrod.annotation.ClientCacheEntryModified; +import org.infinispan.client.hotrod.annotation.ClientCacheEntryRemoved; +import org.infinispan.client.hotrod.annotation.ClientCacheFailover; +import org.infinispan.client.hotrod.annotation.ClientListener; +import org.infinispan.client.hotrod.event.ClientCacheEntryCreatedEvent; +import org.infinispan.client.hotrod.event.ClientCacheEntryModifiedEvent; +import org.infinispan.client.hotrod.event.ClientCacheEntryRemovedEvent; +import org.infinispan.client.hotrod.event.ClientCacheFailoverEvent; +import org.infinispan.client.hotrod.event.ClientEvent; +import org.infinispan.context.Flag; +import org.jboss.logging.Logger; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper; +import org.keycloak.models.sessions.infinispan.entities.SessionEntity; +import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity; +import org.keycloak.models.sessions.infinispan.util.InfinispanUtil; +import java.util.Random; +import java.util.logging.Level; +import org.infinispan.client.hotrod.VersionedValue; + +/** + * @author Marek Posolda + */ +@ClientListener +public class RemoteCacheSessionListener { + + protected static final Logger logger = Logger.getLogger(RemoteCacheSessionListener.class); + + private Cache cache; + private RemoteCache remoteCache; + private boolean distributed; + private String myAddress; + + + protected RemoteCacheSessionListener() { + } + + + protected void init(KeycloakSession session, Cache cache, RemoteCache remoteCache) { + this.cache = cache; + this.remoteCache = remoteCache; + + this.distributed = InfinispanUtil.isDistributedCache(cache); + if (this.distributed) { + this.myAddress = InfinispanUtil.getMyAddress(session); + } else { + this.myAddress = null; + } + } + + + @ClientCacheEntryCreated + public void created(ClientCacheEntryCreatedEvent event) { + String key = (String) event.getKey(); + + if (shouldUpdateLocalCache(event.getType(), key, event.isCommandRetried())) { + // Should load it from remoteStore + cache.get(key); + } + } + + + @ClientCacheEntryModified + public void updated(ClientCacheEntryModifiedEvent event) { + String key = (String) event.getKey(); + + if (shouldUpdateLocalCache(event.getType(), key, event.isCommandRetried())) { + + replaceRemoteEntityInCache(key, event.getVersion()); + } + } + + private static final int MAXIMUM_REPLACE_RETRIES = 10; + + private void replaceRemoteEntityInCache(String key, long eventVersion) { + // TODO can be optimized and remoteSession sent in the event itself? + boolean replaced = false; + int replaceRetries = 0; + int sleepInterval = 25; + do { + replaceRetries++; + + SessionEntityWrapper localEntityWrapper = cache.get(key); + VersionedValue remoteSessionVersioned = remoteCache.getVersioned(key); + if (remoteSessionVersioned == null || remoteSessionVersioned.getVersion() < eventVersion) { + try { + logger.debugf("Got replace remote entity event prematurely, will try again. Event version: %d, got: %d", + eventVersion, remoteSessionVersioned == null ? -1 : remoteSessionVersioned.getVersion()); + Thread.sleep(new Random().nextInt(sleepInterval)); // using exponential backoff + continue; + } catch (InterruptedException ex) { + continue; + } finally { + sleepInterval = sleepInterval << 1; + } + } + SessionEntity remoteSession = (SessionEntity) remoteCache.get(key); + + logger.debugf("Read session%s. Entity read from remote cache: %s", replaceRetries > 1 ? "" : " again", remoteSession); + + SessionEntityWrapper sessionWrapper = remoteSession.mergeRemoteEntityWithLocalEntity(localEntityWrapper); + + // We received event from remoteCache, so we won't update it back + replaced = cache.getAdvancedCache().withFlags(Flag.SKIP_CACHE_STORE, Flag.SKIP_CACHE_LOAD, Flag.IGNORE_RETURN_VALUES) + .replace(key, localEntityWrapper, sessionWrapper); + + if (! replaced) { + logger.debugf("Did not succeed in merging sessions, will try again: %s", remoteSession); + } + } while (replaceRetries < MAXIMUM_REPLACE_RETRIES && ! replaced); + } + + + @ClientCacheEntryRemoved + public void removed(ClientCacheEntryRemovedEvent event) { + String key = (String) event.getKey(); + + if (shouldUpdateLocalCache(event.getType(), key, event.isCommandRetried())) { + // We received event from remoteCache, so we won't update it back + cache.getAdvancedCache().withFlags(Flag.SKIP_CACHE_STORE, Flag.SKIP_CACHE_LOAD, Flag.IGNORE_RETURN_VALUES) + .remove(key); + } + } + + + @ClientCacheFailover + public void failover(ClientCacheFailoverEvent event) { + logger.infof("Received failover event: " + event.toString()); + } + + + // For distributed caches, ensure that local modification is executed just on owner OR if event.isCommandRetried + protected boolean shouldUpdateLocalCache(ClientEvent.Type type, String key, boolean commandRetried) { + boolean result; + + // Case when cache is stopping or stopped already + if (!cache.getStatus().allowInvocations()) { + return false; + } + + if (!distributed || commandRetried) { + result = true; + } else { + String keyAddress = InfinispanUtil.getKeyPrimaryOwnerAddress(cache, key); + result = myAddress.equals(keyAddress); + } + + logger.debugf("Received event from remote store. Event '%s', key '%s', skip '%b'", type.toString(), key, !result); + + return result; + } + + + + @ClientListener(includeCurrentState = true) + public static class FetchInitialStateCacheListener extends RemoteCacheSessionListener { + } + + + @ClientListener(includeCurrentState = false) + public static class DontFetchInitialStateCacheListener extends RemoteCacheSessionListener { + } + + + public static RemoteCacheSessionListener createListener(KeycloakSession session, Cache cache, RemoteCache remoteCache) { + /*boolean isCoordinator = InfinispanUtil.isCoordinator(cache); + + // Just cluster coordinator will fetch userSessions from remote cache. + // In case that coordinator is failover during state fetch, there is slight risk that not all userSessions will be fetched to local cluster. Assume acceptable for now + RemoteCacheSessionListener listener; + if (isCoordinator) { + logger.infof("Will fetch initial state from remote cache for cache '%s'", cache.getName()); + listener = new FetchInitialStateCacheListener(); + } else { + logger.infof("Won't fetch initial state from remote cache for cache '%s'", cache.getName()); + listener = new DontFetchInitialStateCacheListener(); + }*/ + + RemoteCacheSessionListener listener = new RemoteCacheSessionListener(); + listener.init(session, cache, remoteCache); + + return listener; + } + + +} diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/remotestore/RemoteCacheSessionsLoader.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/remotestore/RemoteCacheSessionsLoader.java new file mode 100644 index 0000000000..65c31bc77d --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/remotestore/RemoteCacheSessionsLoader.java @@ -0,0 +1,120 @@ +/* + * 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.models.sessions.infinispan.remotestore; + +import java.io.Serializable; +import java.util.Map; + +import org.infinispan.Cache; +import org.infinispan.client.hotrod.RemoteCache; +import org.infinispan.context.Flag; +import org.jboss.logging.Logger; +import org.keycloak.connections.infinispan.InfinispanConnectionProvider; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper; +import org.keycloak.models.sessions.infinispan.entities.SessionEntity; +import org.keycloak.models.sessions.infinispan.initializer.BaseCacheInitializer; +import org.keycloak.models.sessions.infinispan.initializer.OfflinePersistentUserSessionLoader; +import org.keycloak.models.sessions.infinispan.initializer.SessionLoader; +import org.keycloak.models.sessions.infinispan.util.InfinispanUtil; + +/** + * @author Marek Posolda + */ +public class RemoteCacheSessionsLoader implements SessionLoader { + + private static final Logger log = Logger.getLogger(RemoteCacheSessionsLoader.class); + + // Hardcoded limit for now. See if needs to be configurable (or if preloading can be enabled/disabled in configuration) + public static final int LIMIT = 100000; + + private final String cacheName; + + public RemoteCacheSessionsLoader(String cacheName) { + this.cacheName = cacheName; + } + + @Override + public void init(KeycloakSession session) { + + } + + @Override + public int getSessionsCount(KeycloakSession session) { + RemoteCache remoteCache = InfinispanUtil.getRemoteCache(getCache(session)); + return remoteCache.size(); + } + + @Override + public boolean loadSessions(KeycloakSession session, int first, int max) { + Cache cache = getCache(session); + Cache decoratedCache = cache.getAdvancedCache().withFlags(Flag.SKIP_CACHE_LOAD, Flag.SKIP_CACHE_STORE, Flag.IGNORE_RETURN_VALUES); + + RemoteCache remoteCache = InfinispanUtil.getRemoteCache(cache); + + int size = remoteCache.size(); + + if (size > LIMIT) { + log.infof("Skip bulk load of '%d' sessions from remote cache '%s'. Sessions will be retrieved lazily", size, cache.getName()); + return true; + } else { + log.infof("Will do bulk load of '%d' sessions from remote cache '%s'", size, cache.getName()); + } + + + for (Map.Entry entry : remoteCache.getBulk().entrySet()) { + SessionEntity entity = (SessionEntity) entry.getValue(); + SessionEntityWrapper entityWrapper = new SessionEntityWrapper(entity); + + decoratedCache.putAsync(entry.getKey(), entityWrapper); + } + + return true; + } + + + private Cache getCache(KeycloakSession session) { + InfinispanConnectionProvider ispn = session.getProvider(InfinispanConnectionProvider.class); + return ispn.getCache(cacheName); + } + + + @Override + public boolean isFinished(BaseCacheInitializer initializer) { + Cache workCache = initializer.getWorkCache(); + + // Check if persistent sessions were already loaded in this DC. This is possible just for offline sessions ATM + Boolean sessionsLoaded = (Boolean) workCache + .getAdvancedCache().withFlags(Flag.SKIP_CACHE_LOAD, Flag.SKIP_CACHE_STORE) + .get(OfflinePersistentUserSessionLoader.PERSISTENT_SESSIONS_LOADED_IN_CURRENT_DC); + + if (cacheName.equals(InfinispanConnectionProvider.OFFLINE_SESSION_CACHE_NAME) && sessionsLoaded != null && sessionsLoaded) { + log.debugf("Sessions already loaded in current DC. Skip sessions loading from remote cache '%s'", cacheName); + return true; + } else { + log.debugf("Sessions maybe not yet loaded in current DC. Will load them from remote cache '%s'", cacheName); + return false; + } + } + + + @Override + public void afterAllSessionsLoaded(BaseCacheInitializer initializer) { + + } +} diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/Mappers.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/Mappers.java index dd2db6821f..f75391cd61 100644 --- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/Mappers.java +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/Mappers.java @@ -17,7 +17,7 @@ package org.keycloak.models.sessions.infinispan.stream; -import org.keycloak.models.sessions.infinispan.UserSessionTimestamp; +import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper; import org.keycloak.models.sessions.infinispan.entities.LoginFailureEntity; import org.keycloak.models.sessions.infinispan.entities.LoginFailureKey; import org.keycloak.models.sessions.infinispan.entities.SessionEntity; @@ -25,7 +25,6 @@ import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity; import java.io.Serializable; import java.util.Map; -import java.util.Optional; import java.util.function.Function; /** @@ -33,19 +32,19 @@ import java.util.function.Function; */ public class Mappers { - public static Function>, UserSessionTimestamp> userSessionTimestamp() { - return new UserSessionTimestampMapper(); + public static Function, Map.Entry> unwrap() { + return new SessionUnwrap(); } - public static Function, String> sessionId() { + public static Function>, String> sessionId() { return new SessionIdMapper(); } - public static Function, SessionEntity> sessionEntity() { + public static Function, SessionEntity> sessionEntity() { return new SessionEntityMapper(); } - public static Function, UserSessionEntity> userSessionEntity() { + public static Function>, UserSessionEntity> userSessionEntity() { return new UserSessionEntityMapper(); } @@ -53,32 +52,55 @@ public class Mappers { return new LoginFailureIdMapper(); } - private static class UserSessionTimestampMapper implements Function>, org.keycloak.models.sessions.infinispan.UserSessionTimestamp>, Serializable { + + private static class SessionUnwrap implements Function, Map.Entry>, Serializable { + @Override - public org.keycloak.models.sessions.infinispan.UserSessionTimestamp apply(Map.Entry> e) { - return e.getValue().get(); + public Map.Entry apply(Map.Entry wrapperEntry) { + return new Map.Entry() { + + @Override + public String getKey() { + return wrapperEntry.getKey(); + } + + @Override + public SessionEntity getValue() { + return wrapperEntry.getValue().getEntity(); + } + + @Override + public SessionEntity setValue(SessionEntity value) { + throw new IllegalStateException("Unsupported operation"); + } + + }; } + } - private static class SessionIdMapper implements Function, String>, Serializable { + + private static class SessionIdMapper implements Function>, String>, Serializable { @Override - public String apply(Map.Entry entry) { + public String apply(Map.Entry> entry) { return entry.getKey(); } } - private static class SessionEntityMapper implements Function, SessionEntity>, Serializable { + private static class SessionEntityMapper implements Function, SessionEntity>, Serializable { @Override - public SessionEntity apply(Map.Entry entry) { - return entry.getValue(); + public SessionEntity apply(Map.Entry entry) { + return entry.getValue().getEntity(); } } - private static class UserSessionEntityMapper implements Function, UserSessionEntity>, Serializable { + private static class UserSessionEntityMapper implements Function>, UserSessionEntity>, Serializable { + @Override - public UserSessionEntity apply(Map.Entry entry) { - return (UserSessionEntity) entry.getValue(); + public UserSessionEntity apply(Map.Entry> entry) { + return entry.getValue().getEntity(); } + } private static class LoginFailureIdMapper implements Function, LoginFailureKey>, Serializable { diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/SessionPredicate.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/SessionPredicate.java index c8160bb91b..f72b92d7ce 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/SessionPredicate.java +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/SessionPredicate.java @@ -17,6 +17,7 @@ package org.keycloak.models.sessions.infinispan.stream; +import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper; import org.keycloak.models.sessions.infinispan.entities.SessionEntity; import java.io.Serializable; @@ -26,7 +27,7 @@ import java.util.function.Predicate; /** * @author Stian Thorgersen */ -public class SessionPredicate implements Predicate>, Serializable { +public class SessionPredicate implements Predicate>>, Serializable { private String realm; @@ -39,8 +40,8 @@ public class SessionPredicate implements Predicate entry) { - return realm.equals(entry.getValue().getRealm()); + public boolean test(Map.Entry> entry) { + return realm.equals(entry.getValue().getEntity().getRealm()); } } diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/UserSessionPredicate.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/UserSessionPredicate.java index 0cc3fccf97..06609f2b6d 100644 --- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/UserSessionPredicate.java +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/UserSessionPredicate.java @@ -17,6 +17,7 @@ package org.keycloak.models.sessions.infinispan.stream; +import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper; import org.keycloak.models.sessions.infinispan.entities.SessionEntity; import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity; @@ -27,7 +28,7 @@ import java.util.function.Predicate; /** * @author Stian Thorgersen */ -public class UserSessionPredicate implements Predicate>, Serializable { +public class UserSessionPredicate implements Predicate>>, Serializable { private String realm; @@ -77,12 +78,8 @@ public class UserSessionPredicate implements Predicate entry) { - SessionEntity e = entry.getValue(); - - if (!(e instanceof UserSessionEntity)) { - return false; - } + public boolean test(Map.Entry> entry) { + SessionEntity e = entry.getValue().getEntity(); UserSessionEntity entity = (UserSessionEntity) e; diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/util/InfinispanUtil.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/util/InfinispanUtil.java new file mode 100644 index 0000000000..1bb2862250 --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/util/InfinispanUtil.java @@ -0,0 +1,95 @@ +/* + * Copyright 2016 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.models.sessions.infinispan.util; + +import java.util.Set; + +import org.infinispan.Cache; +import org.infinispan.client.hotrod.RemoteCache; +import org.infinispan.configuration.cache.CacheMode; +import org.infinispan.distribution.DistributionManager; +import org.infinispan.persistence.manager.PersistenceManager; +import org.infinispan.persistence.remote.RemoteStore; +import org.infinispan.remoting.transport.Address; +import org.infinispan.remoting.transport.Transport; +import org.keycloak.connections.infinispan.InfinispanConnectionProvider; +import org.keycloak.models.KeycloakSession; + +/** + * @author Marek Posolda + */ +public class InfinispanUtil { + + // See if we have RemoteStore (external JDG) configured for cross-Data-Center scenario + public static Set getRemoteStores(Cache ispnCache) { + return ispnCache.getAdvancedCache().getComponentRegistry().getComponent(PersistenceManager.class).getStores(RemoteStore.class); + } + + + public static RemoteCache getRemoteCache(Cache ispnCache) { + Set remoteStores = getRemoteStores(ispnCache); + if (remoteStores.isEmpty()) { + return null; + } else { + return remoteStores.iterator().next().getRemoteCache(); + } + } + + + public static boolean isDistributedCache(Cache ispnCache) { + CacheMode cacheMode = ispnCache.getCacheConfiguration().clustering().cacheMode(); + return cacheMode.isDistributed(); + } + + + public static String getMyAddress(KeycloakSession session) { + return session.getProvider(InfinispanConnectionProvider.class).getNodeName(); + } + + public static String getMySite(KeycloakSession session) { + return session.getProvider(InfinispanConnectionProvider.class).getSiteName(); + } + + + /** + * + * @param ispnCache + * @param key + * @return address of the node, who is owner of the specified key in current cluster + */ + public static String getKeyPrimaryOwnerAddress(Cache ispnCache, Object key) { + DistributionManager distManager = ispnCache.getAdvancedCache().getDistributionManager(); + if (distManager == null) { + throw new IllegalArgumentException("Cache '" + ispnCache.getName() + "' is not distributed cache"); + } + + return distManager.getPrimaryLocation(key).toString(); + } + + + /** + * + * @param cache + * @return true if cluster coordinator OR if it's local cache + */ + public static boolean isCoordinator(Cache cache) { + Transport transport = cache.getCacheManager().getTransport(); + return transport == null || transport.isCoordinator(); + } + +} diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/util/KeycloakMarshallUtil.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/util/KeycloakMarshallUtil.java new file mode 100644 index 0000000000..e732c11ccb --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/util/KeycloakMarshallUtil.java @@ -0,0 +1,164 @@ +/* + * 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.models.sessions.infinispan.util; + +import java.io.IOException; +import java.io.ObjectInput; +import java.io.ObjectOutput; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.infinispan.commons.marshall.Externalizer; +import org.infinispan.commons.marshall.MarshallUtil; +import org.jboss.logging.Logger; + +/** + * + * Helper to optimize marshalling/unmarhsalling of some types + * + * @author Marek Posolda + */ +public class KeycloakMarshallUtil { + + private static final Logger log = Logger.getLogger(KeycloakMarshallUtil.class); + + public static final StringExternalizer STRING_EXT = new StringExternalizer(); + + // MAP + + public static void writeMap(Map map, Externalizer keyExternalizer, Externalizer valueExternalizer, ObjectOutput output) throws IOException { + if (map == null) { + output.writeByte(0); + } else { + output.writeByte(1); + + // Copy the map as it can be updated concurrently + Map copy = new HashMap<>(map); + //Map copy = map; + + output.writeInt(copy.size()); + + for (Map.Entry entry : copy.entrySet()) { + keyExternalizer.writeObject(output, entry.getKey()); + valueExternalizer.writeObject(output, entry.getValue()); + } + } + } + + public static > TYPED_MAP readMap(ObjectInput input, + Externalizer keyExternalizer, Externalizer valueExternalizer, + MarshallUtil.MapBuilder mapBuilder) throws IOException, ClassNotFoundException { + byte b = input.readByte(); + if (b == 0) { + return null; + } else { + + int size = input.readInt(); + + TYPED_MAP map = mapBuilder.build(size); + + for (int i=0 ; i void writeCollection(Collection col, Externalizer valueExternalizer, ObjectOutput output) throws IOException { + if (col == null) { + output.writeByte(0); + } else { + output.writeByte(1); + + // Copy the collection as it can be updated concurrently + Collection copy = new LinkedList<>(col); + + output.writeInt(copy.size()); + + for (E entry : copy) { + valueExternalizer.writeObject(output, entry); + } + } + } + + public static > T readCollection(ObjectInput input, Externalizer valueExternalizer, + MarshallUtil.CollectionBuilder colBuilder) throws ClassNotFoundException, IOException { + byte b = input.readByte(); + if (b == 0) { + return null; + } else { + + int size = input.readInt(); + + T col = colBuilder.build(size); + + for (int i=0 ; i implements MarshallUtil.MapBuilder> { + + @Override + public ConcurrentHashMap build(int size) { + return new ConcurrentHashMap<>(size); + } + + } + + public static class HashSetBuilder implements MarshallUtil.CollectionBuilder> { + + @Override + public HashSet build(int size) { + return new HashSet<>(size); + } + } + + + private static class StringExternalizer implements Externalizer { + + @Override + public void writeObject(ObjectOutput output, String str) throws IOException { + MarshallUtil.marshallString(str, output); + } + + @Override + public String readObject(ObjectInput input) throws IOException, ClassNotFoundException { + return MarshallUtil.unmarshallString(input); + } + + } + +} diff --git a/model/infinispan/src/test/java/org/keycloak/cluster/infinispan/ConcurrencyJDGRemoteCacheClientListenersTest.java b/model/infinispan/src/test/java/org/keycloak/cluster/infinispan/ConcurrencyJDGRemoteCacheClientListenersTest.java new file mode 100644 index 0000000000..b5248850ae --- /dev/null +++ b/model/infinispan/src/test/java/org/keycloak/cluster/infinispan/ConcurrencyJDGRemoteCacheClientListenersTest.java @@ -0,0 +1,239 @@ +/* + * Copyright 2017 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.cluster.infinispan; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; + +import org.infinispan.Cache; +import org.infinispan.client.hotrod.RemoteCache; +import org.infinispan.client.hotrod.annotation.ClientCacheEntryCreated; +import org.infinispan.client.hotrod.annotation.ClientCacheEntryModified; +import org.infinispan.client.hotrod.annotation.ClientListener; +import org.infinispan.client.hotrod.event.ClientCacheEntryCreatedEvent; +import org.infinispan.client.hotrod.event.ClientCacheEntryModifiedEvent; +import org.infinispan.manager.EmbeddedCacheManager; +import org.infinispan.persistence.manager.PersistenceManager; +import org.infinispan.persistence.remote.RemoteStore; +import org.infinispan.persistence.remote.configuration.RemoteStoreConfigurationBuilder; +import org.junit.Assert; +import org.keycloak.connections.infinispan.InfinispanConnectionProvider; +import org.keycloak.models.sessions.infinispan.util.InfinispanUtil; + +/** + * Test that hotrod ClientListeners are correctly executed as expected + * + * STEPS TO REPRODUCE: + * - Unzip infinispan-server-8.2.6.Final to some locations ISPN1 and ISPN2 + * + * - Edit both ISPN1/standalone/configuration/clustered.xml and ISPN2/standalone/configuration/clustered.xml . Configure cache in container "clustered" + * + * + + + + + + - Run server1 + ./standalone.sh -c clustered.xml -Djava.net.preferIPv4Stack=true -Djboss.socket.binding.port-offset=1010 -Djboss.default.multicast.address=234.56.78.99 -Djboss.node.name=cache-server + + - Run server2 + ./standalone.sh -c clustered.xml -Djava.net.preferIPv4Stack=true -Djboss.socket.binding.port-offset=2010 -Djboss.default.multicast.address=234.56.78.99 -Djboss.node.name=cache-server-dc-2 + + - Run this test as main class from IDE + * + * + * + * @author Marek Posolda + */ +public class ConcurrencyJDGRemoteCacheClientListenersTest { + + // Helper map to track if listeners were executed + private static Map state = new HashMap<>(); + + private static AtomicInteger totalListenerCalls = new AtomicInteger(0); + + private static AtomicInteger totalErrors = new AtomicInteger(0); + + + public static void main(String[] args) throws Exception { + // Init map somehow + for (int i=0 ; i<1000 ; i++) { + String key = "key-" + i; + EntryInfo entryInfo = new EntryInfo(); + entryInfo.val.set(i); + state.put(key, entryInfo); + } + + // Create caches, listeners and finally worker threads + Worker worker1 = createWorker(1); + Worker worker2 = createWorker(2); + + // Note "run", so it's not executed asynchronously here!!! + worker1.run(); + +// +// // Start and join workers +// worker1.start(); +// worker2.start(); +// +// worker1.join(); +// worker2.join(); + + // Output + for (Map.Entry entry : state.entrySet()) { + System.out.println(entry.getKey() + ":::" + entry.getValue()); + } + + System.out.println("totalListeners: " + totalListenerCalls.get() + ", totalErrors: " + totalErrors.get()); + + + // Assert that ClientListener was able to read the value and save it into EntryInfo + try { + for (Map.Entry entry : state.entrySet()) { + EntryInfo info = entry.getValue(); + Assert.assertEquals(info.val.get(), info.dc1Created.get()); + Assert.assertEquals(info.val.get(), info.dc2Created.get()); + Assert.assertEquals(info.val.get() * 2, info.dc1Updated.get()); + Assert.assertEquals(info.val.get() * 2, info.dc2Updated.get()); + worker1.cache.remove(entry.getKey()); + } + } finally { + // Finish JVM + worker1.cache.getCacheManager().stop(); + worker2.cache.getCacheManager().stop(); + } + } + + private static Worker createWorker(int threadId) { + EmbeddedCacheManager manager = new TestCacheManagerFactory().createManager(threadId, InfinispanConnectionProvider.WORK_CACHE_NAME, RemoteStoreConfigurationBuilder.class); + Cache cache = manager.getCache(InfinispanConnectionProvider.WORK_CACHE_NAME); + + System.out.println("Retrieved cache: " + threadId); + + RemoteStore remoteStore = cache.getAdvancedCache().getComponentRegistry().getComponent(PersistenceManager.class).getStores(RemoteStore.class).iterator().next(); + HotRodListener listener = new HotRodListener(cache, threadId); + remoteStore.getRemoteCache().addClientListener(listener); + + return new Worker(cache, threadId); + } + + + @ClientListener + public static class HotRodListener { + + private final RemoteCache remoteCache; + private final int threadId; + + public HotRodListener(Cache cache, int threadId) { + this.remoteCache = InfinispanUtil.getRemoteCache(cache); + this.threadId = threadId; + } + + //private AtomicInteger listenerCount = new AtomicInteger(0); + + @ClientCacheEntryCreated + public void created(ClientCacheEntryCreatedEvent event) { + String cacheKey = (String) event.getKey(); + event(cacheKey, true); + + } + + + @ClientCacheEntryModified + public void updated(ClientCacheEntryModifiedEvent event) { + String cacheKey = (String) event.getKey(); + event(cacheKey, false); + } + + + private void event(String cacheKey, boolean created) { + EntryInfo entryInfo = state.get(cacheKey); + entryInfo.successfulListenerWrites.incrementAndGet(); + + totalListenerCalls.incrementAndGet(); + + Integer val = remoteCache.get(cacheKey); + if (val != null) { + AtomicInteger dcVal; + if (created) { + dcVal = threadId == 1 ? entryInfo.dc1Created : entryInfo.dc2Created; + } else { + dcVal = threadId == 1 ? entryInfo.dc1Updated : entryInfo.dc2Updated; + } + dcVal.set(val); + } else { + System.err.println("NOT A VALUE FOR KEY: " + cacheKey); + totalErrors.incrementAndGet(); + } + } + + } + + + private static class Worker extends Thread { + + private final Cache cache; + + private final int myThreadId; + + private Worker(Cache cache, int myThreadId) { + this.cache = cache; + this.myThreadId = myThreadId; + } + + @Override + public void run() { + for (Map.Entry entry : state.entrySet()) { + String cacheKey = entry.getKey(); + Integer value = entry.getValue().val.get(); + + this.cache.put(cacheKey, value); + } + + System.out.println("Worker creating finished: " + myThreadId); + + for (Map.Entry entry : state.entrySet()) { + String cacheKey = entry.getKey(); + Integer value = entry.getValue().val.get() * 2; + + this.cache.replace(cacheKey, value); + } + + System.out.println("Worker updating finished: " + myThreadId); + } + + } + + + public static class EntryInfo { + AtomicInteger val = new AtomicInteger(); + AtomicInteger successfulListenerWrites = new AtomicInteger(0); + AtomicInteger dc1Created = new AtomicInteger(); + AtomicInteger dc2Created = new AtomicInteger(); + AtomicInteger dc1Updated = new AtomicInteger(); + AtomicInteger dc2Updated = new AtomicInteger(); + + @Override + public String toString() { + return String.format("val: %d, successfulListenerWrites: %d, dc1Created: %d, dc2Created: %d, dc1Updated: %d, dc2Updated: %d", val.get(), successfulListenerWrites.get(), + dc1Created.get(), dc2Created.get(), dc1Updated.get(), dc2Updated.get()); + } + } +} diff --git a/model/infinispan/src/test/java/org/keycloak/cluster/infinispan/ConcurrencyJDGRemoteCacheTest.java b/model/infinispan/src/test/java/org/keycloak/cluster/infinispan/ConcurrencyJDGRemoteCacheTest.java index e7c1337934..9c23452e06 100644 --- a/model/infinispan/src/test/java/org/keycloak/cluster/infinispan/ConcurrencyJDGRemoteCacheTest.java +++ b/model/infinispan/src/test/java/org/keycloak/cluster/infinispan/ConcurrencyJDGRemoteCacheTest.java @@ -43,11 +43,12 @@ import org.junit.Ignore; import org.keycloak.connections.infinispan.InfinispanConnectionProvider; /** - * Test concurrency for remoteStore (backed by HotRod RemoteCaches) against external JDG + * Test concurrency for remoteStore (backed by HotRod RemoteCaches) against external JDG. Especially tests "putIfAbsent" contract. + * + * Steps: {@see ConcurrencyJDGRemoteCacheClientListenersTest} * * @author Marek Posolda */ -@Ignore public class ConcurrencyJDGRemoteCacheTest { private static Map state = new HashMap<>(); @@ -82,7 +83,7 @@ public class ConcurrencyJDGRemoteCacheTest { } private static Worker createWorker(int threadId) { - EmbeddedCacheManager manager = createManager(threadId); + EmbeddedCacheManager manager = new TestCacheManagerFactory().createManager(threadId, InfinispanConnectionProvider.WORK_CACHE_NAME, RemoteStoreConfigurationBuilder.class); Cache cache = manager.getCache(InfinispanConnectionProvider.WORK_CACHE_NAME); System.out.println("Retrieved cache: " + threadId); @@ -94,56 +95,6 @@ public class ConcurrencyJDGRemoteCacheTest { return new Worker(cache, threadId); } - private static EmbeddedCacheManager createManager(int threadId) { - System.setProperty("java.net.preferIPv4Stack", "true"); - System.setProperty("jgroups.tcp.port", "53715"); - GlobalConfigurationBuilder gcb = new GlobalConfigurationBuilder(); - - boolean clustered = false; - boolean async = false; - boolean allowDuplicateJMXDomains = true; - - if (clustered) { - gcb = gcb.clusteredDefault(); - gcb.transport().clusterName("test-clustering"); - } - - gcb.globalJmxStatistics().allowDuplicateDomains(allowDuplicateJMXDomains); - - EmbeddedCacheManager cacheManager = new DefaultCacheManager(gcb.build()); - - Configuration invalidationCacheConfiguration = getCacheBackedByRemoteStore(threadId); - - cacheManager.defineConfiguration(InfinispanConnectionProvider.WORK_CACHE_NAME, invalidationCacheConfiguration); - return cacheManager; - - } - - private static Configuration getCacheBackedByRemoteStore(int threadId) { - ConfigurationBuilder cacheConfigBuilder = new ConfigurationBuilder(); - - // int port = threadId==1 ? 11222 : 11322; - int port = 11222; - - return cacheConfigBuilder.persistence().addStore(RemoteStoreConfigurationBuilder.class) - .fetchPersistentState(false) - .ignoreModifications(false) - .purgeOnStartup(false) - .preload(false) - .shared(true) - .remoteCacheName(InfinispanConnectionProvider.WORK_CACHE_NAME) - .rawValues(true) - .forceReturnValues(false) - .addServer() - .host("localhost") - .port(port) - .connectionPool() - .maxActive(20) - .exhaustedAction(ExhaustedAction.CREATE_NEW) - .async() - . enabled(false).build(); - } - @ClientListener public static class HotRodListener { @@ -214,7 +165,7 @@ public class ConcurrencyJDGRemoteCacheTest { } } - private static class EntryInfo { + public static class EntryInfo { AtomicInteger successfulInitializations = new AtomicInteger(0); AtomicInteger successfulListenerWrites = new AtomicInteger(0); AtomicInteger th1 = new AtomicInteger(); diff --git a/model/infinispan/src/test/java/org/keycloak/cluster/infinispan/ConcurrencyJDGRemoveSessionTest.java b/model/infinispan/src/test/java/org/keycloak/cluster/infinispan/ConcurrencyJDGRemoveSessionTest.java new file mode 100644 index 0000000000..ff4c3ce83e --- /dev/null +++ b/model/infinispan/src/test/java/org/keycloak/cluster/infinispan/ConcurrencyJDGRemoveSessionTest.java @@ -0,0 +1,292 @@ +/* + * Copyright 2017 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.cluster.infinispan; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.concurrent.atomic.AtomicInteger; + +import org.infinispan.Cache; +import org.infinispan.client.hotrod.RemoteCache; +import org.infinispan.client.hotrod.annotation.ClientCacheEntryCreated; +import org.infinispan.client.hotrod.annotation.ClientCacheEntryModified; +import org.infinispan.client.hotrod.annotation.ClientCacheEntryRemoved; +import org.infinispan.client.hotrod.annotation.ClientListener; +import org.infinispan.client.hotrod.event.ClientCacheEntryCreatedEvent; +import org.infinispan.client.hotrod.event.ClientCacheEntryModifiedEvent; +import org.infinispan.client.hotrod.event.ClientCacheEntryRemovedEvent; +import org.infinispan.context.Flag; +import org.infinispan.manager.EmbeddedCacheManager; +import org.infinispan.persistence.remote.configuration.RemoteStoreConfigurationBuilder; +import org.jboss.logging.Logger; +import org.junit.Assert; +import org.keycloak.common.util.Time; +import org.keycloak.connections.infinispan.InfinispanConnectionProvider; +import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper; +import org.keycloak.models.sessions.infinispan.entities.AuthenticatedClientSessionEntity; +import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity; +import org.keycloak.models.sessions.infinispan.util.InfinispanUtil; + +/** + * Check that removing of session from remoteCache is session immediately removed on remoteCache in other DC. This is true. + * + * Also check that listeners are executed asynchronously with some delay. + * + * Steps: {@see ConcurrencyJDGRemoteCacheClientListenersTest} + * + * @author Marek Posolda + */ +public class ConcurrencyJDGRemoveSessionTest { + + protected static final Logger logger = Logger.getLogger(ConcurrencyJDGRemoveSessionTest.class); + + private static final int ITERATIONS = 10000; + + private static RemoteCache remoteCache1; + private static RemoteCache remoteCache2; + + private static final AtomicInteger failedReplaceCounter = new AtomicInteger(0); + private static final AtomicInteger failedReplaceCounter2 = new AtomicInteger(0); + + private static final AtomicInteger successfulListenerWrites = new AtomicInteger(0); + private static final AtomicInteger successfulListenerWrites2 = new AtomicInteger(0); + + //private static Map state = new HashMap<>(); + + public static void main(String[] args) throws Exception { + Cache> cache1 = createManager(1).getCache(InfinispanConnectionProvider.SESSION_CACHE_NAME); + Cache> cache2 = createManager(2).getCache(InfinispanConnectionProvider.SESSION_CACHE_NAME); + + // Create caches, listeners and finally worker threads + Thread worker1 = createWorker(cache1, 1); + Thread worker2 = createWorker(cache2, 2); + + // Create 100 initial sessions + for (int i=0 ; i wrappedSession = createSessionEntity(sessionId); + cache1.put(sessionId, wrappedSession); + } + + logger.info("SESSIONS CREATED"); + + // Create 100 initial sessions + for (int i=0 ; i=0 ; i--) { + String sessionId = String.valueOf(i); + + logger.infof("Before call cache2.get: %s", sessionId); + + SessionEntityWrapper loadedWrapper = cache2.get(sessionId); + Assert.assertNull("Loaded wrapper not null for key " + sessionId, loadedWrapper); + } + + logger.info("SESSIONS NOT AVAILABLE ON DC2"); + + + // // Start and join workers +// worker1.start(); +// worker2.start(); +// +// worker1.join(); +// worker2.join(); + + } finally { + Thread.sleep(2000); + + // Finish JVM + cache1.getCacheManager().stop(); + cache2.getCacheManager().stop(); + } + + long took = System.currentTimeMillis() - start; + +// // Output +// for (Map.Entry entry : state.entrySet()) { +// System.out.println(entry.getKey() + ":::" + entry.getValue()); +// worker1.cache.remove(entry.getKey()); +// } + +// System.out.println("Finished. Took: " + took + " ms. Notes: " + cache1.get("123").getEntity().getNotes().size() + +// ", successfulListenerWrites: " + successfulListenerWrites.get() + ", successfulListenerWrites2: " + successfulListenerWrites2.get() + +// ", failedReplaceCounter: " + failedReplaceCounter.get() + ", failedReplaceCounter2: " + failedReplaceCounter2.get() ); +// +// System.out.println("Sleeping before other report"); +// +// Thread.sleep(1000); +// +// System.out.println("Finished. Took: " + took + " ms. Notes: " + cache1.get("123").getEntity().getNotes().size() + +// ", successfulListenerWrites: " + successfulListenerWrites.get() + ", successfulListenerWrites2: " + successfulListenerWrites2.get() + +// ", failedReplaceCounter: " + failedReplaceCounter.get() + ", failedReplaceCounter2: " + failedReplaceCounter2.get()); + + + } + + + private static SessionEntityWrapper createSessionEntity(String sessionId) { + // Create 100 initial sessions + UserSessionEntity session = new UserSessionEntity(); + session.setId(sessionId); + session.setRealm("foo"); + session.setBrokerSessionId("!23123123"); + session.setBrokerUserId(null); + session.setUser("foo"); + session.setLoginUsername("foo"); + session.setIpAddress("123.44.143.178"); + session.setStarted(Time.currentTime()); + session.setLastSessionRefresh(Time.currentTime()); + + AuthenticatedClientSessionEntity clientSession = new AuthenticatedClientSessionEntity(); + clientSession.setAuthMethod("saml"); + clientSession.setAction("something"); + clientSession.setTimestamp(1234); + clientSession.setProtocolMappers(new HashSet<>(Arrays.asList("mapper1", "mapper2"))); + clientSession.setRoles(new HashSet<>(Arrays.asList("role1", "role2"))); + session.getAuthenticatedClientSessions().put("client1", clientSession); + + SessionEntityWrapper wrappedSession = new SessionEntityWrapper<>(session); + return wrappedSession; + } + + + private static Thread createWorker(Cache> cache, int threadId) { + System.out.println("Retrieved cache: " + threadId); + + RemoteCache remoteCache = InfinispanUtil.getRemoteCache(cache); + + if (threadId == 1) { + remoteCache1 = remoteCache; + } else { + remoteCache2 = remoteCache; + } + + AtomicInteger counter = threadId ==1 ? successfulListenerWrites : successfulListenerWrites2; + HotRodListener listener = new HotRodListener(cache, remoteCache, counter); + remoteCache.addClientListener(listener); + + return new RemoteCacheWorker(remoteCache, threadId); + //return new CacheWorker(cache, threadId); + } + + + private static EmbeddedCacheManager createManager(int threadId) { + return new TestCacheManagerFactory().createManager(threadId, InfinispanConnectionProvider.SESSION_CACHE_NAME, RemoteStoreConfigurationBuilder.class); + } + + + @ClientListener + public static class HotRodListener { + + private Cache> origCache; + private RemoteCache remoteCache; + private AtomicInteger listenerCount; + + public HotRodListener(Cache> origCache, RemoteCache remoteCache, AtomicInteger listenerCount) { + this.listenerCount = listenerCount; + this.remoteCache = remoteCache; + this.origCache = origCache; + } + + + @ClientCacheEntryCreated + public void created(ClientCacheEntryCreatedEvent event) { + String cacheKey = (String) event.getKey(); + + logger.infof("Listener executed for creating of session %s", cacheKey); + } + + + @ClientCacheEntryModified + public void modified(ClientCacheEntryModifiedEvent event) { + String cacheKey = (String) event.getKey(); + + logger.infof("Listener executed for modifying of session %s", cacheKey); + } + + + @ClientCacheEntryRemoved + public void removed(ClientCacheEntryRemovedEvent event) { + String cacheKey = (String) event.getKey(); + + logger.infof("Listener executed for removing of session %s", cacheKey); + + // TODO: for distributed caches, ensure that it is executed just on owner OR if event.isCommandRetried + origCache + .getAdvancedCache().withFlags(Flag.SKIP_CACHE_LOAD, Flag.SKIP_CACHE_STORE) + .remove(cacheKey); + + } + + } + + private static class RemoteCacheWorker extends Thread { + + private final RemoteCache remoteCache; + + private final int myThreadId; + + private RemoteCacheWorker(RemoteCache remoteCache, int myThreadId) { + this.remoteCache = remoteCache; + this.myThreadId = myThreadId; + } + + @Override + public void run() { + + for (int i=0 ; iMarek Posolda + */ +public class ConcurrencyJDGSessionsCacheTest { + + protected static final Logger logger = Logger.getLogger(ConcurrencyJDGSessionsCacheTest.class); + + private static final int ITERATION_PER_WORKER = 1000; + + private static RemoteCache remoteCache1; + private static RemoteCache remoteCache2; + + private static final AtomicInteger failedReplaceCounter = new AtomicInteger(0); + private static final AtomicInteger failedReplaceCounter2 = new AtomicInteger(0); + + private static final AtomicInteger successfulListenerWrites = new AtomicInteger(0); + private static final AtomicInteger successfulListenerWrites2 = new AtomicInteger(0); + + //private static Map state = new HashMap<>(); + + public static void main(String[] args) throws Exception { + Cache> cache1 = createManager(1).getCache(InfinispanConnectionProvider.SESSION_CACHE_NAME); + Cache> cache2 = createManager(2).getCache(InfinispanConnectionProvider.SESSION_CACHE_NAME); + + // Create initial item + UserSessionEntity session = new UserSessionEntity(); + session.setId("123"); + session.setRealm("foo"); + session.setBrokerSessionId("!23123123"); + session.setBrokerUserId(null); + session.setUser("foo"); + session.setLoginUsername("foo"); + session.setIpAddress("123.44.143.178"); + session.setStarted(Time.currentTime()); + session.setLastSessionRefresh(Time.currentTime()); + + AuthenticatedClientSessionEntity clientSession = new AuthenticatedClientSessionEntity(); + clientSession.setAuthMethod("saml"); + clientSession.setAction("something"); + clientSession.setTimestamp(1234); + clientSession.setProtocolMappers(new HashSet<>(Arrays.asList("mapper1", "mapper2"))); + clientSession.setRoles(new HashSet<>(Arrays.asList("role1", "role2"))); + session.getAuthenticatedClientSessions().put("client1", clientSession); + + SessionEntityWrapper wrappedSession = new SessionEntityWrapper<>(session); + + // Some dummy testing of remoteStore behaviour + logger.info("Before put"); + + cache1 + .getAdvancedCache().withFlags(Flag.CACHE_MODE_LOCAL) // will still invoke remoteStore . Just doesn't propagate to cluster + .put("123", wrappedSession); + + logger.info("After put"); + + cache1.replace("123", wrappedSession); + + logger.info("After replace"); + + cache1.get("123"); + + logger.info("After cache1.get"); + + cache2.get("123"); + + logger.info("After cache2.get"); + + cache1.get("123"); + + logger.info("After cache1.get - second call"); + + cache2.get("123"); + + logger.info("After cache2.get - second call"); + + cache2.replace("123", wrappedSession); + + logger.info("After replace - second call"); + + cache1.get("123"); + + logger.info("After cache1.get - third call"); + + cache2.get("123"); + + logger.info("After cache2.get - third call"); + + cache1 + .getAdvancedCache().withFlags(Flag.SKIP_CACHE_LOAD) + .entrySet().stream().forEach(e -> { + }); + + logger.info("After cache1.stream"); + + // Explicitly call put on remoteCache (KcRemoteCache.write ignores remote writes) + InfinispanUtil.getRemoteCache(cache1).put("123", session); + + // Create caches, listeners and finally worker threads + Thread worker1 = createWorker(cache1, 1); + Thread worker2 = createWorker(cache2, 2); + + long start = System.currentTimeMillis(); + + // Start and join workers + worker1.start(); + worker2.start(); + + worker1.join(); + worker2.join(); + + long took = System.currentTimeMillis() - start; + +// // Output +// for (Map.Entry entry : state.entrySet()) { +// System.out.println(entry.getKey() + ":::" + entry.getValue()); +// worker1.cache.remove(entry.getKey()); +// } + + System.out.println("Finished. Took: " + took + " ms. Notes: " + cache1.get("123").getEntity().getNotes().size() + + ", successfulListenerWrites: " + successfulListenerWrites.get() + ", successfulListenerWrites2: " + successfulListenerWrites2.get() + + ", failedReplaceCounter: " + failedReplaceCounter.get() + ", failedReplaceCounter2: " + failedReplaceCounter2.get() ); + + System.out.println("Sleeping before other report"); + + Thread.sleep(1000); + + System.out.println("Finished. Took: " + took + " ms. Notes: " + cache1.get("123").getEntity().getNotes().size() + + ", successfulListenerWrites: " + successfulListenerWrites.get() + ", successfulListenerWrites2: " + successfulListenerWrites2.get() + + ", failedReplaceCounter: " + failedReplaceCounter.get() + ", failedReplaceCounter2: " + failedReplaceCounter2.get()); + + + + // Finish JVM + cache1.getCacheManager().stop(); + cache2.getCacheManager().stop(); + } + + private static Thread createWorker(Cache> cache, int threadId) { + System.out.println("Retrieved cache: " + threadId); + + RemoteCache remoteCache = InfinispanUtil.getRemoteCache(cache); + + if (threadId == 1) { + remoteCache1 = remoteCache; + } else { + remoteCache2 = remoteCache; + } + + AtomicInteger counter = threadId ==1 ? successfulListenerWrites : successfulListenerWrites2; + HotRodListener listener = new HotRodListener(cache, remoteCache, counter); + remoteCache.addClientListener(listener); + + return new RemoteCacheWorker(remoteCache, threadId); + //return new CacheWorker(cache, threadId); + } + + + private static EmbeddedCacheManager createManager(int threadId) { + return new TestCacheManagerFactory().createManager(threadId, InfinispanConnectionProvider.SESSION_CACHE_NAME, KcRemoteStoreConfigurationBuilder.class); + } + + + @ClientListener + public static class HotRodListener { + + private Cache> origCache; + private RemoteCache remoteCache; + private AtomicInteger listenerCount; + + public HotRodListener(Cache> origCache, RemoteCache remoteCache, AtomicInteger listenerCount) { + this.listenerCount = listenerCount; + this.remoteCache = remoteCache; + this.origCache = origCache; + } + + @ClientCacheEntryCreated + public void created(ClientCacheEntryCreatedEvent event) { + String cacheKey = (String) event.getKey(); + listenerCount.incrementAndGet(); + } + + @ClientCacheEntryModified + public void updated(ClientCacheEntryModifiedEvent event) { + String cacheKey = (String) event.getKey(); + listenerCount.incrementAndGet(); + + // TODO: can be optimized + SessionEntity session = (SessionEntity) remoteCache.get(cacheKey); + SessionEntityWrapper sessionWrapper = new SessionEntityWrapper(session); + + // TODO: for distributed caches, ensure that it is executed just on owner OR if event.isCommandRetried + origCache + .getAdvancedCache().withFlags(Flag.SKIP_CACHE_LOAD, Flag.SKIP_CACHE_STORE) + .replace(cacheKey, sessionWrapper); + } + + + + + } + + private static class RemoteCacheWorker extends Thread { + + private final RemoteCache remoteCache; + + private final int myThreadId; + + private RemoteCacheWorker(RemoteCache remoteCache, int myThreadId) { + this.remoteCache = remoteCache; + this.myThreadId = myThreadId; + } + + @Override + public void run() { + + for (int i=0 ; i versioned = remoteCache.getVersioned("123"); + UserSessionEntity oldSession = versioned.getValue(); + //UserSessionEntity clone = DistributedCacheConcurrentWritesTest.cloneSession(oldSession); + UserSessionEntity clone = oldSession; + + clone.getNotes().put(noteKey, "someVal"); + //cache.replace("123", clone); + replaced = cacheReplace(versioned, clone); + } + + // Try to see if remoteCache on 2nd DC is immediatelly seeing our change + RemoteCache secondDCRemoteCache = myThreadId == 1 ? remoteCache2 : remoteCache1; + UserSessionEntity thatSession = (UserSessionEntity) secondDCRemoteCache.get("123"); + + Assert.assertEquals("someVal", thatSession.getNotes().get(noteKey)); + //System.out.println("Passed"); + } + + } + + private boolean cacheReplace(VersionedValue oldSession, UserSessionEntity newSession) { + try { + boolean replaced = remoteCache.replaceWithVersion("123", newSession, oldSession.getVersion()); + //cache.replace("123", newSession); + if (!replaced) { + failedReplaceCounter.incrementAndGet(); + //return false; + //System.out.println("Replace failed!!!"); + } + return replaced; + } catch (Exception re) { + failedReplaceCounter2.incrementAndGet(); + return false; + } + //return replaced; + } + + } +/* + // Worker, which operates on "classic" cache and rely on operations delegated to the second cache + private static class CacheWorker extends Thread { + + private final Cache> cache; + + private final int myThreadId; + + private CacheWorker(Cache> cache, int myThreadId) { + this.cache = cache; + this.myThreadId = myThreadId; + } + + @Override + public void run() { + + for (int i=0 ; i versioned = cache.getVersioned("123"); + UserSessionEntity oldSession = versioned.getValue(); + //UserSessionEntity clone = DistributedCacheConcurrentWritesTest.cloneSession(oldSession); + UserSessionEntity clone = oldSession; + + clone.getNotes().put(noteKey, "someVal"); + //cache.replace("123", clone); + replaced = cacheReplace(versioned, clone); + } + } + + } + + }*/ + + +} diff --git a/model/infinispan/src/test/java/org/keycloak/cluster/infinispan/TestCacheManagerFactory.java b/model/infinispan/src/test/java/org/keycloak/cluster/infinispan/TestCacheManagerFactory.java new file mode 100644 index 0000000000..06dd95fed5 --- /dev/null +++ b/model/infinispan/src/test/java/org/keycloak/cluster/infinispan/TestCacheManagerFactory.java @@ -0,0 +1,85 @@ +/* + * Copyright 2017 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.cluster.infinispan; + +import org.infinispan.configuration.cache.Configuration; +import org.infinispan.configuration.cache.ConfigurationBuilder; +import org.infinispan.configuration.global.GlobalConfigurationBuilder; +import org.infinispan.manager.DefaultCacheManager; +import org.infinispan.manager.EmbeddedCacheManager; +import org.infinispan.persistence.remote.configuration.ExhaustedAction; +import org.infinispan.persistence.remote.configuration.RemoteStoreConfigurationBuilder; + +/** + * @author Marek Posolda + */ +class TestCacheManagerFactory { + + + EmbeddedCacheManager createManager(int threadId, String cacheName, Class builderClass) { + System.setProperty("java.net.preferIPv4Stack", "true"); + System.setProperty("jgroups.tcp.port", "53715"); + GlobalConfigurationBuilder gcb = new GlobalConfigurationBuilder(); + + boolean clustered = false; + boolean async = false; + boolean allowDuplicateJMXDomains = true; + + if (clustered) { + gcb = gcb.clusteredDefault(); + gcb.transport().clusterName("test-clustering"); + } + + gcb.globalJmxStatistics().allowDuplicateDomains(allowDuplicateJMXDomains); + + EmbeddedCacheManager cacheManager = new DefaultCacheManager(gcb.build()); + + Configuration invalidationCacheConfiguration = getCacheBackedByRemoteStore(threadId, cacheName, builderClass); + + cacheManager.defineConfiguration(cacheName, invalidationCacheConfiguration); + return cacheManager; + + } + + + private Configuration getCacheBackedByRemoteStore(int threadId, String cacheName, Class builderClass) { + ConfigurationBuilder cacheConfigBuilder = new ConfigurationBuilder(); + + int port = threadId==1 ? 12232 : 13232; + //int port = 12232; + + return cacheConfigBuilder.persistence().addStore(builderClass) + .fetchPersistentState(false) + .ignoreModifications(false) + .purgeOnStartup(false) + .preload(false) + .shared(true) + .remoteCacheName(cacheName) + .rawValues(true) + .forceReturnValues(false) + .marshaller(KeycloakHotRodMarshallerFactory.class.getName()) + .addServer() + .host("localhost") + .port(port) + .connectionPool() + .maxActive(20) + .exhaustedAction(ExhaustedAction.CREATE_NEW) + .async() + . enabled(false).build(); + } +} diff --git a/model/infinispan/src/test/java/org/keycloak/models/sessions/infinispan/initializer/DistributedCacheConcurrentWritesTest.java b/model/infinispan/src/test/java/org/keycloak/models/sessions/infinispan/initializer/DistributedCacheConcurrentWritesTest.java new file mode 100644 index 0000000000..80bcd8ba4f --- /dev/null +++ b/model/infinispan/src/test/java/org/keycloak/models/sessions/infinispan/initializer/DistributedCacheConcurrentWritesTest.java @@ -0,0 +1,253 @@ +/* + * 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.models.sessions.infinispan.initializer; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; + +import org.infinispan.Cache; +import org.infinispan.configuration.cache.CacheMode; +import org.infinispan.configuration.cache.Configuration; +import org.infinispan.configuration.cache.ConfigurationBuilder; +import org.infinispan.configuration.global.GlobalConfigurationBuilder; +import org.infinispan.manager.DefaultCacheManager; +import org.infinispan.manager.EmbeddedCacheManager; +import org.infinispan.remoting.transport.jgroups.JGroupsTransport; +import org.jgroups.JChannel; +import org.junit.Ignore; +import org.keycloak.common.util.Time; +import org.keycloak.connections.infinispan.InfinispanConnectionProvider; +import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper; +import org.keycloak.models.sessions.infinispan.entities.AuthenticatedClientSessionEntity; +import org.keycloak.models.sessions.infinispan.entities.SessionEntity; +import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity; + +/** + * Test concurrent writes to distributed cache with usage of atomic replace + * + * @author Marek Posolda + */ +@Ignore +public class DistributedCacheConcurrentWritesTest { + + private static final int ITERATION_PER_WORKER = 1000; + + private static final AtomicInteger failedReplaceCounter = new AtomicInteger(0); + private static final AtomicInteger failedReplaceCounter2 = new AtomicInteger(0); + + public static void main(String[] args) throws Exception { + CacheWrapper cache1 = createCache("node1"); + CacheWrapper cache2 = createCache("node2"); + + // Create initial item + UserSessionEntity session = new UserSessionEntity(); + session.setId("123"); + session.setRealm("foo"); + session.setBrokerSessionId("!23123123"); + session.setBrokerUserId(null); + session.setUser("foo"); + session.setLoginUsername("foo"); + session.setIpAddress("123.44.143.178"); + session.setStarted(Time.currentTime()); + session.setLastSessionRefresh(Time.currentTime()); + + AuthenticatedClientSessionEntity clientSession = new AuthenticatedClientSessionEntity(); + clientSession.setAuthMethod("saml"); + clientSession.setAction("something"); + clientSession.setTimestamp(1234); + clientSession.setProtocolMappers(new HashSet<>(Arrays.asList("mapper1", "mapper2"))); + clientSession.setRoles(new HashSet<>(Arrays.asList("role1", "role2"))); + session.getAuthenticatedClientSessions().put("client1", clientSession); + + cache1.put("123", session); + + // Create 2 workers for concurrent write and start them + Worker worker1 = new Worker(1, cache1); + Worker worker2 = new Worker(2, cache2); + + long start = System.currentTimeMillis(); + + System.out.println("Started clustering test"); + + worker1.start(); + //worker1.join(); + worker2.start(); + + worker1.join(); + worker2.join(); + + long took = System.currentTimeMillis() - start; + session = cache1.get("123").getEntity(); + System.out.println("Took: " + took + " ms. Notes count: " + session.getNotes().size() + ", failedReplaceCounter: " + failedReplaceCounter.get() + + ", failedReplaceCounter2: " + failedReplaceCounter2.get()); + + // JGroups statistics + JChannel channel = (JChannel)((JGroupsTransport)cache1.wrappedCache.getAdvancedCache().getRpcManager().getTransport()).getChannel(); + System.out.println("Sent MB: " + channel.getSentBytes() / 1000000 + ", sent messages: " + channel.getSentMessages() + ", received MB: " + channel.getReceivedBytes() / 1000000 + + ", received messages: " + channel.getReceivedMessages()); + + // Kill JVM + cache1.getCache().stop(); + cache2.getCache().stop(); + cache1.getCache().getCacheManager().stop(); + cache2.getCache().getCacheManager().stop(); + + System.out.println("Managers killed"); + } + + + private static class Worker extends Thread { + + private final CacheWrapper cache; + private final int threadId; + + public Worker(int threadId, CacheWrapper cache) { + this.threadId = threadId; + this.cache = cache; + } + + @Override + public void run() { + + for (int i=0 ; i oldWrapped = cache.get("123"); + UserSessionEntity oldSession = oldWrapped.getEntity(); + //UserSessionEntity clone = DistributedCacheConcurrentWritesTest.cloneSession(oldSession); + UserSessionEntity clone = oldSession; + + clone.getNotes().put(noteKey, "someVal"); + //cache.replace("123", clone); + replaced = cacheReplace(oldWrapped, clone); + } + } + + } + + private boolean cacheReplace(SessionEntityWrapper oldSession, UserSessionEntity newSession) { + try { + boolean replaced = cache.replace("123", oldSession, newSession); + //cache.replace("123", newSession); + if (!replaced) { + failedReplaceCounter.incrementAndGet(); + //return false; + //System.out.println("Replace failed!!!"); + } + return replaced; + } catch (Exception re) { + failedReplaceCounter2.incrementAndGet(); + return false; + } + //return replaced; + } + + } + + // Session clone + + private static UserSessionEntity cloneSession(UserSessionEntity session) { + UserSessionEntity clone = new UserSessionEntity(); + clone.setId(session.getId()); + clone.setRealm(session.getRealm()); + clone.setNotes(new ConcurrentHashMap<>(session.getNotes())); + return clone; + } + + + // Cache creation utils + + public static class CacheWrapper { + + private final Cache> wrappedCache; + + public CacheWrapper(Cache> wrappedCache) { + this.wrappedCache = wrappedCache; + } + + + public SessionEntityWrapper get(K key) { + SessionEntityWrapper val = wrappedCache.get(key); + return val; + } + + public void put(K key, V newVal) { + SessionEntityWrapper newWrapper = new SessionEntityWrapper<>(newVal); + wrappedCache.put(key, newWrapper); + } + + + public boolean replace(K key, SessionEntityWrapper oldVal, V newVal) { + SessionEntityWrapper newWrapper = new SessionEntityWrapper<>(newVal); + return wrappedCache.replace(key, oldVal, newWrapper); + } + + private Cache> getCache() { + return wrappedCache; + } + + } + + + public static CacheWrapper createCache(String nodeName) { + EmbeddedCacheManager mgr = createManager(nodeName); + Cache> wrapped = mgr.getCache(InfinispanConnectionProvider.SESSION_CACHE_NAME); + return new CacheWrapper<>(wrapped); + } + + + public static EmbeddedCacheManager createManager(String nodeName) { + System.setProperty("java.net.preferIPv4Stack", "true"); + System.setProperty("jgroups.tcp.port", "53715"); + GlobalConfigurationBuilder gcb = new GlobalConfigurationBuilder(); + + boolean clustered = true; + boolean async = false; + boolean allowDuplicateJMXDomains = true; + + if (clustered) { + gcb = gcb.clusteredDefault(); + gcb.transport().clusterName("test-clustering"); + gcb.transport().nodeName(nodeName); + } + gcb.globalJmxStatistics().allowDuplicateDomains(allowDuplicateJMXDomains); + + EmbeddedCacheManager cacheManager = new DefaultCacheManager(gcb.build()); + + + ConfigurationBuilder distConfigBuilder = new ConfigurationBuilder(); + if (clustered) { + distConfigBuilder.clustering().cacheMode(async ? CacheMode.DIST_ASYNC : CacheMode.DIST_SYNC); + distConfigBuilder.clustering().hash().numOwners(1); + + // Disable L1 cache + distConfigBuilder.clustering().hash().l1().enabled(false); + } + Configuration distConfig = distConfigBuilder.build(); + + cacheManager.defineConfiguration(InfinispanConnectionProvider.SESSION_CACHE_NAME, distConfig); + return cacheManager; + + } +} diff --git a/model/infinispan/src/test/java/org/keycloak/models/sessions/infinispan/initializer/DistributedCacheWriteSkewTest.java b/model/infinispan/src/test/java/org/keycloak/models/sessions/infinispan/initializer/DistributedCacheWriteSkewTest.java new file mode 100644 index 0000000000..89e775078c --- /dev/null +++ b/model/infinispan/src/test/java/org/keycloak/models/sessions/infinispan/initializer/DistributedCacheWriteSkewTest.java @@ -0,0 +1,216 @@ +/* + * 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.models.sessions.infinispan.initializer; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import org.infinispan.Cache; +import org.infinispan.configuration.cache.CacheMode; +import org.infinispan.configuration.cache.Configuration; +import org.infinispan.configuration.cache.ConfigurationBuilder; +import org.infinispan.configuration.cache.VersioningScheme; +import org.infinispan.configuration.global.GlobalConfigurationBuilder; +import org.infinispan.context.Flag; +import org.infinispan.manager.DefaultCacheManager; +import org.infinispan.manager.EmbeddedCacheManager; +import org.infinispan.remoting.transport.jgroups.JGroupsTransport; +import org.infinispan.transaction.LockingMode; +import org.infinispan.transaction.lookup.DummyTransactionManagerLookup; +import org.infinispan.util.concurrent.IsolationLevel; +import org.jgroups.JChannel; +import org.junit.Ignore; +import org.keycloak.common.util.Time; +import org.keycloak.connections.infinispan.InfinispanConnectionProvider; +import org.keycloak.models.sessions.infinispan.entities.AuthenticatedClientSessionEntity; +import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity; + +/** + * Test concurrent writes to distributed cache with usage of write skew + * + * @author Marek Posolda + */ +@Ignore +public class DistributedCacheWriteSkewTest { + + private static final int ITERATION_PER_WORKER = 1000; + + private static final AtomicInteger failedReplaceCounter = new AtomicInteger(0); + + public static void main(String[] args) throws Exception { + Cache cache1 = createManager("node1").getCache(InfinispanConnectionProvider.SESSION_CACHE_NAME); + Cache cache2 = createManager("node2").getCache(InfinispanConnectionProvider.SESSION_CACHE_NAME); + + // Create initial item + UserSessionEntity session = new UserSessionEntity(); + session.setId("123"); + session.setRealm("foo"); + session.setBrokerSessionId("!23123123"); + session.setBrokerUserId(null); + session.setUser("foo"); + session.setLoginUsername("foo"); + session.setIpAddress("123.44.143.178"); + session.setStarted(Time.currentTime()); + session.setLastSessionRefresh(Time.currentTime()); + + AuthenticatedClientSessionEntity clientSession = new AuthenticatedClientSessionEntity(); + clientSession.setAuthMethod("saml"); + clientSession.setAction("something"); + clientSession.setTimestamp(1234); + clientSession.setProtocolMappers(new HashSet<>(Arrays.asList("mapper1", "mapper2"))); + clientSession.setRoles(new HashSet<>(Arrays.asList("role1", "role2"))); + session.getAuthenticatedClientSessions().put("client1", clientSession); + + cache1.put("123", session); + + //cache1.replace("123", session); + + // Create 2 workers for concurrent write and start them + Worker worker1 = new Worker(1, cache1); + Worker worker2 = new Worker(2, cache2); + + long start = System.currentTimeMillis(); + + System.out.println("Started clustering test"); + + worker1.start(); + //worker1.join(); + worker2.start(); + + worker1.join(); + worker2.join(); + + long took = System.currentTimeMillis() - start; + session = cache1.get("123"); + System.out.println("Took: " + took + " ms. Notes count: " + session.getNotes().size() + ", failedReplaceCounter: " + failedReplaceCounter.get()); + + // JGroups statistics + JChannel channel = (JChannel)((JGroupsTransport)cache1.getAdvancedCache().getRpcManager().getTransport()).getChannel(); + System.out.println("Sent MB: " + channel.getSentBytes() / 1000000 + ", sent messages: " + channel.getSentMessages() + ", received MB: " + channel.getReceivedBytes() / 1000000 + + ", received messages: " + channel.getReceivedMessages()); + + // Kill JVM + cache1.stop(); + cache2.stop(); + cache1.getCacheManager().stop(); + cache2.getCacheManager().stop(); + + System.out.println("Managers killed"); + } + + + private static class Worker extends Thread { + + private final Cache cache; + private final int threadId; + + public Worker(int threadId, Cache cache) { + this.threadId = threadId; + this.cache = cache; + } + + @Override + public void run() { + + for (int i=0 ; i keycloak-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../../pom.xml 4.0.0 diff --git a/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAPolicyStore.java b/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAPolicyStore.java index eb350be7ae..1bd41e2d86 100644 --- a/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAPolicyStore.java +++ b/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAPolicyStore.java @@ -24,6 +24,7 @@ import java.util.List; import java.util.Map; import javax.persistence.EntityManager; +import javax.persistence.FlushModeType; import javax.persistence.NoResultException; import javax.persistence.Query; import javax.persistence.TypedQuery; @@ -34,14 +35,10 @@ import javax.persistence.criteria.Root; import org.keycloak.authorization.AuthorizationProvider; import org.keycloak.authorization.jpa.entities.PolicyEntity; -import org.keycloak.authorization.jpa.entities.ResourceServerEntity; import org.keycloak.authorization.model.Policy; -import org.keycloak.authorization.model.Resource; import org.keycloak.authorization.model.ResourceServer; import org.keycloak.authorization.store.PolicyStore; -import org.keycloak.authorization.store.StoreFactory; import org.keycloak.models.utils.KeycloakModelUtils; -import org.keycloak.models.utils.RepresentationToModel; import org.keycloak.representations.idm.authorization.AbstractPolicyRepresentation; /** @@ -96,8 +93,10 @@ public class JPAPolicyStore implements PolicyStore { public Policy findByName(String name, String resourceServerId) { TypedQuery query = entityManager.createNamedQuery("findPolicyIdByName", String.class); + query.setFlushMode(FlushModeType.COMMIT); query.setParameter("serverId", resourceServerId); query.setParameter("name", name); + try { String id = query.getSingleResult(); return provider.getStoreFactory().getPolicyStore().findById(id, resourceServerId); @@ -167,6 +166,7 @@ public class JPAPolicyStore implements PolicyStore { public List findByResource(final String resourceId, String resourceServerId) { TypedQuery query = entityManager.createNamedQuery("findPolicyIdByResource", String.class); + query.setFlushMode(FlushModeType.COMMIT); query.setParameter("resourceId", resourceId); query.setParameter("serverId", resourceServerId); @@ -182,6 +182,7 @@ public class JPAPolicyStore implements PolicyStore { public List findByResourceType(final String resourceType, String resourceServerId) { TypedQuery query = entityManager.createNamedQuery("findPolicyIdByResourceType", String.class); + query.setFlushMode(FlushModeType.COMMIT); query.setParameter("type", resourceType); query.setParameter("serverId", resourceServerId); @@ -202,6 +203,7 @@ public class JPAPolicyStore implements PolicyStore { // Use separate subquery to handle DB2 and MSSSQL TypedQuery query = entityManager.createNamedQuery("findPolicyIdByScope", String.class); + query.setFlushMode(FlushModeType.COMMIT); query.setParameter("scopeIds", scopeIds); query.setParameter("serverId", resourceServerId); @@ -217,6 +219,7 @@ public class JPAPolicyStore implements PolicyStore { public List findByType(String type, String resourceServerId) { TypedQuery query = entityManager.createNamedQuery("findPolicyIdByType", String.class); + query.setFlushMode(FlushModeType.COMMIT); query.setParameter("serverId", resourceServerId); query.setParameter("type", type); @@ -233,6 +236,7 @@ public class JPAPolicyStore implements PolicyStore { TypedQuery query = entityManager.createNamedQuery("findPolicyIdByDependentPolices", String.class); + query.setFlushMode(FlushModeType.COMMIT); query.setParameter("serverId", resourceServerId); query.setParameter("policyId", policyId); diff --git a/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAResourceStore.java b/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAResourceStore.java index 8a647d8a85..7a505abf34 100644 --- a/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAResourceStore.java +++ b/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAResourceStore.java @@ -19,13 +19,13 @@ package org.keycloak.authorization.jpa.store; import org.keycloak.authorization.AuthorizationProvider; import org.keycloak.authorization.jpa.entities.ResourceEntity; -import org.keycloak.authorization.jpa.entities.ResourceServerEntity; import org.keycloak.authorization.model.Resource; import org.keycloak.authorization.model.ResourceServer; import org.keycloak.authorization.store.ResourceStore; import org.keycloak.models.utils.KeycloakModelUtils; import javax.persistence.EntityManager; +import javax.persistence.FlushModeType; import javax.persistence.NoResultException; import javax.persistence.Query; import javax.persistence.TypedQuery; @@ -34,7 +34,6 @@ import javax.persistence.criteria.CriteriaQuery; import javax.persistence.criteria.Predicate; import javax.persistence.criteria.Root; import java.util.ArrayList; -import java.util.Arrays; import java.util.LinkedList; import java.util.List; import java.util.Map; @@ -62,13 +61,14 @@ public class JPAResourceStore implements ResourceStore { entity.setOwner(owner); this.entityManager.persist(entity); + this.entityManager.flush(); return new ResourceAdapter(entity, entityManager, provider.getStoreFactory()); } @Override public void delete(String id) { - ResourceEntity resource = entityManager.find(ResourceEntity.class, id); + ResourceEntity resource = entityManager.getReference(ResourceEntity.class, id); if (resource == null) return; resource.getScopes().clear(); @@ -90,14 +90,18 @@ public class JPAResourceStore implements ResourceStore { public List findByOwner(String ownerId, String resourceServerId) { TypedQuery query = entityManager.createNamedQuery("findResourceIdByOwner", String.class); + query.setFlushMode(FlushModeType.COMMIT); query.setParameter("owner", ownerId); query.setParameter("serverId", resourceServerId); List result = query.getResultList(); List list = new LinkedList<>(); + ResourceStore resourceStore = provider.getStoreFactory().getResourceStore(); + for (String id : result) { - list.add(provider.getStoreFactory().getResourceStore().findById(id, resourceServerId)); + list.add(resourceStore.findById(id, resourceServerId)); } + return list; } @@ -105,14 +109,18 @@ public class JPAResourceStore implements ResourceStore { public List findByUri(String uri, String resourceServerId) { TypedQuery query = entityManager.createNamedQuery("findResourceIdByUri", String.class); + query.setFlushMode(FlushModeType.COMMIT); query.setParameter("uri", uri); query.setParameter("serverId", resourceServerId); List result = query.getResultList(); List list = new LinkedList<>(); + ResourceStore resourceStore = provider.getStoreFactory().getResourceStore(); + for (String id : result) { - list.add(provider.getStoreFactory().getResourceStore().findById(id, resourceServerId)); + list.add(resourceStore.findById(id, resourceServerId)); } + return list; } @@ -124,9 +132,12 @@ public class JPAResourceStore implements ResourceStore { List result = query.getResultList(); List list = new LinkedList<>(); + ResourceStore resourceStore = provider.getStoreFactory().getResourceStore(); + for (String id : result) { - list.add(provider.getStoreFactory().getResourceStore().findById(id, resourceServerId)); + list.add(resourceStore.findById(id, resourceServerId)); } + return list; } @@ -163,9 +174,12 @@ public class JPAResourceStore implements ResourceStore { List result = query.getResultList(); List list = new LinkedList<>(); + ResourceStore resourceStore = provider.getStoreFactory().getResourceStore(); + for (String id : result) { - list.add(provider.getStoreFactory().getResourceStore().findById(id, resourceServerId)); + list.add(resourceStore.findById(id, resourceServerId)); } + return list; } @@ -173,14 +187,18 @@ public class JPAResourceStore implements ResourceStore { public List findByScope(List scopes, String resourceServerId) { TypedQuery query = entityManager.createNamedQuery("findResourceIdByScope", String.class); + query.setFlushMode(FlushModeType.COMMIT); query.setParameter("scopeIds", scopes); query.setParameter("serverId", resourceServerId); List result = query.getResultList(); List list = new LinkedList<>(); + ResourceStore resourceStore = provider.getStoreFactory().getResourceStore(); + for (String id : result) { - list.add(provider.getStoreFactory().getResourceStore().findById(id, resourceServerId)); + list.add(resourceStore.findById(id, resourceServerId)); } + return list; } @@ -188,8 +206,10 @@ public class JPAResourceStore implements ResourceStore { public Resource findByName(String name, String resourceServerId) { TypedQuery query = entityManager.createNamedQuery("findResourceIdByName", String.class); + query.setFlushMode(FlushModeType.COMMIT); query.setParameter("serverId", resourceServerId); query.setParameter("name", name); + try { String id = query.getSingleResult(); return provider.getStoreFactory().getResourceStore().findById(id, resourceServerId); @@ -202,14 +222,18 @@ public class JPAResourceStore implements ResourceStore { public List findByType(String type, String resourceServerId) { TypedQuery query = entityManager.createNamedQuery("findResourceIdByType", String.class); + query.setFlushMode(FlushModeType.COMMIT); query.setParameter("type", type); query.setParameter("serverId", resourceServerId); List result = query.getResultList(); List list = new LinkedList<>(); + ResourceStore resourceStore = provider.getStoreFactory().getResourceStore(); + for (String id : result) { - list.add(provider.getStoreFactory().getResourceStore().findById(id, resourceServerId)); + list.add(resourceStore.findById(id, resourceServerId)); } + return list; } } diff --git a/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAScopeStore.java b/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAScopeStore.java index f8a9350442..befde658ac 100644 --- a/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAScopeStore.java +++ b/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAScopeStore.java @@ -23,6 +23,7 @@ import java.util.List; import java.util.Map; import javax.persistence.EntityManager; +import javax.persistence.FlushModeType; import javax.persistence.NoResultException; import javax.persistence.Query; import javax.persistence.TypedQuery; @@ -32,7 +33,6 @@ import javax.persistence.criteria.Predicate; import javax.persistence.criteria.Root; import org.keycloak.authorization.AuthorizationProvider; -import org.keycloak.authorization.jpa.entities.ResourceServerEntity; import org.keycloak.authorization.jpa.entities.ScopeEntity; import org.keycloak.authorization.model.ResourceServer; import org.keycloak.authorization.model.Scope; @@ -61,6 +61,7 @@ public class JPAScopeStore implements ScopeStore { entity.setResourceServer(ResourceServerAdapter.toEntity(entityManager, resourceServer)); this.entityManager.persist(entity); + this.entityManager.flush(); return new ScopeAdapter(entity, entityManager, provider.getStoreFactory()); } @@ -91,8 +92,10 @@ public class JPAScopeStore implements ScopeStore { try { TypedQuery query = entityManager.createNamedQuery("findScopeIdByName", String.class); + query.setFlushMode(FlushModeType.COMMIT); query.setParameter("serverId", resourceServerId); query.setParameter("name", name); + String id = query.getSingleResult(); return provider.getStoreFactory().getScopeStore().findById(id, resourceServerId); } catch (NoResultException nre) { @@ -104,6 +107,7 @@ public class JPAScopeStore implements ScopeStore { public List findByResourceServer(final String serverId) { TypedQuery query = entityManager.createNamedQuery("findScopeIdByResourceServer", String.class); + query.setFlushMode(FlushModeType.COMMIT); query.setParameter("serverId", serverId); List result = query.getResultList(); diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java index b71a493d01..05d7517af5 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java @@ -1335,6 +1335,18 @@ public class RealmAdapter implements RealmModel, JpaModel { realm.setClientAuthenticationFlow(flow.getId()); } + @Override + public AuthenticationFlowModel getDockerAuthenticationFlow() { + String flowId = realm.getDockerAuthenticationFlow(); + if (flowId == null) return null; + return getAuthenticationFlowById(flowId); + } + + @Override + public void setDockerAuthenticationFlow(AuthenticationFlowModel flow) { + realm.setDockerAuthenticationFlow(flow.getId()); + } + @Override public List getAuthenticationFlows() { return realm.getAuthenticationFlows().stream() diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmEntity.java index 13988dcd12..33578e3156 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmEntity.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmEntity.java @@ -220,6 +220,8 @@ public class RealmEntity { @Column(name="CLIENT_AUTH_FLOW") protected String clientAuthenticationFlow; + @Column(name="DOCKER_AUTH_FLOW") + protected String dockerAuthenticationFlow; @Column(name="INTERNATIONALIZATION_ENABLED") @@ -733,6 +735,15 @@ public class RealmEntity { this.clientAuthenticationFlow = clientAuthenticationFlow; } + public String getDockerAuthenticationFlow() { + return dockerAuthenticationFlow; + } + + public RealmEntity setDockerAuthenticationFlow(String dockerAuthenticationFlow) { + this.dockerAuthenticationFlow = dockerAuthenticationFlow; + return this; + } + public Collection getClientTemplates() { return clientTemplates; } diff --git a/model/jpa/src/main/resources/META-INF/jpa-changelog-3.2.0.xml b/model/jpa/src/main/resources/META-INF/jpa-changelog-3.2.0.xml index bd55645295..daa1c5040f 100644 --- a/model/jpa/src/main/resources/META-INF/jpa-changelog-3.2.0.xml +++ b/model/jpa/src/main/resources/META-INF/jpa-changelog-3.2.0.xml @@ -15,10 +15,14 @@ ~ See the License for the specific language governing permissions and ~ limitations under the License. --> - + + + + + + - @@ -38,9 +42,6 @@ - - - diff --git a/model/pom.xml b/model/pom.xml index 944ec515ab..43f383425c 100755 --- a/model/pom.xml +++ b/model/pom.xml @@ -20,7 +20,7 @@ keycloak-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../pom.xml Keycloak Model Parent diff --git a/pom.xml b/pom.xml index d5b108d6fe..8b216dcbef 100755 --- a/pom.xml +++ b/pom.xml @@ -33,7 +33,7 @@ org.keycloak keycloak-parent - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT pom @@ -45,7 +45,7 @@ 7.2.0.Final 11.0.0.Alpha1 1.2.2.Final - 7.1.0.Beta1-redhat-2 + 7.1.0.Beta1-redhat-5 1.2.2.Final 3.0.0.Beta11 @@ -80,6 +80,7 @@ 2.2.11 20140925 1.4.11.Final + 5.0.3 2.0.5 @@ -123,6 +124,7 @@ 2.3.7 1.1.0.Final 1.6.5 + 1.5 -Xms512m -Xmx2048m -XX:MetaspaceSize=96m -XX:MaxMetaspaceSize=256m @@ -1500,6 +1502,11 @@ maven-bundle-plugin ${osgi.bundle.plugin.version} + + com.github.eirslett + frontend-maven-plugin + ${frontend.plugin.version} + diff --git a/proxy/launcher/pom.xml b/proxy/launcher/pom.xml index dbb2a6a609..e3a71d1ab6 100755 --- a/proxy/launcher/pom.xml +++ b/proxy/launcher/pom.xml @@ -21,7 +21,7 @@ keycloak-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../../pom.xml 4.0.0 diff --git a/proxy/pom.xml b/proxy/pom.xml index 081e32059b..8858109d76 100755 --- a/proxy/pom.xml +++ b/proxy/pom.xml @@ -20,7 +20,7 @@ keycloak-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../pom.xml Keycloak Proxy diff --git a/proxy/proxy-server/pom.xml b/proxy/proxy-server/pom.xml index 450a408687..be58cd6e93 100755 --- a/proxy/proxy-server/pom.xml +++ b/proxy/proxy-server/pom.xml @@ -21,7 +21,7 @@ keycloak-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../../pom.xml 4.0.0 diff --git a/saml-core-api/pom.xml b/saml-core-api/pom.xml index d2c1f991b3..bd33054aa1 100755 --- a/saml-core-api/pom.xml +++ b/saml-core-api/pom.xml @@ -21,7 +21,7 @@ keycloak-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../pom.xml 4.0.0 diff --git a/saml-core/pom.xml b/saml-core/pom.xml index 100b6efe42..8ed8de1be8 100755 --- a/saml-core/pom.xml +++ b/saml-core/pom.xml @@ -21,7 +21,7 @@ keycloak-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../pom.xml 4.0.0 diff --git a/saml-core/src/main/java/org/keycloak/saml/BaseSAML2BindingBuilder.java b/saml-core/src/main/java/org/keycloak/saml/BaseSAML2BindingBuilder.java index 86b3ecb21e..be74b74de2 100755 --- a/saml-core/src/main/java/org/keycloak/saml/BaseSAML2BindingBuilder.java +++ b/saml-core/src/main/java/org/keycloak/saml/BaseSAML2BindingBuilder.java @@ -345,7 +345,7 @@ public class BaseSAML2BindingBuilder { logger.debugv("saml document: {0}", documentAsString); byte[] responseBytes = documentAsString.getBytes(GeneralConstants.SAML_CHARSET); - return RedirectBindingUtil.deflateBase64URLEncode(responseBytes); + return RedirectBindingUtil.deflateBase64Encode(responseBytes); } @@ -370,7 +370,7 @@ public class BaseSAML2BindingBuilder { } catch (InvalidKeyException | SignatureException e) { throw new ProcessingException(e); } - String encodedSig = RedirectBindingUtil.base64URLEncode(sig); + String encodedSig = RedirectBindingUtil.base64Encode(sig); builder.queryParam(GeneralConstants.SAML_SIGNATURE_REQUEST_KEY, encodedSig); } return builder.build(); diff --git a/saml-core/src/main/java/org/keycloak/saml/SAMLRequestParser.java b/saml-core/src/main/java/org/keycloak/saml/SAMLRequestParser.java index 00160e6e53..335a72d11e 100755 --- a/saml-core/src/main/java/org/keycloak/saml/SAMLRequestParser.java +++ b/saml-core/src/main/java/org/keycloak/saml/SAMLRequestParser.java @@ -56,10 +56,8 @@ public class SAMLRequestParser { is = new ByteArrayInputStream(message.getBytes(GeneralConstants.SAML_CHARSET)); } - SAML2Request saml2Request = new SAML2Request(); try { - saml2Request.getSAML2ObjectFromStream(is); - return saml2Request.getSamlDocumentHolder(); + return SAML2Request.getSAML2ObjectFromStream(is); } catch (Exception e) { logger.samlBase64DecodingError(e); } @@ -76,10 +74,8 @@ public class SAMLRequestParser { log.debug(str); } is = new ByteArrayInputStream(samlBytes); - SAML2Request saml2Request = new SAML2Request(); try { - saml2Request.getSAML2ObjectFromStream(is); - return saml2Request.getSamlDocumentHolder(); + return SAML2Request.getSAML2ObjectFromStream(is); } catch (Exception e) { logger.samlBase64DecodingError(e); } diff --git a/saml-core/src/main/java/org/keycloak/saml/SPMetadataDescriptor.java b/saml-core/src/main/java/org/keycloak/saml/SPMetadataDescriptor.java index 5feda2b568..b020eb73f3 100755 --- a/saml-core/src/main/java/org/keycloak/saml/SPMetadataDescriptor.java +++ b/saml-core/src/main/java/org/keycloak/saml/SPMetadataDescriptor.java @@ -23,14 +23,19 @@ package org.keycloak.saml; */ public class SPMetadataDescriptor { - public static String getSPDescriptor(String binding, String assertionEndpoint, String logoutEndpoint, boolean wantAuthnRequestsSigned, boolean wantAssertionsSigned, String entityId, String nameIDPolicyFormat, String signingCerts) { + public static String getSPDescriptor(String binding, String assertionEndpoint, String logoutEndpoint, + boolean wantAuthnRequestsSigned, boolean wantAssertionsSigned, boolean wantAssertionsEncrypted, + String entityId, String nameIDPolicyFormat, String signingCerts, String encryptionCerts) { String descriptor = "\n" + " \n"; - if (wantAuthnRequestsSigned && signingCerts != null) { + if (wantAuthnRequestsSigned && signingCerts != null) { descriptor += signingCerts; } + if (wantAssertionsEncrypted && encryptionCerts != null) { + descriptor += encryptionCerts; + } descriptor += " \n" + " " + nameIDPolicyFormat + "\n" + diff --git a/saml-core/src/main/java/org/keycloak/saml/common/util/DocumentUtil.java b/saml-core/src/main/java/org/keycloak/saml/common/util/DocumentUtil.java index f516124f7b..717732274e 100755 --- a/saml-core/src/main/java/org/keycloak/saml/common/util/DocumentUtil.java +++ b/saml-core/src/main/java/org/keycloak/saml/common/util/DocumentUtil.java @@ -511,7 +511,7 @@ public class DocumentUtil { }; - private static DocumentBuilder getDocumentBuilder() throws ParserConfigurationException { + public static DocumentBuilder getDocumentBuilder() throws ParserConfigurationException { DocumentBuilder res = XML_DOCUMENT_BUILDER.get(); res.reset(); return res; diff --git a/saml-core/src/main/java/org/keycloak/saml/processing/api/saml/v2/request/SAML2Request.java b/saml-core/src/main/java/org/keycloak/saml/processing/api/saml/v2/request/SAML2Request.java index 2bfa41f9db..cb8a348631 100755 --- a/saml-core/src/main/java/org/keycloak/saml/processing/api/saml/v2/request/SAML2Request.java +++ b/saml-core/src/main/java/org/keycloak/saml/processing/api/saml/v2/request/SAML2Request.java @@ -156,7 +156,7 @@ public class SAML2Request { * @throws IOException * @throws ParsingException */ - public SAML2Object getSAML2ObjectFromStream(InputStream is) throws ConfigurationException, ParsingException, + public static SAMLDocumentHolder getSAML2ObjectFromStream(InputStream is) throws ConfigurationException, ParsingException, ProcessingException { if (is == null) throw logger.nullArgumentError("InputStream"); @@ -167,8 +167,7 @@ public class SAML2Request { JAXPValidationUtil.checkSchemaValidation(samlDocument); SAML2Object requestType = (SAML2Object) samlParser.parse(samlDocument); - samlDocumentHolder = new SAMLDocumentHolder(requestType, samlDocument); - return requestType; + return new SAMLDocumentHolder(requestType, samlDocument); } /** diff --git a/saml-core/src/main/java/org/keycloak/saml/processing/core/saml/v2/writers/SAMLAssertionWriter.java b/saml-core/src/main/java/org/keycloak/saml/processing/core/saml/v2/writers/SAMLAssertionWriter.java index 867aceb2fd..41461bf889 100755 --- a/saml-core/src/main/java/org/keycloak/saml/processing/core/saml/v2/writers/SAMLAssertionWriter.java +++ b/saml-core/src/main/java/org/keycloak/saml/processing/core/saml/v2/writers/SAMLAssertionWriter.java @@ -47,6 +47,7 @@ import java.net.URI; import java.util.List; import java.util.Set; +import javax.xml.crypto.dsig.XMLSignature; import static org.keycloak.saml.common.constants.JBossSAMLURIConstants.ASSERTION_NSURI; /** @@ -69,8 +70,17 @@ public class SAMLAssertionWriter extends BaseWriter { * @throws org.keycloak.saml.common.exceptions.ProcessingException */ public void write(AssertionType assertion) throws ProcessingException { + write(assertion, false); + } + + public void write(AssertionType assertion, boolean forceWriteDsigNamespace) throws ProcessingException { + Element sig = assertion.getSignature(); + StaxUtil.writeStartElement(writer, ASSERTION_PREFIX, JBossSAMLConstants.ASSERTION.get(), ASSERTION_NSURI.get()); StaxUtil.writeNameSpace(writer, ASSERTION_PREFIX, ASSERTION_NSURI.get()); + if (forceWriteDsigNamespace && sig != null && sig.getPrefix() != null && ! sig.hasAttribute("xmlns:" + sig.getPrefix())) { + StaxUtil.writeNameSpace(writer, sig.getPrefix(), XMLSignature.XMLNS); + } StaxUtil.writeDefaultNameSpace(writer, ASSERTION_NSURI.get()); // Attributes @@ -82,7 +92,6 @@ public class SAMLAssertionWriter extends BaseWriter { if (issuer != null) write(issuer, new QName(ASSERTION_NSURI.get(), JBossSAMLConstants.ISSUER.get(), ASSERTION_PREFIX)); - Element sig = assertion.getSignature(); if (sig != null) StaxUtil.writeDOMElement(writer, sig); diff --git a/saml-core/src/main/java/org/keycloak/saml/processing/core/saml/v2/writers/SAMLResponseWriter.java b/saml-core/src/main/java/org/keycloak/saml/processing/core/saml/v2/writers/SAMLResponseWriter.java index 9327a73651..d2a59b94f7 100755 --- a/saml-core/src/main/java/org/keycloak/saml/processing/core/saml/v2/writers/SAMLResponseWriter.java +++ b/saml-core/src/main/java/org/keycloak/saml/processing/core/saml/v2/writers/SAMLResponseWriter.java @@ -38,6 +38,7 @@ import javax.xml.stream.XMLStreamWriter; import java.net.URI; import java.util.List; import org.keycloak.dom.saml.v2.protocol.ExtensionsType; +import javax.xml.crypto.dsig.XMLSignature; /** * Write a SAML Response to stream @@ -63,8 +64,17 @@ public class SAMLResponseWriter extends BaseWriter { * @throws org.keycloak.saml.common.exceptions.ProcessingException */ public void write(ResponseType response) throws ProcessingException { + write(response, false); + } + + public void write(ResponseType response, boolean forceWriteDsigNamespace) throws ProcessingException { + Element sig = response.getSignature(); + StaxUtil.writeStartElement(writer, PROTOCOL_PREFIX, JBossSAMLConstants.RESPONSE.get(), JBossSAMLURIConstants.PROTOCOL_NSURI.get()); + if (forceWriteDsigNamespace && sig != null && sig.getPrefix() != null && ! sig.hasAttribute("xmlns:" + sig.getPrefix())) { + StaxUtil.writeNameSpace(writer, sig.getPrefix(), XMLSignature.XMLNS); + } StaxUtil.writeNameSpace(writer, PROTOCOL_PREFIX, JBossSAMLURIConstants.PROTOCOL_NSURI.get()); StaxUtil.writeNameSpace(writer, ASSERTION_PREFIX, JBossSAMLURIConstants.ASSERTION_NSURI.get()); @@ -75,7 +85,6 @@ public class SAMLResponseWriter extends BaseWriter { write(issuer, new QName(JBossSAMLURIConstants.ASSERTION_NSURI.get(), JBossSAMLConstants.ISSUER.get(), ASSERTION_PREFIX)); } - Element sig = response.getSignature(); if (sig != null) { StaxUtil.writeDOMElement(writer, sig); } diff --git a/saml-core/src/main/java/org/keycloak/saml/processing/web/util/RedirectBindingUtil.java b/saml-core/src/main/java/org/keycloak/saml/processing/web/util/RedirectBindingUtil.java index 587113c5b5..9c0938fc52 100755 --- a/saml-core/src/main/java/org/keycloak/saml/processing/web/util/RedirectBindingUtil.java +++ b/saml-core/src/main/java/org/keycloak/saml/processing/web/util/RedirectBindingUtil.java @@ -60,6 +60,19 @@ public class RedirectBindingUtil { return URLDecoder.decode(str, GeneralConstants.SAML_CHARSET_NAME); } + /** + * On the byte array, apply base64 encoding + * + * @param stringToEncode + * + * @return + * + * @throws IOException + */ + public static String base64Encode(byte[] stringToEncode) throws IOException { + return Base64.encodeBytes(stringToEncode, Base64.DONT_BREAK_LINES); + } + /** * On the byte array, apply base64 encoding following by URL encoding * diff --git a/server-spi-private/pom.xml b/server-spi-private/pom.xml index 0e9a511cfa..3d3f430c07 100755 --- a/server-spi-private/pom.xml +++ b/server-spi-private/pom.xml @@ -21,7 +21,7 @@ keycloak-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../pom.xml 4.0.0 diff --git a/server-spi-private/src/main/java/org/keycloak/cluster/ClusterProvider.java b/server-spi-private/src/main/java/org/keycloak/cluster/ClusterProvider.java index abed174520..c1d59c5c0e 100644 --- a/server-spi-private/src/main/java/org/keycloak/cluster/ClusterProvider.java +++ b/server-spi-private/src/main/java/org/keycloak/cluster/ClusterProvider.java @@ -21,6 +21,7 @@ package org.keycloak.cluster; import org.keycloak.provider.Provider; import java.util.concurrent.Callable; +import java.util.concurrent.Future; /** * Various utils related to clustering and concurrent tasks on cluster nodes @@ -47,9 +48,21 @@ public interface ClusterProvider extends Provider { ExecutionResult executeIfNotExecuted(String taskKey, int taskTimeoutInSeconds, Callable task); + /** + * Execute given task just if it's not already in progress (either on this or any other cluster node). It will return corresponding future to every caller and this future is fulfilled if: + * - The task is successfully finished. In that case Future will be true + * - The task wasn't successfully finished. For example because cluster node failover. In that case Future will be false + * + * @param taskKey + * @param taskTimeoutInSeconds timeout for given task. If there is existing task in progress for longer time, it's considered outdated so we will start our task. + * @param task + * @return Future, which will be completed once the running task is finished. Returns true if task was successfully finished. Otherwise (for example if cluster node when task was running leaved cluster) returns false + */ + Future executeIfNotExecutedAsync(String taskKey, int taskTimeoutInSeconds, Callable task); + + /** * Register task (listener) under given key. When this key will be put to the cache on any cluster node, the task will be executed. - * When using {@link #ALL} as the taskKey, then listener will be always triggered for any value put into the cache. * * @param taskKey * @param task @@ -58,18 +71,24 @@ public interface ClusterProvider extends Provider { /** - * Notify registered listeners on all cluster nodes. It will notify listeners registered under given taskKey AND also listeners registered with {@link #ALL} key (those are always executed) + * Notify registered listeners on all cluster nodes in all datacenters. It will notify listeners registered under given taskKey * * @param taskKey * @param event * @param ignoreSender if true, then sender node itself won't receive the notification + * @param dcNotify Specify which DCs to notify. See {@link DCNotify} enum values for more info */ - void notify(String taskKey, ClusterEvent event, boolean ignoreSender); + void notify(String taskKey, ClusterEvent event, boolean ignoreSender, DCNotify dcNotify); + enum DCNotify { + /** Send message to all cluster nodes in all DCs **/ + ALL_DCS, + + /** Send message to all cluster nodes on THIS datacenter only **/ + LOCAL_DC_ONLY, + + /** Send message to all cluster nodes in all datacenters, but NOT to this datacenter. Option "ignoreSender" of method {@link #notify} will be ignored as sender is ignored anyway due it is in this datacenter **/ + ALL_BUT_LOCAL_DC + } - /** - * Special value to be used with {@link #registerListener} to specify that particular listener will be always triggered for all notifications - * with any key. - */ - String ALL = "ALL"; } diff --git a/server-spi-private/src/main/java/org/keycloak/email/EmailSenderProvider.java b/server-spi-private/src/main/java/org/keycloak/email/EmailSenderProvider.java index 7ea2b49cdb..a12b028e95 100755 --- a/server-spi-private/src/main/java/org/keycloak/email/EmailSenderProvider.java +++ b/server-spi-private/src/main/java/org/keycloak/email/EmailSenderProvider.java @@ -17,15 +17,15 @@ package org.keycloak.email; -import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.provider.Provider; +import java.util.Map; + /** * @author Stian Thorgersen */ public interface EmailSenderProvider extends Provider { - void send(RealmModel realm, UserModel user, String subject, String textBody, String htmlBody) throws EmailException; - + void send(Map config, UserModel user, String subject, String textBody, String htmlBody) throws EmailException; } diff --git a/server-spi-private/src/main/java/org/keycloak/email/EmailTemplateProvider.java b/server-spi-private/src/main/java/org/keycloak/email/EmailTemplateProvider.java index 1cc6151d6b..da245fcd76 100755 --- a/server-spi-private/src/main/java/org/keycloak/email/EmailTemplateProvider.java +++ b/server-spi-private/src/main/java/org/keycloak/email/EmailTemplateProvider.java @@ -22,6 +22,8 @@ import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.provider.Provider; +import java.util.Map; + /** * @author Stian Thorgersen */ @@ -46,6 +48,15 @@ public interface EmailTemplateProvider extends Provider { */ public void sendPasswordReset(String link, long expirationInMinutes) throws EmailException; + /** + * Test SMTP connection with current logged in user + * + * @param config SMTP server configuration + * @param user SMTP recipient + * @throws EmailException + */ + public void sendSmtpTestEmail(Map config, UserModel user) throws EmailException; + /** * Send to confirm that user wants to link his account with identity broker link */ diff --git a/server-spi-private/src/main/java/org/keycloak/events/EventType.java b/server-spi-private/src/main/java/org/keycloak/events/EventType.java index 920646fa58..b48e2433a7 100755 --- a/server-spi-private/src/main/java/org/keycloak/events/EventType.java +++ b/server-spi-private/src/main/java/org/keycloak/events/EventType.java @@ -123,7 +123,9 @@ public enum EventType { CLIENT_DELETE_ERROR(true), CLIENT_INITIATED_ACCOUNT_LINKING(true), - CLIENT_INITIATED_ACCOUNT_LINKING_ERROR(true); + CLIENT_INITIATED_ACCOUNT_LINKING_ERROR(true), + TOKEN_EXCHANGE(true), + TOKEN_EXCHANGE_ERROR(true); private boolean saveByDefault; diff --git a/server-spi-private/src/main/java/org/keycloak/migration/migrators/MigrateTo3_2_0.java b/server-spi-private/src/main/java/org/keycloak/migration/migrators/MigrateTo3_2_0.java index 17cd0ac53c..98686af7a0 100644 --- a/server-spi-private/src/main/java/org/keycloak/migration/migrators/MigrateTo3_2_0.java +++ b/server-spi-private/src/main/java/org/keycloak/migration/migrators/MigrateTo3_2_0.java @@ -27,11 +27,8 @@ import org.keycloak.migration.ModelVersion; import org.keycloak.models.KeycloakSession; import org.keycloak.models.PasswordPolicy; import org.keycloak.models.RealmModel; +import org.keycloak.models.utils.DefaultAuthenticationFlows; -/** - * @author Bill Burke - * @version $Revision: 1 $ - */ public class MigrateTo3_2_0 implements Migration { public static final ModelVersion VERSION = new ModelVersion("3.2.0"); @@ -44,6 +41,10 @@ public class MigrateTo3_2_0 implements Migration { realm.setPasswordPolicy(builder.remove(PasswordPolicy.HASH_ITERATIONS_ID).build(session)); } + if (realm.getDockerAuthenticationFlow() == null) { + DefaultAuthenticationFlows.dockerAuthenticationFlow(realm); + } + ClientModel realmAccess = realm.getClientByClientId(Constants.REALM_MANAGEMENT_CLIENT_ID); if (realmAccess != null) { addRoles(realmAccess); diff --git a/server-spi-private/src/main/java/org/keycloak/models/Constants.java b/server-spi-private/src/main/java/org/keycloak/models/Constants.java index 40f9081b77..8ff0966b9b 100755 --- a/server-spi-private/src/main/java/org/keycloak/models/Constants.java +++ b/server-spi-private/src/main/java/org/keycloak/models/Constants.java @@ -57,6 +57,8 @@ public interface Constants { String KEY = "key"; String SKIP_LINK = "skipLink"; + String TEMPLATE_ATTR_ACTION_URI = "actionUri"; + String TEMPLATE_ATTR_REQUIRED_ACTIONS = "requiredActions"; // Prefix for user attributes used in various "context"data maps String USER_ATTRIBUTES_PREFIX = "user.attributes."; diff --git a/server-spi-private/src/main/java/org/keycloak/models/session/PersistentUserSessionAdapter.java b/server-spi-private/src/main/java/org/keycloak/models/session/PersistentUserSessionAdapter.java index 23436e05e0..6bea75f7fb 100644 --- a/server-spi-private/src/main/java/org/keycloak/models/session/PersistentUserSessionAdapter.java +++ b/server-spi-private/src/main/java/org/keycloak/models/session/PersistentUserSessionAdapter.java @@ -115,11 +115,6 @@ public class PersistentUserSessionAdapter implements UserSessionModel { return user; } - @Override - public void setUser(UserModel user) { - throw new IllegalStateException("Not supported"); - } - @Override public RealmModel getRealm() { return realm; diff --git a/server-spi-private/src/main/java/org/keycloak/models/utils/DefaultAuthenticationFlows.java b/server-spi-private/src/main/java/org/keycloak/models/utils/DefaultAuthenticationFlows.java index b02881406e..8030da6c97 100755 --- a/server-spi-private/src/main/java/org/keycloak/models/utils/DefaultAuthenticationFlows.java +++ b/server-spi-private/src/main/java/org/keycloak/models/utils/DefaultAuthenticationFlows.java @@ -42,6 +42,7 @@ public class DefaultAuthenticationFlows { public static final String RESET_CREDENTIALS_FLOW = "reset credentials"; public static final String LOGIN_FORMS_FLOW = "forms"; public static final String SAML_ECP_FLOW = "saml ecp"; + public static final String DOCKER_AUTH = "docker auth"; public static final String CLIENT_AUTHENTICATION_FLOW = "clients"; public static final String FIRST_BROKER_LOGIN_FLOW = "first broker login"; @@ -58,6 +59,7 @@ public class DefaultAuthenticationFlows { if (realm.getFlowByAlias(CLIENT_AUTHENTICATION_FLOW) == null) clientAuthFlow(realm); if (realm.getFlowByAlias(FIRST_BROKER_LOGIN_FLOW) == null) firstBrokerLoginFlow(realm, false); if (realm.getFlowByAlias(SAML_ECP_FLOW) == null) samlEcpProfile(realm); + if (realm.getFlowByAlias(DOCKER_AUTH) == null) dockerAuthenticationFlow(realm); } public static void migrateFlows(RealmModel realm) { if (realm.getFlowByAlias(BROWSER_FLOW) == null) browserFlow(realm, true); @@ -67,6 +69,7 @@ public class DefaultAuthenticationFlows { if (realm.getFlowByAlias(CLIENT_AUTHENTICATION_FLOW) == null) clientAuthFlow(realm); if (realm.getFlowByAlias(FIRST_BROKER_LOGIN_FLOW) == null) firstBrokerLoginFlow(realm, true); if (realm.getFlowByAlias(SAML_ECP_FLOW) == null) samlEcpProfile(realm); + if (realm.getFlowByAlias(DOCKER_AUTH) == null) dockerAuthenticationFlow(realm); } public static void registrationFlow(RealmModel realm) { @@ -528,4 +531,26 @@ public class DefaultAuthenticationFlows { realm.addAuthenticatorExecution(execution); } + + public static void dockerAuthenticationFlow(final RealmModel realm) { + AuthenticationFlowModel dockerAuthFlow = new AuthenticationFlowModel(); + + dockerAuthFlow.setAlias(DOCKER_AUTH); + dockerAuthFlow.setDescription("Used by Docker clients to authenticate against the IDP"); + dockerAuthFlow.setProviderId("basic-flow"); + dockerAuthFlow.setTopLevel(true); + dockerAuthFlow.setBuiltIn(true); + dockerAuthFlow = realm.addAuthenticationFlow(dockerAuthFlow); + realm.setDockerAuthenticationFlow(dockerAuthFlow); + + AuthenticationExecutionModel execution = new AuthenticationExecutionModel(); + + execution.setParentFlow(dockerAuthFlow.getId()); + execution.setRequirement(AuthenticationExecutionModel.Requirement.REQUIRED); + execution.setAuthenticator("docker-http-basic-authenticator"); + execution.setPriority(10); + execution.setAuthenticatorFlow(false); + + realm.addAuthenticatorExecution(execution); + } } diff --git a/server-spi-private/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java b/server-spi-private/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java index b454460325..dc69fc82de 100755 --- a/server-spi-private/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java +++ b/server-spi-private/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java @@ -489,6 +489,7 @@ public final class KeycloakModelUtils { if ((realmFlow = realm.getClientAuthenticationFlow()) != null && realmFlow.getId().equals(model.getId())) return true; if ((realmFlow = realm.getDirectGrantFlow()) != null && realmFlow.getId().equals(model.getId())) return true; if ((realmFlow = realm.getResetCredentialsFlow()) != null && realmFlow.getId().equals(model.getId())) return true; + if ((realmFlow = realm.getDockerAuthenticationFlow()) != null && realmFlow.getId().equals(model.getId())) return true; for (IdentityProviderModel idp : realm.getIdentityProviders()) { if (model.getId().equals(idp.getFirstBrokerLoginFlowId())) return true; diff --git a/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java b/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java index a2a3e092ea..d11c2729db 100755 --- a/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java +++ b/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java @@ -24,6 +24,7 @@ import org.keycloak.authorization.model.ResourceServer; import org.keycloak.authorization.model.Scope; import org.keycloak.authorization.policy.provider.PolicyProviderFactory; import org.keycloak.authorization.store.ResourceStore; +import org.keycloak.common.Profile; import org.keycloak.common.util.MultivaluedHashMap; import org.keycloak.common.util.Time; import org.keycloak.component.ComponentModel; @@ -291,6 +292,7 @@ public class ModelToRepresentation { if (realm.getDirectGrantFlow() != null) rep.setDirectGrantFlow(realm.getDirectGrantFlow().getAlias()); if (realm.getResetCredentialsFlow() != null) rep.setResetCredentialsFlow(realm.getResetCredentialsFlow().getAlias()); if (realm.getClientAuthenticationFlow() != null) rep.setClientAuthenticationFlow(realm.getClientAuthenticationFlow().getAlias()); + if (realm.getDockerAuthenticationFlow() != null) rep.setDockerAuthenticationFlow(realm.getDockerAuthenticationFlow().getAlias()); List defaultRoles = realm.getDefaultRoles(); if (!defaultRoles.isEmpty()) { diff --git a/server-spi-private/src/main/java/org/keycloak/models/utils/PostMigrationEvent.java b/server-spi-private/src/main/java/org/keycloak/models/utils/PostMigrationEvent.java index 73889e05fd..430d895fc9 100644 --- a/server-spi-private/src/main/java/org/keycloak/models/utils/PostMigrationEvent.java +++ b/server-spi-private/src/main/java/org/keycloak/models/utils/PostMigrationEvent.java @@ -17,6 +17,7 @@ package org.keycloak.models.utils; +import org.keycloak.models.KeycloakSession; import org.keycloak.provider.ProviderEvent; /** @@ -25,4 +26,14 @@ import org.keycloak.provider.ProviderEvent; * @author Marek Posolda */ public class PostMigrationEvent implements ProviderEvent { + + private final KeycloakSession session; + + public PostMigrationEvent(KeycloakSession session) { + this.session = session; + } + + public KeycloakSession getSession() { + return session; + } } diff --git a/server-spi-private/src/main/java/org/keycloak/models/utils/RepresentationToModel.java b/server-spi-private/src/main/java/org/keycloak/models/utils/RepresentationToModel.java index 4a4b4fbfc3..fe27fae666 100755 --- a/server-spi-private/src/main/java/org/keycloak/models/utils/RepresentationToModel.java +++ b/server-spi-private/src/main/java/org/keycloak/models/utils/RepresentationToModel.java @@ -76,6 +76,7 @@ import org.keycloak.models.ScopeContainerModel; import org.keycloak.models.UserConsentModel; import org.keycloak.models.UserCredentialModel; import org.keycloak.models.UserModel; +import org.keycloak.models.UserProvider; import org.keycloak.provider.ProviderConfigProperty; import org.keycloak.representations.idm.ApplicationRepresentation; import org.keycloak.representations.idm.AuthenticationExecutionExportRepresentation; @@ -614,6 +615,18 @@ public class RepresentationToModel { } } + // Added in 3.2 + if (rep.getDockerAuthenticationFlow() == null) { + AuthenticationFlowModel dockerAuthenticationFlow = newRealm.getFlowByAlias(DefaultAuthenticationFlows.DOCKER_AUTH); + if (dockerAuthenticationFlow == null) { + DefaultAuthenticationFlows.dockerAuthenticationFlow(newRealm); + } else { + newRealm.setDockerAuthenticationFlow(dockerAuthenticationFlow); + } + } else { + newRealm.setDockerAuthenticationFlow(newRealm.getFlowByAlias(rep.getDockerAuthenticationFlow())); + } + DefaultAuthenticationFlows.addIdentityProviderAuthenticator(newRealm, defaultProvider); } @@ -898,6 +911,9 @@ public class RepresentationToModel { if (rep.getClientAuthenticationFlow() != null) { realm.setClientAuthenticationFlow(realm.getFlowByAlias(rep.getClientAuthenticationFlow())); } + if (rep.getDockerAuthenticationFlow() != null) { + realm.setDockerAuthenticationFlow(realm.getFlowByAlias(rep.getDockerAuthenticationFlow())); + } } // Basic realm stuff @@ -1201,6 +1217,7 @@ public class RepresentationToModel { if (rep.isUseTemplateScope() != null) resource.setUseTemplateScope(rep.isUseTemplateScope()); if (rep.isUseTemplateMappers() != null) resource.setUseTemplateMappers(rep.isUseTemplateMappers()); + if (rep.getSecret() != null) resource.setSecret(rep.getSecret()); if (rep.getClientTemplate() != null) { if (rep.getClientTemplate().equals(ClientTemplateRepresentation.NONE)) { @@ -1913,24 +1930,21 @@ public class RepresentationToModel { resourceServer.setPolicyEnforcementMode(rep.getPolicyEnforcementMode()); resourceServer.setAllowRemoteResourceManagement(rep.isAllowRemoteResourceManagement()); - rep.getScopes().forEach(scope -> { + for (ScopeRepresentation scope : rep.getScopes()) { toModel(scope, resourceServer, authorization); - }); + } KeycloakSession session = authorization.getKeycloakSession(); RealmModel realm = authorization.getRealm(); - rep.getResources().forEach(resourceRepresentation -> { - ResourceOwnerRepresentation owner = resourceRepresentation.getOwner(); + for (ResourceRepresentation resource : rep.getResources()) { + ResourceOwnerRepresentation owner = resource.getOwner(); if (owner == null) { owner = new ResourceOwnerRepresentation(); - resourceRepresentation.setOwner(owner); - } - - owner.setId(resourceServer.getClientId()); - - if (owner.getName() != null) { + owner.setId(resourceServer.getClientId()); + resource.setOwner(owner); + } else if (owner.getName() != null) { UserModel user = session.users().getUserByUsername(owner.getName(), realm); if (user != null) { @@ -1938,8 +1952,8 @@ public class RepresentationToModel { } } - toModel(resourceRepresentation, resourceServer, authorization); - }); + toModel(resource, resourceServer, authorization); + } importPolicies(authorization, resourceServer, rep.getPolicies(), null); } @@ -1958,7 +1972,9 @@ public class RepresentationToModel { PolicyStore policyStore = storeFactory.getPolicyStore(); try { List policies = (List) JsonSerialization.readValue(applyPolicies, List.class); - config.put("applyPolicies", JsonSerialization.writeValueAsString(policies.stream().map(policyName -> { + Set policyIds = new HashSet<>(); + + for (String policyName : policies) { Policy policy = policyStore.findByName(policyName, resourceServer.getId()); if (policy == null) { @@ -1972,8 +1988,10 @@ public class RepresentationToModel { } } - return policy.getId(); - }).collect(Collectors.toList()))); + policyIds.add(policy.getId()); + } + + config.put("applyPolicies", JsonSerialization.writeValueAsString(policyIds)); } catch (Exception e) { throw new RuntimeException("Error while importing policy [" + policyRepresentation.getName() + "].", e); } @@ -2012,33 +2030,40 @@ public class RepresentationToModel { if (representation instanceof PolicyRepresentation) { PolicyRepresentation policy = PolicyRepresentation.class.cast(representation); - String resourcesConfig = policy.getConfig().get("resources"); - if (resourcesConfig != null) { - try { - resources = JsonSerialization.readValue(resourcesConfig, Set.class); - } catch (IOException e) { - throw new RuntimeException(e); + if (resources == null) { + String resourcesConfig = policy.getConfig().get("resources"); + + if (resourcesConfig != null) { + try { + resources = JsonSerialization.readValue(resourcesConfig, Set.class); + } catch (IOException e) { + throw new RuntimeException(e); + } } } - String scopesConfig = policy.getConfig().get("scopes"); + if (scopes == null) { + String scopesConfig = policy.getConfig().get("scopes"); - if (scopesConfig != null) { - try { - scopes = JsonSerialization.readValue(scopesConfig, Set.class); - } catch (IOException e) { - throw new RuntimeException(e); + if (scopesConfig != null) { + try { + scopes = JsonSerialization.readValue(scopesConfig, Set.class); + } catch (IOException e) { + throw new RuntimeException(e); + } } } - String policiesConfig = policy.getConfig().get("applyPolicies"); + if (policies == null) { + String policiesConfig = policy.getConfig().get("applyPolicies"); - if (policiesConfig != null) { - try { - policies = JsonSerialization.readValue(policiesConfig, Set.class); - } catch (IOException e) { - throw new RuntimeException(e); + if (policiesConfig != null) { + try { + policies = JsonSerialization.readValue(policiesConfig, Set.class); + } catch (IOException e) { + throw new RuntimeException(e); + } } } @@ -2229,10 +2254,10 @@ public class RepresentationToModel { existing.setType(resource.getType()); existing.setUri(resource.getUri()); existing.setIconUri(resource.getIconUri()); - existing.updateScopes(resource.getScopes().stream() .map((ScopeRepresentation scope) -> toModel(scope, resourceServer, authorization)) .collect(Collectors.toSet())); + return existing; } @@ -2243,11 +2268,30 @@ public class RepresentationToModel { owner.setId(resourceServer.getClientId()); } - if (owner.getId() == null) { + String ownerId = owner.getId(); + + if (ownerId == null) { throw new RuntimeException("No owner specified for resource [" + resource.getName() + "]."); } - Resource model = resourceStore.create(resource.getName(), resourceServer, owner.getId()); + if (!resourceServer.getClientId().equals(ownerId)) { + RealmModel realm = authorization.getRealm(); + KeycloakSession keycloakSession = authorization.getKeycloakSession(); + UserProvider users = keycloakSession.users(); + UserModel ownerModel = users.getUserById(ownerId, realm); + + if (ownerModel == null) { + ownerModel = users.getUserByUsername(ownerId, realm); + } + + if (ownerModel == null) { + throw new RuntimeException("Owner must be a valid username or user identifier. If the resource server, the client id or null."); + } + + owner.setId(ownerModel.getId()); + } + + Resource model = resourceStore.create(resource.getName(), resourceServer, ownerId); model.setType(resource.getType()); model.setUri(resource.getUri()); diff --git a/server-spi/pom.xml b/server-spi/pom.xml index 68672fb8ea..e251128e6e 100755 --- a/server-spi/pom.xml +++ b/server-spi/pom.xml @@ -21,7 +21,7 @@ keycloak-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../pom.xml 4.0.0 diff --git a/server-spi/src/main/java/org/keycloak/models/AbstractKeycloakTransaction.java b/server-spi/src/main/java/org/keycloak/models/AbstractKeycloakTransaction.java new file mode 100644 index 0000000000..a3ea54bcd4 --- /dev/null +++ b/server-spi/src/main/java/org/keycloak/models/AbstractKeycloakTransaction.java @@ -0,0 +1,91 @@ +/* + * 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.models; + +import org.jboss.logging.Logger; + +/** + * Handles some common transaction logic related to start, rollback-only etc. + * + * @author Marek Posolda + */ +public abstract class AbstractKeycloakTransaction implements KeycloakTransaction { + + public static final Logger logger = Logger.getLogger(AbstractKeycloakTransaction.class); + + protected TransactionState state = TransactionState.NOT_STARTED; + + @Override + public void begin() { + if (state != TransactionState.NOT_STARTED) { + throw new IllegalStateException("Transaction already started"); + } + + state = TransactionState.STARTED; + } + + @Override + public void commit() { + if (state != TransactionState.STARTED) { + throw new IllegalStateException("Transaction in illegal state for commit: " + state); + } + + commitImpl(); + + state = TransactionState.FINISHED; + } + + @Override + public void rollback() { + if (state != TransactionState.STARTED && state != TransactionState.ROLLBACK_ONLY) { + throw new IllegalStateException("Transaction in illegal state for rollback: " + state); + } + + rollbackImpl(); + + state = TransactionState.FINISHED; + } + + @Override + public void setRollbackOnly() { + state = TransactionState.ROLLBACK_ONLY; + } + + @Override + public boolean getRollbackOnly() { + return state == TransactionState.ROLLBACK_ONLY; + } + + @Override + public boolean isActive() { + return state == TransactionState.STARTED || state == TransactionState.ROLLBACK_ONLY; + } + + public TransactionState getState() { + return state; + } + + public enum TransactionState { + NOT_STARTED, STARTED, ROLLBACK_ONLY, FINISHED + } + + + protected abstract void commitImpl(); + + protected abstract void rollbackImpl(); +} diff --git a/server-spi/src/main/java/org/keycloak/models/ActionTokenKeyModel.java b/server-spi/src/main/java/org/keycloak/models/ActionTokenKeyModel.java index cf9d7d02e1..2725945def 100644 --- a/server-spi/src/main/java/org/keycloak/models/ActionTokenKeyModel.java +++ b/server-spi/src/main/java/org/keycloak/models/ActionTokenKeyModel.java @@ -43,4 +43,8 @@ public interface ActionTokenKeyModel { * @return Single-use random value used for verification whether the relevant action is allowed. */ UUID getActionVerificationNonce(); + + default String serializeKey() { + return String.format("%s.%d.%s.%s", getUserId(), getExpiration(), getActionVerificationNonce(), getActionId()); + } } 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 133c247076..f8d32e1f64 100755 --- a/server-spi/src/main/java/org/keycloak/models/RealmModel.java +++ b/server-spi/src/main/java/org/keycloak/models/RealmModel.java @@ -247,6 +247,9 @@ public interface RealmModel extends RoleContainerModel { AuthenticationFlowModel getClientAuthenticationFlow(); void setClientAuthenticationFlow(AuthenticationFlowModel flow); + AuthenticationFlowModel getDockerAuthenticationFlow(); + void setDockerAuthenticationFlow(AuthenticationFlowModel flow); + List getAuthenticationFlows(); AuthenticationFlowModel getFlowByAlias(String alias); AuthenticationFlowModel addAuthenticationFlow(AuthenticationFlowModel model); diff --git a/server-spi/src/main/java/org/keycloak/models/UserSessionModel.java b/server-spi/src/main/java/org/keycloak/models/UserSessionModel.java index 28a31457c1..a6f1c355ae 100755 --- a/server-spi/src/main/java/org/keycloak/models/UserSessionModel.java +++ b/server-spi/src/main/java/org/keycloak/models/UserSessionModel.java @@ -62,8 +62,6 @@ public interface UserSessionModel { State getState(); void setState(State state); - void setUser(UserModel user); - // Will completely restart whole state of user session. It will just keep same ID. void restartSession(RealmModel realm, UserModel user, String loginUsername, String ipAddress, String authMethod, boolean rememberMe, String brokerSessionId, String brokerUserId); diff --git a/server-spi/src/main/java/org/keycloak/models/UserSessionProvider.java b/server-spi/src/main/java/org/keycloak/models/UserSessionProvider.java index 848c098e70..8334838b8c 100755 --- a/server-spi/src/main/java/org/keycloak/models/UserSessionProvider.java +++ b/server-spi/src/main/java/org/keycloak/models/UserSessionProvider.java @@ -20,6 +20,7 @@ package org.keycloak.models; import org.keycloak.provider.Provider; import java.util.List; +import java.util.function.Predicate; /** * @author Bill Burke @@ -37,6 +38,12 @@ public interface UserSessionProvider extends Provider { List getUserSessionByBrokerUserId(RealmModel realm, String brokerUserId); UserSessionModel getUserSessionByBrokerSessionId(RealmModel realm, String brokerSessionId); + /** + * Return userSession of specified ID as long as the predicate passes. Otherwise returs null. + * If predicate doesn't pass, implementation can do some best-effort actions to try have predicate passing (eg. download userSession from other DC) + */ + UserSessionModel getUserSessionWithPredicate(RealmModel realm, String id, boolean offline, Predicate predicate); + long getActiveUserSessions(RealmModel realm, ClientModel client); /** This will remove attached ClientLoginSessionModels too **/ diff --git a/server-spi/src/main/java/org/keycloak/provider/ProviderFactory.java b/server-spi/src/main/java/org/keycloak/provider/ProviderFactory.java index 3cf8d2cab7..5c83253a8d 100755 --- a/server-spi/src/main/java/org/keycloak/provider/ProviderFactory.java +++ b/server-spi/src/main/java/org/keycloak/provider/ProviderFactory.java @@ -53,4 +53,8 @@ public interface ProviderFactory { public String getId(); + default int order() { + return 0; + } + } diff --git a/server-spi/src/main/java/org/keycloak/sessions/CommonClientSessionModel.java b/server-spi/src/main/java/org/keycloak/sessions/CommonClientSessionModel.java index c47a6a5ea2..1598714c37 100644 --- a/server-spi/src/main/java/org/keycloak/sessions/CommonClientSessionModel.java +++ b/server-spi/src/main/java/org/keycloak/sessions/CommonClientSessionModel.java @@ -53,7 +53,7 @@ public interface CommonClientSessionModel { // TODO: Not needed here...? public Set getProtocolMappers(); public void setProtocolMappers(Set protocolMappers); - + public static enum Action { OAUTH_GRANT, CODE_TO_TOKEN, diff --git a/services/pom.xml b/services/pom.xml index c1295dbeec..733b81277e 100755 --- a/services/pom.xml +++ b/services/pom.xml @@ -21,7 +21,7 @@ keycloak-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../pom.xml 4.0.0 @@ -144,6 +144,12 @@ com.fasterxml.jackson.core jackson-annotations + + com.fasterxml.woodstox + woodstox-core + ${woodstox.version} + test + com.google.zxing javase diff --git a/services/src/main/java/org/keycloak/authentication/actiontoken/AbstractActionTokenHander.java b/services/src/main/java/org/keycloak/authentication/actiontoken/AbstractActionTokenHander.java index 52d94d95f7..ccdc2f8cd9 100644 --- a/services/src/main/java/org/keycloak/authentication/actiontoken/AbstractActionTokenHander.java +++ b/services/src/main/java/org/keycloak/authentication/actiontoken/AbstractActionTokenHander.java @@ -20,6 +20,7 @@ import org.keycloak.Config.Scope; import org.keycloak.events.EventType; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.representations.JsonWebToken; import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.sessions.AuthenticationSessionModel; @@ -27,7 +28,7 @@ import org.keycloak.sessions.AuthenticationSessionModel; * * @author hmlnarik */ -public abstract class AbstractActionTokenHander implements ActionTokenHandler, ActionTokenHandlerFactory { +public abstract class AbstractActionTokenHander implements ActionTokenHandler, ActionTokenHandlerFactory { private final String id; private final Class tokenClass; @@ -86,8 +87,8 @@ public abstract class AbstractActionTokenHander im } @Override - public String getAuthenticationSessionIdFromToken(T token) { - return token == null ? null : token.getAuthenticationSessionId(); + public String getAuthenticationSessionIdFromToken(T token, ActionTokenContext tokenContext) { + return token instanceof DefaultActionToken ? ((DefaultActionToken) token).getAuthenticationSessionId() : null; } @Override diff --git a/services/src/main/java/org/keycloak/authentication/actiontoken/ActionTokenHandler.java b/services/src/main/java/org/keycloak/authentication/actiontoken/ActionTokenHandler.java index f8d02d3468..4f980706ef 100644 --- a/services/src/main/java/org/keycloak/authentication/actiontoken/ActionTokenHandler.java +++ b/services/src/main/java/org/keycloak/authentication/actiontoken/ActionTokenHandler.java @@ -17,6 +17,7 @@ package org.keycloak.authentication.actiontoken; import org.keycloak.TokenVerifier.Predicate; +import org.keycloak.common.VerificationException; import org.keycloak.events.EventBuilder; import org.keycloak.events.EventType; import org.keycloak.provider.Provider; @@ -64,7 +65,7 @@ public interface ActionTokenHandler extends Provider { * @param token Token. Can be {@code null} * @return authentication session ID */ - String getAuthenticationSessionIdFromToken(T token); + String getAuthenticationSessionIdFromToken(T token, ActionTokenContext tokenContext); /** * Returns a event type logged with {@link EventBuilder} class. @@ -93,7 +94,7 @@ public interface ActionTokenHandler extends Provider { * @param tokenContext * @return */ - AuthenticationSessionModel startFreshAuthenticationSession(T token, ActionTokenContext tokenContext); + AuthenticationSessionModel startFreshAuthenticationSession(T token, ActionTokenContext tokenContext) throws VerificationException; /** * Returns {@code true} when the token can be used repeatedly to invoke the action, {@code false} when the token diff --git a/services/src/main/java/org/keycloak/authentication/actiontoken/DefaultActionToken.java b/services/src/main/java/org/keycloak/authentication/actiontoken/DefaultActionToken.java index ba4488039a..0f514d0d04 100644 --- a/services/src/main/java/org/keycloak/authentication/actiontoken/DefaultActionToken.java +++ b/services/src/main/java/org/keycloak/authentication/actiontoken/DefaultActionToken.java @@ -39,7 +39,7 @@ public class DefaultActionToken extends DefaultActionTokenKey implements ActionT public static final String JSON_FIELD_AUTHENTICATION_SESSION_ID = "asid"; - public static final Predicate ACTION_TOKEN_BASIC_CHECKS = t -> { + public static final Predicate ACTION_TOKEN_BASIC_CHECKS = t -> { if (t.getActionVerificationNonce() == null) { throw new VerificationException("Nonce not present."); } @@ -131,7 +131,7 @@ public class DefaultActionToken extends DefaultActionTokenKey implements ActionT *
  • {@code issuer}: URI of the given realm
  • *
  • {@code audience}: URI of the given realm (same as issuer)
  • * - * + * * @param session * @param realm * @param uri diff --git a/services/src/main/java/org/keycloak/authentication/actiontoken/DefaultActionTokenKey.java b/services/src/main/java/org/keycloak/authentication/actiontoken/DefaultActionTokenKey.java index b41681f303..9723005a85 100644 --- a/services/src/main/java/org/keycloak/authentication/actiontoken/DefaultActionTokenKey.java +++ b/services/src/main/java/org/keycloak/authentication/actiontoken/DefaultActionTokenKey.java @@ -36,6 +36,9 @@ public class DefaultActionTokenKey extends JsonWebToken implements ActionTokenKe @JsonProperty(value = JSON_FIELD_ACTION_VERIFICATION_NONCE, required = true) private UUID actionVerificationNonce; + public DefaultActionTokenKey() { + } + public DefaultActionTokenKey(String userId, String actionId, int absoluteExpirationInSecs, UUID actionVerificationNonce) { this.subject = userId; this.type = actionId; @@ -60,10 +63,6 @@ public class DefaultActionTokenKey extends JsonWebToken implements ActionTokenKe return actionVerificationNonce; } - public String serializeKey() { - return String.format("%s.%d.%s.%s", getUserId(), getExpiration(), getActionVerificationNonce(), getActionId()); - } - public static DefaultActionTokenKey from(String serializedKey) { if (serializedKey == null) { return null; diff --git a/services/src/main/java/org/keycloak/authentication/actiontoken/execactions/ExecuteActionsActionTokenHandler.java b/services/src/main/java/org/keycloak/authentication/actiontoken/execactions/ExecuteActionsActionTokenHandler.java index 9993ab76e8..a1c857f0f1 100644 --- a/services/src/main/java/org/keycloak/authentication/actiontoken/execactions/ExecuteActionsActionTokenHandler.java +++ b/services/src/main/java/org/keycloak/authentication/actiontoken/execactions/ExecuteActionsActionTokenHandler.java @@ -22,14 +22,20 @@ import org.keycloak.authentication.RequiredActionProvider; import org.keycloak.authentication.actiontoken.*; import org.keycloak.events.Errors; import org.keycloak.events.EventType; +import org.keycloak.forms.login.LoginFormsProvider; import org.keycloak.models.*; +import org.keycloak.models.UserModel.RequiredAction; import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.protocol.oidc.utils.RedirectUtils; +import org.keycloak.provider.ProviderFactory; +import org.keycloak.services.Urls; import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.services.messages.Messages; import org.keycloak.sessions.AuthenticationSessionModel; import java.util.Objects; import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriBuilder; +import javax.ws.rs.core.UriInfo; /** * @@ -64,6 +70,21 @@ public class ExecuteActionsActionTokenHandler extends AbstractActionTokenHander< @Override public Response handleToken(ExecuteActionsActionToken token, ActionTokenContext tokenContext) { AuthenticationSessionModel authSession = tokenContext.getAuthenticationSession(); + final UriInfo uriInfo = tokenContext.getUriInfo(); + final RealmModel realm = tokenContext.getRealm(); + final KeycloakSession session = tokenContext.getSession(); + if (tokenContext.isAuthenticationSessionFresh()) { + // Update the authentication session in the token + token.setAuthenticationSessionId(authSession.getId()); + UriBuilder builder = Urls.actionTokenBuilder(uriInfo.getBaseUri(), token.serialize(session, realm, uriInfo)); + String confirmUri = builder.build(realm.getName()).toString(); + + return session.getProvider(LoginFormsProvider.class) + .setSuccess(Messages.CONFIRM_EXECUTION_OF_ACTIONS) + .setAttribute(Constants.TEMPLATE_ATTR_ACTION_URI, confirmUri) + .setAttribute(Constants.TEMPLATE_ATTR_REQUIRED_ACTIONS, token.getRequiredActions()) + .createInfoPage(); + } String redirectUri = RedirectUtils.verifyRedirectUri(tokenContext.getUriInfo(), token.getRedirectUri(), tokenContext.getRealm(), authSession.getClient()); diff --git a/services/src/main/java/org/keycloak/authentication/actiontoken/idpverifyemail/IdpVerifyAccountLinkActionToken.java b/services/src/main/java/org/keycloak/authentication/actiontoken/idpverifyemail/IdpVerifyAccountLinkActionToken.java index 7776634193..39c6f9ae34 100644 --- a/services/src/main/java/org/keycloak/authentication/actiontoken/idpverifyemail/IdpVerifyAccountLinkActionToken.java +++ b/services/src/main/java/org/keycloak/authentication/actiontoken/idpverifyemail/IdpVerifyAccountLinkActionToken.java @@ -30,6 +30,7 @@ public class IdpVerifyAccountLinkActionToken extends DefaultActionToken { private static final String JSON_FIELD_IDENTITY_PROVIDER_USERNAME = "idpu"; private static final String JSON_FIELD_IDENTITY_PROVIDER_ALIAS = "idpa"; + private static final String JSON_FIELD_ORIGINAL_AUTHENTICATION_SESSION_ID = "oasid"; @JsonProperty(value = JSON_FIELD_IDENTITY_PROVIDER_USERNAME) private String identityProviderUsername; @@ -37,6 +38,9 @@ public class IdpVerifyAccountLinkActionToken extends DefaultActionToken { @JsonProperty(value = JSON_FIELD_IDENTITY_PROVIDER_ALIAS) private String identityProviderAlias; + @JsonProperty(value = JSON_FIELD_ORIGINAL_AUTHENTICATION_SESSION_ID) + private String originalAuthenticationSessionId; + public IdpVerifyAccountLinkActionToken(String userId, int absoluteExpirationInSecs, String authenticationSessionId, String identityProviderUsername, String identityProviderAlias) { super(userId, TOKEN_TYPE, absoluteExpirationInSecs, null, authenticationSessionId); @@ -62,4 +66,12 @@ public class IdpVerifyAccountLinkActionToken extends DefaultActionToken { public void setIdentityProviderAlias(String identityProviderAlias) { this.identityProviderAlias = identityProviderAlias; } + + public String getOriginalAuthenticationSessionId() { + return originalAuthenticationSessionId; + } + + public void setOriginalAuthenticationSessionId(String originalAuthenticationSessionId) { + this.originalAuthenticationSessionId = originalAuthenticationSessionId; + } } diff --git a/services/src/main/java/org/keycloak/authentication/actiontoken/idpverifyemail/IdpVerifyAccountLinkActionTokenHandler.java b/services/src/main/java/org/keycloak/authentication/actiontoken/idpverifyemail/IdpVerifyAccountLinkActionTokenHandler.java index bd56eea4ab..c5dc897a18 100644 --- a/services/src/main/java/org/keycloak/authentication/actiontoken/idpverifyemail/IdpVerifyAccountLinkActionTokenHandler.java +++ b/services/src/main/java/org/keycloak/authentication/actiontoken/idpverifyemail/IdpVerifyAccountLinkActionTokenHandler.java @@ -24,13 +24,18 @@ import org.keycloak.authentication.authenticators.broker.IdpEmailVerificationAut import org.keycloak.events.*; import org.keycloak.forms.login.LoginFormsProvider; import org.keycloak.models.Constants; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; +import org.keycloak.services.Urls; import org.keycloak.services.managers.AuthenticationSessionManager; import org.keycloak.services.messages.Messages; import org.keycloak.sessions.AuthenticationSessionModel; import org.keycloak.sessions.AuthenticationSessionProvider; import java.util.Collections; import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriBuilder; +import javax.ws.rs.core.UriInfo; /** * Action token handler for verification of e-mail address. @@ -58,6 +63,9 @@ public class IdpVerifyAccountLinkActionTokenHandler extends AbstractActionTokenH public Response handleToken(IdpVerifyAccountLinkActionToken token, ActionTokenContext tokenContext) { UserModel user = tokenContext.getAuthenticationSession().getAuthenticatedUser(); EventBuilder event = tokenContext.getEvent(); + final UriInfo uriInfo = tokenContext.getUriInfo(); + final RealmModel realm = tokenContext.getRealm(); + final KeycloakSession session = tokenContext.getSession(); event.event(EventType.IDENTITY_PROVIDER_LINK_ACCOUNT) .detail(Details.EMAIL, user.getEmail()) @@ -65,16 +73,28 @@ public class IdpVerifyAccountLinkActionTokenHandler extends AbstractActionTokenH .detail(Details.IDENTITY_PROVIDER_USERNAME, token.getIdentityProviderUsername()) .success(); + AuthenticationSessionModel authSession = tokenContext.getAuthenticationSession(); + if (tokenContext.isAuthenticationSessionFresh()) { + token.setOriginalAuthenticationSessionId(token.getAuthenticationSessionId()); + token.setAuthenticationSessionId(authSession.getId()); + UriBuilder builder = Urls.actionTokenBuilder(uriInfo.getBaseUri(), token.serialize(session, realm, uriInfo)); + String confirmUri = builder.build(realm.getName()).toString(); + + return session.getProvider(LoginFormsProvider.class) + .setSuccess(Messages.CONFIRM_ACCOUNT_LINKING, token.getIdentityProviderUsername(), token.getIdentityProviderAlias()) + .setAttribute(Constants.TEMPLATE_ATTR_ACTION_URI, confirmUri) + .createInfoPage(); + } + // verify user email as we know it is valid as this entry point would never have gotten here. user.setEmailVerified(true); - AuthenticationSessionModel authSession = tokenContext.getAuthenticationSession(); - if (tokenContext.isAuthenticationSessionFresh()) { - AuthenticationSessionManager asm = new AuthenticationSessionManager(tokenContext.getSession()); - asm.removeAuthenticationSession(tokenContext.getRealm(), authSession, true); + if (token.getOriginalAuthenticationSessionId() != null) { + AuthenticationSessionManager asm = new AuthenticationSessionManager(session); + asm.removeAuthenticationSession(realm, authSession, true); - AuthenticationSessionProvider authSessProvider = tokenContext.getSession().authenticationSessions(); - authSession = authSessProvider.getAuthenticationSession(tokenContext.getRealm(), token.getAuthenticationSessionId()); + AuthenticationSessionProvider authSessProvider = session.authenticationSessions(); + authSession = authSessProvider.getAuthenticationSession(realm, token.getOriginalAuthenticationSessionId()); if (authSession != null) { authSession.setAuthNote(IdpEmailVerificationAuthenticator.VERIFY_ACCOUNT_IDP_USERNAME, token.getIdentityProviderUsername()); @@ -85,7 +105,7 @@ public class IdpVerifyAccountLinkActionTokenHandler extends AbstractActionTokenH ); } - return tokenContext.getSession().getProvider(LoginFormsProvider.class) + return session.getProvider(LoginFormsProvider.class) .setSuccess(Messages.IDENTITY_PROVIDER_LINK_SUCCESS, token.getIdentityProviderAlias(), token.getIdentityProviderUsername()) .setAttribute(Constants.SKIP_LINK, true) .createInfoPage(); diff --git a/services/src/main/java/org/keycloak/authentication/actiontoken/verifyemail/VerifyEmailActionToken.java b/services/src/main/java/org/keycloak/authentication/actiontoken/verifyemail/VerifyEmailActionToken.java index 656c518718..f9ebc6d606 100644 --- a/services/src/main/java/org/keycloak/authentication/actiontoken/verifyemail/VerifyEmailActionToken.java +++ b/services/src/main/java/org/keycloak/authentication/actiontoken/verifyemail/VerifyEmailActionToken.java @@ -29,10 +29,14 @@ public class VerifyEmailActionToken extends DefaultActionToken { public static final String TOKEN_TYPE = "verify-email"; private static final String JSON_FIELD_EMAIL = "eml"; + private static final String JSON_FIELD_ORIGINAL_AUTHENTICATION_SESSION_ID = "oasid"; @JsonProperty(value = JSON_FIELD_EMAIL) private String email; + @JsonProperty(value = JSON_FIELD_ORIGINAL_AUTHENTICATION_SESSION_ID) + private String originalAuthenticationSessionId; + public VerifyEmailActionToken(String userId, int absoluteExpirationInSecs, String authenticationSessionId, String email) { super(userId, TOKEN_TYPE, absoluteExpirationInSecs, null, authenticationSessionId); this.email = email; @@ -48,4 +52,12 @@ public class VerifyEmailActionToken extends DefaultActionToken { public void setEmail(String email) { this.email = email; } + + public String getOriginalAuthenticationSessionId() { + return originalAuthenticationSessionId; + } + + public void setOriginalAuthenticationSessionId(String originalAuthenticationSessionId) { + this.originalAuthenticationSessionId = originalAuthenticationSessionId; + } } diff --git a/services/src/main/java/org/keycloak/authentication/actiontoken/verifyemail/VerifyEmailActionTokenHandler.java b/services/src/main/java/org/keycloak/authentication/actiontoken/verifyemail/VerifyEmailActionTokenHandler.java index abe2127098..b5d046e971 100644 --- a/services/src/main/java/org/keycloak/authentication/actiontoken/verifyemail/VerifyEmailActionTokenHandler.java +++ b/services/src/main/java/org/keycloak/authentication/actiontoken/verifyemail/VerifyEmailActionTokenHandler.java @@ -21,14 +21,20 @@ import org.keycloak.TokenVerifier.Predicate; import org.keycloak.authentication.actiontoken.*; import org.keycloak.events.*; import org.keycloak.forms.login.LoginFormsProvider; +import org.keycloak.models.Constants; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.models.UserModel.RequiredAction; +import org.keycloak.services.Urls; import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.services.managers.AuthenticationSessionManager; import org.keycloak.services.messages.Messages; import org.keycloak.sessions.AuthenticationSessionModel; import java.util.Objects; import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriBuilder; +import javax.ws.rs.core.UriInfo; /** * Action token handler for verification of e-mail address. @@ -57,13 +63,29 @@ public class VerifyEmailActionTokenHandler extends AbstractActionTokenHander tokenContext) { + public Response handleToken(VerifyEmailActionToken token, ActionTokenContext tokenContext) { UserModel user = tokenContext.getAuthenticationSession().getAuthenticatedUser(); EventBuilder event = tokenContext.getEvent(); event.event(EventType.VERIFY_EMAIL).detail(Details.EMAIL, user.getEmail()); AuthenticationSessionModel authSession = tokenContext.getAuthenticationSession(); + final UriInfo uriInfo = tokenContext.getUriInfo(); + final RealmModel realm = tokenContext.getRealm(); + final KeycloakSession session = tokenContext.getSession(); + + if (tokenContext.isAuthenticationSessionFresh()) { + // Update the authentication session in the token + token.setOriginalAuthenticationSessionId(token.getAuthenticationSessionId()); + token.setAuthenticationSessionId(authSession.getId()); + UriBuilder builder = Urls.actionTokenBuilder(uriInfo.getBaseUri(), token.serialize(session, realm, uriInfo)); + String confirmUri = builder.build(realm.getName()).toString(); + + return session.getProvider(LoginFormsProvider.class) + .setSuccess(Messages.CONFIRM_EMAIL_ADDRESS_VERIFICATION, user.getEmail()) + .setAttribute(Constants.TEMPLATE_ATTR_ACTION_URI, confirmUri) + .createInfoPage(); + } // verify user email as we know it is valid as this entry point would never have gotten here. user.setEmailVerified(true); @@ -72,9 +94,10 @@ public class VerifyEmailActionTokenHandler extends AbstractActionTokenHanderPedro Igor @@ -83,7 +78,11 @@ public class ResourceServerService { this.adminEvent = adminEvent; } - public void create(boolean newClient) { + public ResourceServer create(boolean newClient) { + if (resourceServer != null) { + throw new IllegalStateException("Resource server already created"); + } + this.auth.realm().requireManageAuthorization(); UserModel serviceAccount = this.session.users().getServiceAccount(client); @@ -96,6 +95,8 @@ public class ResourceServerService { createDefaultRoles(serviceAccount); createDefaultPermission(createDefaultResource(), createDefaultPolicy()); audit(OperationType.CREATE, uriInfo, newClient); + + return resourceServer; } @PUT @@ -111,22 +112,7 @@ public class ResourceServerService { public void delete() { this.auth.realm().requireManageAuthorization(); - StoreFactory storeFactory = authorization.getStoreFactory(); - ResourceStore resourceStore = storeFactory.getResourceStore(); - String id = resourceServer.getId(); - - PolicyStore policyStore = storeFactory.getPolicyStore(); - - policyStore.findByResourceServer(id).forEach(scope -> policyStore.delete(scope.getId())); - - resourceStore.findByResourceServer(id).forEach(resource -> resourceStore.delete(resource.getId())); - - ScopeStore scopeStore = storeFactory.getScopeStore(); - - scopeStore.findByResourceServer(id).forEach(scope -> scopeStore.delete(scope.getId())); - - storeFactory.getResourceServerStore().delete(id); - + authorization.getStoreFactory().getResourceServerStore().delete(resourceServer.getId()); audit(OperationType.DELETE, uriInfo, false); } @@ -148,7 +134,7 @@ public class ResourceServerService { @Path("/import") @POST @Consumes(MediaType.APPLICATION_JSON) - public Response importSettings(@Context final UriInfo uriInfo, ResourceServerRepresentation rep) throws IOException { + public Response importSettings(@Context final UriInfo uriInfo, ResourceServerRepresentation rep) { this.auth.realm().requireManageAuthorization(); rep.setClientId(client.getId()); diff --git a/services/src/main/java/org/keycloak/authorization/admin/ResourceSetService.java b/services/src/main/java/org/keycloak/authorization/admin/ResourceSetService.java index 7c95281f14..3f8b7373c3 100644 --- a/services/src/main/java/org/keycloak/authorization/admin/ResourceSetService.java +++ b/services/src/main/java/org/keycloak/authorization/admin/ResourceSetService.java @@ -101,39 +101,24 @@ public class ResourceSetService { Resource existingResource = storeFactory.getResourceStore().findByName(resource.getName(), this.resourceServer.getId()); ResourceOwnerRepresentation owner = resource.getOwner(); - if (existingResource != null && existingResource.getResourceServer().getId().equals(this.resourceServer.getId()) - && existingResource.getOwner().equals(owner)) { + if (owner == null) { + owner = new ResourceOwnerRepresentation(); + owner.setId(resourceServer.getClientId()); + } + + String ownerId = owner.getId(); + + if (ownerId == null) { + return ErrorResponse.error("You must specify the resource owner.", Status.BAD_REQUEST); + } + + if (existingResource != null && existingResource.getOwner().equals(ownerId)) { return ErrorResponse.exists("Resource with name [" + resource.getName() + "] already exists."); } - if (owner != null) { - String ownerId = owner.getId(); - - if (ownerId != null) { - if (!resourceServer.getClientId().equals(ownerId)) { - RealmModel realm = authorization.getRealm(); - KeycloakSession keycloakSession = authorization.getKeycloakSession(); - UserProvider users = keycloakSession.users(); - UserModel ownerModel = users.getUserById(ownerId, realm); - - if (ownerModel == null) { - ownerModel = users.getUserByUsername(ownerId, realm); - } - - if (ownerModel == null) { - return ErrorResponse.error("Owner must be a valid username or user identifier. If the resource server, the client id or null.", Status.BAD_REQUEST); - } - - owner.setId(ownerModel.getId()); - } - } - } - - Resource model = toModel(resource, this.resourceServer, authorization); - ResourceRepresentation representation = new ResourceRepresentation(); - representation.setId(model.getId()); + representation.setId(toModel(resource, this.resourceServer, authorization).getId()); return Response.status(Status.CREATED).entity(representation).build(); } diff --git a/services/src/main/java/org/keycloak/authorization/common/ClientModelIdentity.java b/services/src/main/java/org/keycloak/authorization/common/ClientModelIdentity.java new file mode 100644 index 0000000000..d2c6b67672 --- /dev/null +++ b/services/src/main/java/org/keycloak/authorization/common/ClientModelIdentity.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.authorization.common; + +import org.keycloak.authorization.attribute.Attributes; +import org.keycloak.authorization.identity.Identity; +import org.keycloak.common.util.MultivaluedHashMap; +import org.keycloak.models.ClientModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.models.RoleModel; +import org.keycloak.models.UserModel; + +import java.util.Map; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class ClientModelIdentity implements Identity { + protected RealmModel realm; + protected ClientModel client; + protected UserModel serviceAccount; + + public ClientModelIdentity(KeycloakSession session, ClientModel client) { + this.realm = client.getRealm(); + this.client = client; + this.serviceAccount = session.users().getServiceAccount(client); + } + + @Override + public String getId() { + return client.getId(); + } + + @Override + public Attributes getAttributes() { + MultivaluedHashMap map = new MultivaluedHashMap(); + if (serviceAccount != null) map.addAll(serviceAccount.getAttributes()); + return Attributes.from(map); + } + + @Override + public boolean hasRealmRole(String roleName) { + if (serviceAccount == null) return false; + RoleModel role = realm.getRole(roleName); + if (role == null) return false; + return serviceAccount.hasRole(role); + } + + @Override + public boolean hasClientRole(String clientId, String roleName) { + if (serviceAccount == null) return false; + ClientModel client = realm.getClientByClientId(clientId); + RoleModel role = client.getRole(roleName); + if (role == null) return false; + return serviceAccount.hasRole(role); + } + + @Override + public boolean hasRole(String roleName) { + throw new RuntimeException("Should not execute"); + } + + @Override + public boolean hasClientRole(String roleName) { + throw new RuntimeException("Should not execute"); + } +} diff --git a/services/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProvider.java b/services/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProvider.java index 45183c3658..a5304baac6 100755 --- a/services/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProvider.java +++ b/services/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProvider.java @@ -75,7 +75,7 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProviderBill Burke @@ -517,6 +519,17 @@ public class SAMLEndpoint { protected class PostBinding extends Binding { @Override protected void verifySignature(String key, SAMLDocumentHolder documentHolder) throws VerificationException { + NodeList nl = documentHolder.getSamlDocument().getElementsByTagNameNS(XMLSignature.XMLNS, "Signature"); + boolean anyElementSigned = (nl != null && nl.getLength() > 0); + if ((! anyElementSigned) && (documentHolder.getSamlObject() instanceof ResponseType)) { + ResponseType responseType = (ResponseType) documentHolder.getSamlObject(); + List assertions = responseType.getAssertions(); + if (! assertions.isEmpty() ) { + // Only relax verification if the response is an authnresponse and contains (encrypted/plaintext) assertion. + // In that case, signature is validated on assertion element + return; + } + } SamlProtocolUtils.verifyDocumentSignature(documentHolder.getSamlDocument(), getIDPKeyLocator()); } diff --git a/services/src/main/java/org/keycloak/broker/saml/SAMLIdentityProvider.java b/services/src/main/java/org/keycloak/broker/saml/SAMLIdentityProvider.java index 886ee4d19c..6a4470ba5b 100755 --- a/services/src/main/java/org/keycloak/broker/saml/SAMLIdentityProvider.java +++ b/services/src/main/java/org/keycloak/broker/saml/SAMLIdentityProvider.java @@ -54,6 +54,7 @@ import java.util.Set; import java.util.TreeSet; import org.keycloak.dom.saml.v2.metadata.KeyTypes; import org.keycloak.keys.KeyMetadata; +import org.keycloak.keys.KeyMetadata.Status; import org.keycloak.saml.processing.core.util.KeycloakKeySamlExtensionGenerator; import org.keycloak.sessions.AuthenticationSessionModel; @@ -237,18 +238,27 @@ public class SAMLIdentityProvider extends AbstractIdentityProvider keys = new TreeSet<>((o1, o2) -> o1.getStatus() == o2.getStatus() // Status can be only PASSIVE OR ACTIVE, push PASSIVE to end of list ? (int) (o2.getProviderPriority() - o1.getProviderPriority()) : (o1.getStatus() == KeyMetadata.Status.PASSIVE ? 1 : -1)); keys.addAll(session.keys().getRsaKeys(realm, false)); for (RsaKeyMetadata key : keys) { - addKeyInfo(keysString, key, KeyTypes.SIGNING.value()); + addKeyInfo(signingKeysString, key, KeyTypes.SIGNING.value()); + + if (key.getStatus() == Status.ACTIVE) { + addKeyInfo(encryptionKeysString, key, KeyTypes.ENCRYPTION.value()); + } } - String descriptor = SPMetadataDescriptor.getSPDescriptor(authnBinding, endpoint, endpoint, wantAuthnRequestsSigned, wantAssertionsSigned, entityId, nameIDPolicyFormat, keysString.toString()); + String descriptor = SPMetadataDescriptor.getSPDescriptor(authnBinding, endpoint, endpoint, + wantAuthnRequestsSigned, wantAssertionsSigned, wantAssertionsEncrypted, + entityId, nameIDPolicyFormat, signingKeysString.toString(), encryptionKeysString.toString()); + return Response.ok(descriptor, MediaType.APPLICATION_XML_TYPE).build(); } diff --git a/services/src/main/java/org/keycloak/broker/saml/SAMLIdentityProviderConfig.java b/services/src/main/java/org/keycloak/broker/saml/SAMLIdentityProviderConfig.java index 4d200a00a5..c6d999c3e2 100755 --- a/services/src/main/java/org/keycloak/broker/saml/SAMLIdentityProviderConfig.java +++ b/services/src/main/java/org/keycloak/broker/saml/SAMLIdentityProviderConfig.java @@ -27,6 +27,24 @@ public class SAMLIdentityProviderConfig extends IdentityProviderModel { public static final XmlKeyInfoKeyNameTransformer DEFAULT_XML_KEY_INFO_KEY_NAME_TRANSFORMER = XmlKeyInfoKeyNameTransformer.NONE; + public static final String ADD_EXTENSIONS_ELEMENT_WITH_KEY_INFO = "addExtensionsElementWithKeyInfo"; + public static final String BACKCHANNEL_SUPPORTED = "backchannelSupported"; + public static final String ENCRYPTION_PUBLIC_KEY = "encryptionPublicKey"; + public static final String FORCE_AUTHN = "forceAuthn"; + public static final String NAME_ID_POLICY_FORMAT = "nameIDPolicyFormat"; + public static final String POST_BINDING_AUTHN_REQUEST = "postBindingAuthnRequest"; + public static final String POST_BINDING_LOGOUT = "postBindingLogout"; + public static final String POST_BINDING_RESPONSE = "postBindingResponse"; + public static final String SIGNATURE_ALGORITHM = "signatureAlgorithm"; + public static final String SIGNING_CERTIFICATE_KEY = "signingCertificate"; + public static final String SINGLE_LOGOUT_SERVICE_URL = "singleLogoutServiceUrl"; + public static final String SINGLE_SIGN_ON_SERVICE_URL = "singleSignOnServiceUrl"; + public static final String VALIDATE_SIGNATURE = "validateSignature"; + public static final String WANT_ASSERTIONS_ENCRYPTED = "wantAssertionsEncrypted"; + public static final String WANT_ASSERTIONS_SIGNED = "wantAssertionsSigned"; + public static final String WANT_AUTHN_REQUESTS_SIGNED = "wantAuthnRequestsSigned"; + public static final String XML_SIG_KEY_INFO_KEY_NAME_TRANSFORMER = "xmlSigKeyInfoKeyNameTransformer"; + public SAMLIdentityProviderConfig() { } @@ -35,35 +53,35 @@ public class SAMLIdentityProviderConfig extends IdentityProviderModel { } public String getSingleSignOnServiceUrl() { - return getConfig().get("singleSignOnServiceUrl"); + return getConfig().get(SINGLE_SIGN_ON_SERVICE_URL); } public void setSingleSignOnServiceUrl(String singleSignOnServiceUrl) { - getConfig().put("singleSignOnServiceUrl", singleSignOnServiceUrl); + getConfig().put(SINGLE_SIGN_ON_SERVICE_URL, singleSignOnServiceUrl); } public String getSingleLogoutServiceUrl() { - return getConfig().get("singleLogoutServiceUrl"); + return getConfig().get(SINGLE_LOGOUT_SERVICE_URL); } public void setSingleLogoutServiceUrl(String singleLogoutServiceUrl) { - getConfig().put("singleLogoutServiceUrl", singleLogoutServiceUrl); + getConfig().put(SINGLE_LOGOUT_SERVICE_URL, singleLogoutServiceUrl); } public boolean isValidateSignature() { - return Boolean.valueOf(getConfig().get("validateSignature")); + return Boolean.valueOf(getConfig().get(VALIDATE_SIGNATURE)); } public void setValidateSignature(boolean validateSignature) { - getConfig().put("validateSignature", String.valueOf(validateSignature)); + getConfig().put(VALIDATE_SIGNATURE, String.valueOf(validateSignature)); } public boolean isForceAuthn() { - return Boolean.valueOf(getConfig().get("forceAuthn")); + return Boolean.valueOf(getConfig().get(FORCE_AUTHN)); } public void setForceAuthn(boolean forceAuthn) { - getConfig().put("forceAuthn", String.valueOf(forceAuthn)); + getConfig().put(FORCE_AUTHN, String.valueOf(forceAuthn)); } /** @@ -103,82 +121,80 @@ public class SAMLIdentityProviderConfig extends IdentityProviderModel { return crt.split(","); } - public static final String SIGNING_CERTIFICATE_KEY = "signingCertificate"; - public String getNameIDPolicyFormat() { - return getConfig().get("nameIDPolicyFormat"); + return getConfig().get(NAME_ID_POLICY_FORMAT); } public void setNameIDPolicyFormat(String nameIDPolicyFormat) { - getConfig().put("nameIDPolicyFormat", nameIDPolicyFormat); + getConfig().put(NAME_ID_POLICY_FORMAT, nameIDPolicyFormat); } public boolean isWantAuthnRequestsSigned() { - return Boolean.valueOf(getConfig().get("wantAuthnRequestsSigned")); + return Boolean.valueOf(getConfig().get(WANT_AUTHN_REQUESTS_SIGNED)); } public void setWantAuthnRequestsSigned(boolean wantAuthnRequestsSigned) { - getConfig().put("wantAuthnRequestsSigned", String.valueOf(wantAuthnRequestsSigned)); + getConfig().put(WANT_AUTHN_REQUESTS_SIGNED, String.valueOf(wantAuthnRequestsSigned)); } public boolean isWantAssertionsSigned() { - return Boolean.valueOf(getConfig().get("wantAssertionsSigned")); + return Boolean.valueOf(getConfig().get(WANT_ASSERTIONS_SIGNED)); } public void setWantAssertionsSigned(boolean wantAssertionsSigned) { - getConfig().put("wantAssertionsSigned", String.valueOf(wantAssertionsSigned)); + getConfig().put(WANT_ASSERTIONS_SIGNED, String.valueOf(wantAssertionsSigned)); } public boolean isWantAssertionsEncrypted() { - return Boolean.valueOf(getConfig().get("wantAssertionsEncrypted")); + return Boolean.valueOf(getConfig().get(WANT_ASSERTIONS_ENCRYPTED)); } public void setWantAssertionsEncrypted(boolean wantAssertionsEncrypted) { - getConfig().put("wantAssertionsEncrypted", String.valueOf(wantAssertionsEncrypted)); + getConfig().put(WANT_ASSERTIONS_ENCRYPTED, String.valueOf(wantAssertionsEncrypted)); } public boolean isAddExtensionsElementWithKeyInfo() { - return Boolean.valueOf(getConfig().get("addExtensionsElementWithKeyInfo")); + return Boolean.valueOf(getConfig().get(ADD_EXTENSIONS_ELEMENT_WITH_KEY_INFO)); } public void setAddExtensionsElementWithKeyInfo(boolean addExtensionsElementWithKeyInfo) { - getConfig().put("addExtensionsElementWithKeyInfo", String.valueOf(addExtensionsElementWithKeyInfo)); + getConfig().put(ADD_EXTENSIONS_ELEMENT_WITH_KEY_INFO, String.valueOf(addExtensionsElementWithKeyInfo)); } public String getSignatureAlgorithm() { - return getConfig().get("signatureAlgorithm"); + return getConfig().get(SIGNATURE_ALGORITHM); } public void setSignatureAlgorithm(String signatureAlgorithm) { - getConfig().put("signatureAlgorithm", signatureAlgorithm); + getConfig().put(SIGNATURE_ALGORITHM, signatureAlgorithm); } public String getEncryptionPublicKey() { - return getConfig().get("encryptionPublicKey"); + return getConfig().get(ENCRYPTION_PUBLIC_KEY); } public void setEncryptionPublicKey(String encryptionPublicKey) { - getConfig().put("encryptionPublicKey", encryptionPublicKey); + getConfig().put(ENCRYPTION_PUBLIC_KEY, encryptionPublicKey); } public boolean isPostBindingAuthnRequest() { - return Boolean.valueOf(getConfig().get("postBindingAuthnRequest")); + return Boolean.valueOf(getConfig().get(POST_BINDING_AUTHN_REQUEST)); } public void setPostBindingAuthnRequest(boolean postBindingAuthnRequest) { - getConfig().put("postBindingAuthnRequest", String.valueOf(postBindingAuthnRequest)); + getConfig().put(POST_BINDING_AUTHN_REQUEST, String.valueOf(postBindingAuthnRequest)); } public boolean isPostBindingResponse() { - return Boolean.valueOf(getConfig().get("postBindingResponse")); + return Boolean.valueOf(getConfig().get(POST_BINDING_RESPONSE)); } public void setPostBindingResponse(boolean postBindingResponse) { - getConfig().put("postBindingResponse", String.valueOf(postBindingResponse)); + getConfig().put(POST_BINDING_RESPONSE, String.valueOf(postBindingResponse)); } public boolean isPostBindingLogout() { - String postBindingLogout = getConfig().get("postBindingLogout"); + String postBindingLogout = getConfig().get(POST_BINDING_LOGOUT); if (postBindingLogout == null) { // To maintain unchanged behavior when adding this field, we set the inital value to equal that // of the binding for the response: @@ -188,15 +204,15 @@ public class SAMLIdentityProviderConfig extends IdentityProviderModel { } public void setPostBindingLogout(boolean postBindingLogout) { - getConfig().put("postBindingLogout", String.valueOf(postBindingLogout)); + getConfig().put(POST_BINDING_LOGOUT, String.valueOf(postBindingLogout)); } public boolean isBackchannelSupported() { - return Boolean.valueOf(getConfig().get("backchannelSupported")); + return Boolean.valueOf(getConfig().get(BACKCHANNEL_SUPPORTED)); } public void setBackchannelSupported(boolean backchannel) { - getConfig().put("backchannelSupported", String.valueOf(backchannel)); + getConfig().put(BACKCHANNEL_SUPPORTED, String.valueOf(backchannel)); } /** @@ -204,11 +220,11 @@ public class SAMLIdentityProviderConfig extends IdentityProviderModel { * @return Configured ransformer of {@link #DEFAULT_XML_KEY_INFO_KEY_NAME_TRANSFORMER} if not set. */ public XmlKeyInfoKeyNameTransformer getXmlSigKeyInfoKeyNameTransformer() { - return XmlKeyInfoKeyNameTransformer.from(getConfig().get("xmlSigKeyInfoKeyNameTransformer"), DEFAULT_XML_KEY_INFO_KEY_NAME_TRANSFORMER); + return XmlKeyInfoKeyNameTransformer.from(getConfig().get(XML_SIG_KEY_INFO_KEY_NAME_TRANSFORMER), DEFAULT_XML_KEY_INFO_KEY_NAME_TRANSFORMER); } public void setXmlSigKeyInfoKeyNameTransformer(XmlKeyInfoKeyNameTransformer xmlSigKeyInfoKeyNameTransformer) { - getConfig().put("xmlSigKeyInfoKeyNameTransformer", + getConfig().put(XML_SIG_KEY_INFO_KEY_NAME_TRANSFORMER, xmlSigKeyInfoKeyNameTransformer == null ? null : xmlSigKeyInfoKeyNameTransformer.name()); diff --git a/services/src/main/java/org/keycloak/email/DefaultEmailSenderProvider.java b/services/src/main/java/org/keycloak/email/DefaultEmailSenderProvider.java index 7477d843fa..ca3575c7a9 100644 --- a/services/src/main/java/org/keycloak/email/DefaultEmailSenderProvider.java +++ b/services/src/main/java/org/keycloak/email/DefaultEmailSenderProvider.java @@ -20,7 +20,6 @@ package org.keycloak.email; import com.sun.mail.smtp.SMTPMessage; import org.jboss.logging.Logger; import org.keycloak.models.KeycloakSession; -import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.services.ServicesLogger; import org.keycloak.truststore.HostnameVerificationPolicy; @@ -57,20 +56,22 @@ public class DefaultEmailSenderProvider implements EmailSenderProvider { } @Override - public void send(RealmModel realm, UserModel user, String subject, String textBody, String htmlBody) throws EmailException { + public void send(Map config, UserModel user, String subject, String textBody, String htmlBody) throws EmailException { Transport transport = null; try { String address = retrieveEmailAddress(user); - Map config = realm.getSmtpConfig(); Properties props = new Properties(); - props.setProperty("mail.smtp.host", config.get("host")); + + if (config.containsKey("host")) { + props.setProperty("mail.smtp.host", config.get("host")); + } boolean auth = "true".equals(config.get("auth")); boolean ssl = "true".equals(config.get("ssl")); boolean starttls = "true".equals(config.get("starttls")); - if (config.containsKey("port")) { + if (config.containsKey("port") && config.get("port") != null) { props.setProperty("mail.smtp.port", config.get("port")); } @@ -103,13 +104,13 @@ public class DefaultEmailSenderProvider implements EmailSenderProvider { Multipart multipart = new MimeMultipart("alternative"); - if(textBody != null) { + if (textBody != null) { MimeBodyPart textPart = new MimeBodyPart(); textPart.setText(textBody, "UTF-8"); multipart.addBodyPart(textPart); } - if(htmlBody != null) { + if (htmlBody != null) { MimeBodyPart htmlPart = new MimeBodyPart(); htmlPart.setContent(htmlBody, "text/html; charset=UTF-8"); multipart.addBodyPart(htmlPart); @@ -153,13 +154,16 @@ public class DefaultEmailSenderProvider implements EmailSenderProvider { } } - protected InternetAddress toInternetAddress(String email, String displayName) throws UnsupportedEncodingException, AddressException { + protected InternetAddress toInternetAddress(String email, String displayName) throws UnsupportedEncodingException, AddressException, EmailException { + if (email == null || "".equals(email.trim())) { + throw new EmailException("Please provide a valid address", null); + } if (displayName == null || "".equals(displayName.trim())) { return new InternetAddress(email); } return new InternetAddress(email, displayName, "utf-8"); } - + protected String retrieveEmailAddress(UserModel user) { return user.getEmail(); } diff --git a/services/src/main/java/org/keycloak/email/freemarker/FreeMarkerEmailTemplateProvider.java b/services/src/main/java/org/keycloak/email/freemarker/FreeMarkerEmailTemplateProvider.java index 5105eaef41..ddf29a15aa 100755 --- a/services/src/main/java/org/keycloak/email/freemarker/FreeMarkerEmailTemplateProvider.java +++ b/services/src/main/java/org/keycloak/email/freemarker/FreeMarkerEmailTemplateProvider.java @@ -97,7 +97,7 @@ public class FreeMarkerEmailTemplateProvider implements EmailTemplateProvider { @Override public void sendPasswordReset(String link, long expirationInMinutes) throws EmailException { - Map attributes = new HashMap(); + Map attributes = new HashMap(this.attributes); attributes.put("user", new ProfileBean(user)); attributes.put("link", link); attributes.put("linkExpiration", expirationInMinutes); @@ -107,9 +107,22 @@ public class FreeMarkerEmailTemplateProvider implements EmailTemplateProvider { send("passwordResetSubject", "password-reset.ftl", attributes); } + @Override + public void sendSmtpTestEmail(Map config, UserModel user) throws EmailException { + setRealm(session.getContext().getRealm()); + setUser(user); + + Map attributes = new HashMap(this.attributes); + attributes.put("user", new ProfileBean(user)); + attributes.put("realmName", realm.getName()); + + EmailTemplate email = processTemplate("emailTestSubject", Collections.emptyList(), "email-test.ftl", attributes); + send(config, email.getSubject(), email.getTextBody(), email.getHtmlBody()); + } + @Override public void sendConfirmIdentityBrokerLink(String link, long expirationInMinutes) throws EmailException { - Map attributes = new HashMap(); + Map attributes = new HashMap(this.attributes); attributes.put("user", new ProfileBean(user)); attributes.put("link", link); attributes.put("linkExpiration", expirationInMinutes); @@ -129,7 +142,7 @@ public class FreeMarkerEmailTemplateProvider implements EmailTemplateProvider { @Override public void sendExecuteActions(String link, long expirationInMinutes) throws EmailException { - Map attributes = new HashMap(); + Map attributes = new HashMap(this.attributes); attributes.put("user", new ProfileBean(user)); attributes.put("link", link); attributes.put("linkExpiration", expirationInMinutes); @@ -142,7 +155,7 @@ public class FreeMarkerEmailTemplateProvider implements EmailTemplateProvider { @Override public void sendVerifyEmail(String link, long expirationInMinutes) throws EmailException { - Map attributes = new HashMap(); + Map attributes = new HashMap(this.attributes); attributes.put("user", new ProfileBean(user)); attributes.put("link", link); attributes.put("linkExpiration", expirationInMinutes); @@ -156,7 +169,7 @@ public class FreeMarkerEmailTemplateProvider implements EmailTemplateProvider { send(subjectKey, Collections.emptyList(), template, attributes); } - private void send(String subjectKey, List subjectAttributes, String template, Map attributes) throws EmailException { + private EmailTemplate processTemplate(String subjectKey, List subjectAttributes, String template, Map attributes) throws EmailException { try { ThemeProvider themeProvider = session.getProvider(ThemeProvider.class, "extending"); Theme theme = themeProvider.getTheme(realm.getEmailTheme(), Theme.Type.EMAIL); @@ -168,27 +181,39 @@ public class FreeMarkerEmailTemplateProvider implements EmailTemplateProvider { String textTemplate = String.format("text/%s", template); String textBody; try { - textBody = freeMarker.processTemplate(attributes, textTemplate, theme); + textBody = freeMarker.processTemplate(attributes, textTemplate, theme); } catch (final FreeMarkerException e ) { - textBody = null; + textBody = null; } String htmlTemplate = String.format("html/%s", template); String htmlBody; try { - htmlBody = freeMarker.processTemplate(attributes, htmlTemplate, theme); + htmlBody = freeMarker.processTemplate(attributes, htmlTemplate, theme); } catch (final FreeMarkerException e ) { - htmlBody = null; + htmlBody = null; } - send(subject, textBody, htmlBody); + return new EmailTemplate(subject, textBody, htmlBody); + } catch (Exception e) { + throw new EmailException("Failed to template email", e); + } + } + private void send(String subjectKey, List subjectAttributes, String template, Map attributes) throws EmailException { + try { + EmailTemplate email = processTemplate(subjectKey, subjectAttributes, template, attributes); + send(email.getSubject(), email.getTextBody(), email.getHtmlBody()); } catch (Exception e) { throw new EmailException("Failed to template email", e); } } private void send(String subject, String textBody, String htmlBody) throws EmailException { + send(realm.getSmtpConfig(), subject, textBody, htmlBody); + } + + private void send(Map config, String subject, String textBody, String htmlBody) throws EmailException { EmailSenderProvider emailSender = session.getProvider(EmailSenderProvider.class); - emailSender.send(realm, user, subject, textBody, htmlBody); + emailSender.send(config, user, subject, textBody, htmlBody); } @Override @@ -203,4 +228,29 @@ public class FreeMarkerEmailTemplateProvider implements EmailTemplateProvider { return sb.toString(); } + private class EmailTemplate { + + private String subject; + private String textBody; + private String htmlBody; + + public EmailTemplate(String subject, String textBody, String htmlBody) { + this.subject = subject; + this.textBody = textBody; + this.htmlBody = htmlBody; + } + + public String getSubject() { + return subject; + } + + public String getTextBody() { + return textBody; + } + + public String getHtmlBody() { + return htmlBody; + } + } + } diff --git a/services/src/main/java/org/keycloak/forms/account/freemarker/model/ApplicationsBean.java b/services/src/main/java/org/keycloak/forms/account/freemarker/model/ApplicationsBean.java index de5fd930ec..2669c2967e 100755 --- a/services/src/main/java/org/keycloak/forms/account/freemarker/model/ApplicationsBean.java +++ b/services/src/main/java/org/keycloak/forms/account/freemarker/model/ApplicationsBean.java @@ -19,6 +19,7 @@ package org.keycloak.forms.account.freemarker.model; import org.keycloak.common.util.MultivaluedHashMap; import org.keycloak.models.ClientModel; +import org.keycloak.models.Constants; import org.keycloak.models.KeycloakSession; import org.keycloak.models.ProtocolMapperModel; import org.keycloak.models.RealmModel; @@ -27,8 +28,10 @@ import org.keycloak.models.UserConsentModel; import org.keycloak.models.UserModel; import org.keycloak.protocol.oidc.TokenManager; import org.keycloak.services.managers.UserSessionManager; +import org.keycloak.services.resources.admin.permissions.AdminPermissions; import java.util.ArrayList; +import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Set; @@ -51,10 +54,17 @@ public class ApplicationsBean { continue; } - Set availableRoles = TokenManager.getAccess(null, false, client, user); - // Don't show applications, which user doesn't have access into (any available roles) - if (availableRoles.isEmpty()) { - continue; + Set availableRoles = new HashSet<>(); + if (client.getClientId().equals(Constants.ADMIN_CLI_CLIENT_ID) + || client.getClientId().equals(Constants.ADMIN_CONSOLE_CLIENT_ID)) { + if (!AdminPermissions.realms(session, realm, user).isAdmin()) continue; + + } else { + availableRoles = TokenManager.getAccess(null, false, client, user); + // Don't show applications, which user doesn't have access into (any available roles) + if (availableRoles.isEmpty()) { + continue; + } } List realmRolesAvailable = new LinkedList(); MultivaluedHashMap resourceRolesAvailable = new MultivaluedHashMap(); @@ -140,7 +150,48 @@ public class ApplicationsBean { public MultivaluedHashMap getResourceRolesGranted() { return resourceRolesGranted; } - + + public String getEffectiveUrl() { + String rootUrl = getClient().getRootUrl(); + String baseUrl = getClient().getBaseUrl(); + + if (rootUrl == null) rootUrl = ""; + if (baseUrl == null) baseUrl = ""; + + if (rootUrl.equals("") && baseUrl.equals("")) { + return ""; + } + + if (rootUrl.equals("") && !baseUrl.equals("")) { + return baseUrl; + } + + if (!rootUrl.equals("") && baseUrl.equals("")) { + return rootUrl; + } + + if (isBaseUrlRelative() && !rootUrl.equals("")) { + return concatUrls(rootUrl, baseUrl); + } + + return baseUrl; + } + + private String concatUrls(String u1, String u2) { + if (u1.endsWith("/")) u1 = u1.substring(0, u1.length() - 1); + if (u2.startsWith("/")) u2 = u2.substring(1); + return u1 + "/" + u2; + } + + private boolean isBaseUrlRelative() { + String baseUrl = getClient().getBaseUrl(); + if (baseUrl.equals("")) return false; + if (baseUrl.startsWith("/")) return true; + if (baseUrl.startsWith("./")) return true; + if (baseUrl.startsWith("../")) return true; + return false; + } + public ClientModel getClient() { return client; } diff --git a/services/src/main/java/org/keycloak/forms/login/freemarker/FreeMarkerLoginFormsProvider.java b/services/src/main/java/org/keycloak/forms/login/freemarker/FreeMarkerLoginFormsProvider.java index d7eb01cf1c..8ec6a5b299 100755 --- a/services/src/main/java/org/keycloak/forms/login/freemarker/FreeMarkerLoginFormsProvider.java +++ b/services/src/main/java/org/keycloak/forms/login/freemarker/FreeMarkerLoginFormsProvider.java @@ -449,7 +449,7 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider { public Response createIdpLinkEmailPage() { BrokeredIdentityContext brokerContext = (BrokeredIdentityContext) this.attributes.get(IDENTITY_PROVIDER_BROKER_CONTEXT); String idpAlias = brokerContext.getIdpConfig().getAlias(); - idpAlias = ObjectUtil.capitalize(idpAlias);; + idpAlias = ObjectUtil.capitalize(idpAlias); setMessage(MessageType.WARNING, Messages.LINK_IDP, idpAlias); return createResponse(LoginFormsPages.LOGIN_IDP_LINK_EMAIL); diff --git a/services/src/main/java/org/keycloak/protocol/AuthorizationEndpointBase.java b/services/src/main/java/org/keycloak/protocol/AuthorizationEndpointBase.java index 9c1e5a5e8e..c06caa0737 100755 --- a/services/src/main/java/org/keycloak/protocol/AuthorizationEndpointBase.java +++ b/services/src/main/java/org/keycloak/protocol/AuthorizationEndpointBase.java @@ -22,6 +22,7 @@ import org.jboss.resteasy.spi.HttpRequest; import org.keycloak.authentication.AuthenticationProcessor; import org.keycloak.common.ClientConnection; import org.keycloak.events.Details; +import org.keycloak.events.Errors; import org.keycloak.events.EventBuilder; import org.keycloak.models.AuthenticationFlowModel; import org.keycloak.models.ClientModel; @@ -29,9 +30,12 @@ import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserSessionModel; import org.keycloak.protocol.LoginProtocol.Error; +import org.keycloak.services.ErrorPageException; import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.services.managers.AuthenticationSessionManager; import org.keycloak.services.managers.ClientSessionCode; +import org.keycloak.services.managers.UserSessionCrossDCManager; +import org.keycloak.services.messages.Messages; import org.keycloak.services.resources.LoginActionsService; import org.keycloak.services.util.CacheControlUtil; import org.keycloak.services.util.AuthenticationFlowURLHelper; @@ -62,7 +66,7 @@ public abstract class AuthorizationEndpointBase { @Context protected HttpHeaders headers; @Context - protected HttpRequest request; + protected HttpRequest httpRequest; @Context protected KeycloakSession session; @Context @@ -84,7 +88,7 @@ public abstract class AuthorizationEndpointBase { .setRealm(realm) .setSession(session) .setUriInfo(uriInfo) - .setRequest(request); + .setRequest(httpRequest); authSession.setAuthNote(AuthenticationProcessor.CURRENT_FLOW_PATH, flowPath); @@ -147,6 +151,19 @@ public abstract class AuthorizationEndpointBase { return realm.getBrowserFlow(); } + protected void checkSsl() { + if (!uriInfo.getBaseUri().getScheme().equals("https") && realm.getSslRequired().isRequired(clientConnection)) { + event.error(Errors.SSL_REQUIRED); + throw new ErrorPageException(session, Messages.HTTPS_REQUIRED); + } + } + + protected void checkRealm() { + if (!realm.isEnabled()) { + event.error(Errors.REALM_DISABLED); + throw new ErrorPageException(session, Messages.REALM_NOT_ENABLED); + } + } protected AuthorizationEndpointChecks getOrCreateAuthenticationSession(ClientModel client, String requestState) { AuthenticationSessionManager manager = new AuthenticationSessionManager(session); @@ -192,7 +209,7 @@ public abstract class AuthorizationEndpointBase { } } - UserSessionModel userSession = authSessionId==null ? null : session.sessions().getUserSession(realm, authSessionId); + UserSessionModel userSession = authSessionId==null ? null : new UserSessionCrossDCManager(session).getUserSessionIfExistsRemotely(realm, authSessionId); if (userSession != null) { logger.debugf("Sent request to authz endpoint. We don't have authentication session with ID '%s' but we have userSession. Will re-create authentication session with same ID", authSessionId); diff --git a/services/src/main/java/org/keycloak/protocol/docker/DockerAuthV2Protocol.java b/services/src/main/java/org/keycloak/protocol/docker/DockerAuthV2Protocol.java new file mode 100644 index 0000000000..3a7a3247ba --- /dev/null +++ b/services/src/main/java/org/keycloak/protocol/docker/DockerAuthV2Protocol.java @@ -0,0 +1,184 @@ +package org.keycloak.protocol.docker; + +import org.jboss.logging.Logger; +import org.jboss.resteasy.specimpl.ResponseBuilderImpl; +import org.keycloak.events.EventBuilder; +import org.keycloak.events.EventType; +import org.keycloak.jose.jws.JWSBuilder; +import org.keycloak.models.AuthenticatedClientSessionModel; +import org.keycloak.models.ClientModel; +import org.keycloak.models.KeyManager; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.ProtocolMapperModel; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserSessionModel; +import org.keycloak.models.utils.KeycloakModelUtils; +import org.keycloak.protocol.LoginProtocol; +import org.keycloak.protocol.ProtocolMapper; +import org.keycloak.protocol.docker.mapper.DockerAuthV2AttributeMapper; +import org.keycloak.representations.docker.DockerResponse; +import org.keycloak.representations.docker.DockerResponseToken; +import org.keycloak.services.ErrorResponseException; +import org.keycloak.services.managers.ClientSessionCode; +import org.keycloak.sessions.AuthenticationSessionModel; +import org.keycloak.util.TokenUtil; + +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.text.SimpleDateFormat; +import java.util.Date; +import java.util.Set; + +public class DockerAuthV2Protocol implements LoginProtocol { + protected static final Logger logger = Logger.getLogger(DockerEndpoint.class); + + public static final String LOGIN_PROTOCOL = "docker-v2"; + public static final String ACCOUNT_PARAM = "account"; + public static final String SERVICE_PARAM = "service"; + public static final String SCOPE_PARAM = "scope"; + public static final String ISSUER = "docker.iss"; // don't want to overlap with OIDC notes + public static final String ISO_8601_DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ss'Z'"; + + private KeycloakSession session; + private RealmModel realm; + private UriInfo uriInfo; + private HttpHeaders headers; + private EventBuilder event; + + public DockerAuthV2Protocol() { + } + + public DockerAuthV2Protocol(final KeycloakSession session, final RealmModel realm, final UriInfo uriInfo, final HttpHeaders headers, final EventBuilder event) { + this.session = session; + this.realm = realm; + this.uriInfo = uriInfo; + this.headers = headers; + this.event = event; + } + + @Override + public LoginProtocol setSession(final KeycloakSession session) { + this.session = session; + return this; + } + + @Override + public LoginProtocol setRealm(final RealmModel realm) { + this.realm = realm; + return this; + } + + @Override + public LoginProtocol setUriInfo(final UriInfo uriInfo) { + this.uriInfo = uriInfo; + return this; + } + + @Override + public LoginProtocol setHttpHeaders(final HttpHeaders headers) { + this.headers = headers; + return this; + } + + @Override + public LoginProtocol setEventBuilder(final EventBuilder event) { + this.event = event; + return this; + } + + @Override + public Response authenticated(final UserSessionModel userSession, final AuthenticatedClientSessionModel clientSession) { + // First, create a base response token with realm + user values populated + final ClientModel client = clientSession.getClient(); + DockerResponseToken responseToken = new DockerResponseToken() + .id(KeycloakModelUtils.generateId()) + .type(TokenUtil.TOKEN_TYPE_BEARER) + .issuer(clientSession.getNote(DockerAuthV2Protocol.ISSUER)) + .subject(userSession.getUser().getUsername()) + .issuedNow() + .audience(client.getClientId()) + .issuedFor(client.getClientId()); + + // since realm access token is given in seconds + final int accessTokenLifespan = realm.getAccessTokenLifespan(); + responseToken.notBefore(responseToken.getIssuedAt()) + .expiration(responseToken.getIssuedAt() + accessTokenLifespan); + + // Next, allow mappers to decorate the token to add/remove scopes as appropriate + final ClientSessionCode accessCode = new ClientSessionCode<>(session, realm, clientSession); + final Set mappings = accessCode.getRequestedProtocolMappers(); + for (final ProtocolMapperModel mapping : mappings) { + final ProtocolMapper mapper = (ProtocolMapper) session.getKeycloakSessionFactory().getProviderFactory(ProtocolMapper.class, mapping.getProtocolMapper()); + if (mapper instanceof DockerAuthV2AttributeMapper) { + final DockerAuthV2AttributeMapper dockerAttributeMapper = (DockerAuthV2AttributeMapper) mapper; + if (dockerAttributeMapper.appliesTo(responseToken)) { + responseToken = dockerAttributeMapper.transformDockerResponseToken(responseToken, mapping, session, userSession, clientSession); + } + } + } + + try { + // Finally, construct the response to the docker client with the token + metadata + if (event.getEvent() != null && EventType.LOGIN.equals(event.getEvent().getType())) { + final KeyManager.ActiveRsaKey activeKey = session.keys().getActiveRsaKey(realm); + final String encodedToken = new JWSBuilder() + .kid(new DockerKeyIdentifier(activeKey.getPublicKey()).toString()) + .type("JWT") + .jsonContent(responseToken) + .rsa256(activeKey.getPrivateKey()); + final String expiresInIso8601String = new SimpleDateFormat(ISO_8601_DATE_FORMAT).format(new Date(responseToken.getIssuedAt() * 1000L)); + + final DockerResponse responseEntity = new DockerResponse() + .setToken(encodedToken) + .setExpires_in(accessTokenLifespan) + .setIssued_at(expiresInIso8601String); + return new ResponseBuilderImpl().status(Response.Status.OK).header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON).entity(responseEntity).build(); + } else { + logger.errorv("Unable to handle request for event type {0}. Currently only LOGIN event types are supported by docker protocol.", event.getEvent() == null ? "null" : event.getEvent().getType()); + throw new ErrorResponseException("invalid_request", "Event type not supported", Response.Status.BAD_REQUEST); + } + } catch (final InstantiationException e) { + logger.errorv("Error attempting to create Key ID for Docker JOSE header: ", e.getMessage()); + throw new ErrorResponseException("token_error", "Unable to construct JOSE header for JWT", Response.Status.INTERNAL_SERVER_ERROR); + } + + } + + @Override + public Response sendError(final AuthenticationSessionModel clientSession, final LoginProtocol.Error error) { + return new ResponseBuilderImpl().status(Response.Status.INTERNAL_SERVER_ERROR).build(); + } + + @Override + public void backchannelLogout(final UserSessionModel userSession, final AuthenticatedClientSessionModel clientSession) { + errorResponse(userSession, "backchannelLogout"); + + } + + @Override + public Response frontchannelLogout(final UserSessionModel userSession, final AuthenticatedClientSessionModel clientSession) { + return errorResponse(userSession, "frontchannelLogout"); + } + + @Override + public Response finishLogout(final UserSessionModel userSession) { + return errorResponse(userSession, "finishLogout"); + } + + @Override + public boolean requireReauthentication(final UserSessionModel userSession, final AuthenticationSessionModel clientSession) { + return true; + } + + private Response errorResponse(final UserSessionModel userSession, final String methodName) { + logger.errorv("User {0} attempted to invoke unsupported method {1} on docker protocol.", userSession.getUser().getUsername(), methodName); + throw new ErrorResponseException("invalid_request", String.format("Attempted to invoke unsupported docker method %s", methodName), Response.Status.BAD_REQUEST); + } + + @Override + public void close() { + // no-op + } +} diff --git a/services/src/main/java/org/keycloak/protocol/docker/DockerAuthV2ProtocolFactory.java b/services/src/main/java/org/keycloak/protocol/docker/DockerAuthV2ProtocolFactory.java new file mode 100644 index 0000000000..be4c6c0bac --- /dev/null +++ b/services/src/main/java/org/keycloak/protocol/docker/DockerAuthV2ProtocolFactory.java @@ -0,0 +1,86 @@ +package org.keycloak.protocol.docker; + +import org.keycloak.common.Profile; +import org.keycloak.events.EventBuilder; +import org.keycloak.models.ClientModel; +import org.keycloak.models.ClientTemplateModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.ProtocolMapperModel; +import org.keycloak.models.RealmModel; +import org.keycloak.protocol.AbstractLoginProtocolFactory; +import org.keycloak.protocol.LoginProtocol; +import org.keycloak.protocol.docker.mapper.AllowAllDockerProtocolMapper; +import org.keycloak.provider.EnvironmentDependentProviderFactory; +import org.keycloak.representations.idm.ClientRepresentation; +import org.keycloak.representations.idm.ClientTemplateRepresentation; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class DockerAuthV2ProtocolFactory extends AbstractLoginProtocolFactory implements EnvironmentDependentProviderFactory { + + static List builtins = new ArrayList<>(); + static List defaultBuiltins = new ArrayList<>(); + + static { + final ProtocolMapperModel addAllRequestedScopeMapper = new ProtocolMapperModel(); + addAllRequestedScopeMapper.setName(AllowAllDockerProtocolMapper.PROVIDER_ID); + addAllRequestedScopeMapper.setProtocolMapper(AllowAllDockerProtocolMapper.PROVIDER_ID); + addAllRequestedScopeMapper.setProtocol(DockerAuthV2Protocol.LOGIN_PROTOCOL); + addAllRequestedScopeMapper.setConsentRequired(false); + addAllRequestedScopeMapper.setConfig(Collections.EMPTY_MAP); + builtins.add(addAllRequestedScopeMapper); + defaultBuiltins.add(addAllRequestedScopeMapper); + } + + @Override + protected void addDefaults(final ClientModel client) { + defaultBuiltins.forEach(builtinMapper -> client.addProtocolMapper(builtinMapper)); + } + + @Override + public List getBuiltinMappers() { + return builtins; + } + + @Override + public List getDefaultBuiltinMappers() { + return defaultBuiltins; + } + + @Override + public Object createProtocolEndpoint(final RealmModel realm, final EventBuilder event) { + return new DockerV2LoginProtocolService(realm, event); + } + + @Override + public void setupClientDefaults(final ClientRepresentation rep, final ClientModel newClient) { + // no-op + } + + @Override + public void setupTemplateDefaults(final ClientTemplateRepresentation clientRep, final ClientTemplateModel newClient) { + // no-op + } + + @Override + public LoginProtocol create(final KeycloakSession session) { + return new DockerAuthV2Protocol().setSession(session); + } + + @Override + public String getId() { + return DockerAuthV2Protocol.LOGIN_PROTOCOL; + } + + @Override + public boolean isSupported() { + return Profile.isFeatureEnabled(Profile.Feature.DOCKER); + } + + @Override + public int order() { + return -100; + } +} diff --git a/services/src/main/java/org/keycloak/protocol/docker/DockerAuthenticator.java b/services/src/main/java/org/keycloak/protocol/docker/DockerAuthenticator.java new file mode 100644 index 0000000000..b2c2b37886 --- /dev/null +++ b/services/src/main/java/org/keycloak/protocol/docker/DockerAuthenticator.java @@ -0,0 +1,76 @@ +package org.keycloak.protocol.docker; + +import org.jboss.logging.Logger; +import org.jboss.resteasy.specimpl.ResponseBuilderImpl; +import org.keycloak.authentication.AuthenticationFlowContext; +import org.keycloak.authentication.AuthenticationFlowError; +import org.keycloak.events.Errors; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserModel; +import org.keycloak.protocol.saml.profile.ecp.authenticator.HttpBasicAuthenticator; +import org.keycloak.representations.docker.DockerAccess; +import org.keycloak.representations.docker.DockerError; +import org.keycloak.representations.docker.DockerErrorResponseToken; + +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import java.util.Collections; +import java.util.Locale; +import java.util.Optional; + +public class DockerAuthenticator extends HttpBasicAuthenticator { + private static final Logger logger = Logger.getLogger(DockerAuthenticator.class); + + public static final String ID = "docker-http-basic-authenticator"; + + @Override + protected void notValidCredentialsAction(final AuthenticationFlowContext context, final RealmModel realm, final UserModel user) { + invalidUserAction(context, realm, user.getUsername(), context.getSession().getContext().resolveLocale(user)); + } + + @Override + protected void nullUserAction(final AuthenticationFlowContext context, final RealmModel realm, final String userId) { + final String localeString = Optional.ofNullable(realm.getDefaultLocale()).orElse(Locale.ENGLISH.toString()); + invalidUserAction(context, realm, userId, new Locale(localeString)); + } + + @Override + protected void userDisabledAction(AuthenticationFlowContext context, RealmModel realm, UserModel user) { + context.getEvent().user(user); + context.getEvent().error(Errors.USER_DISABLED); + + final DockerError error = new DockerError("UNAUTHORIZED","Invalid username or password.", + Collections.singletonList(new DockerAccess(context.getAuthenticationSession().getClientNote(DockerAuthV2Protocol.SCOPE_PARAM)))); + + context.failure(AuthenticationFlowError.USER_DISABLED, new ResponseBuilderImpl() + .status(Response.Status.UNAUTHORIZED) + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON) + .entity(new DockerErrorResponseToken(Collections.singletonList(error))) + .build()); + } + + /** + * For Docker protocol the same error message will be returned for invalid credentials and incorrect user name. For SAML + * ECP, there is a different behavior for each. + */ + private void invalidUserAction(final AuthenticationFlowContext context, final RealmModel realm, final String userId, final Locale locale) { + context.getEvent().user(userId); + context.getEvent().error(Errors.INVALID_USER_CREDENTIALS); + + final DockerError error = new DockerError("UNAUTHORIZED","Invalid username or password.", + Collections.singletonList(new DockerAccess(context.getAuthenticationSession().getClientNote(DockerAuthV2Protocol.SCOPE_PARAM)))); + + context.failure(AuthenticationFlowError.INVALID_USER, new ResponseBuilderImpl() + .status(Response.Status.UNAUTHORIZED) + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON) + .entity(new DockerErrorResponseToken(Collections.singletonList(error))) + .build()); + } + + @Override + public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) { + return true; + } +} diff --git a/services/src/main/java/org/keycloak/protocol/docker/DockerAuthenticatorFactory.java b/services/src/main/java/org/keycloak/protocol/docker/DockerAuthenticatorFactory.java new file mode 100644 index 0000000000..9bba9c490c --- /dev/null +++ b/services/src/main/java/org/keycloak/protocol/docker/DockerAuthenticatorFactory.java @@ -0,0 +1,84 @@ +package org.keycloak.protocol.docker; + +import org.keycloak.Config; +import org.keycloak.authentication.Authenticator; +import org.keycloak.authentication.AuthenticatorFactory; +import org.keycloak.common.Profile; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.provider.EnvironmentDependentProviderFactory; +import org.keycloak.provider.ProviderConfigProperty; + +import java.util.Collections; +import java.util.List; + +import static org.keycloak.models.AuthenticationExecutionModel.Requirement; + +public class DockerAuthenticatorFactory implements AuthenticatorFactory { + + @Override + public String getHelpText() { + return "Uses HTTP Basic authentication to validate docker users, returning a docker error token on auth failure"; + } + + @Override + public List getConfigProperties() { + return Collections.emptyList(); + } + + @Override + public String getDisplayType() { + return "Docker Authenticator"; + } + + @Override + public String getReferenceCategory() { + return "docker"; + } + + @Override + public boolean isConfigurable() { + return false; + } + + private static final Requirement[] REQUIREMENT_CHOICES = { + Requirement.REQUIRED, + }; + + @Override + public Requirement[] getRequirementChoices() { + return REQUIREMENT_CHOICES; + } + + + @Override + public boolean isUserSetupAllowed() { + return false; + } + + @Override + public Authenticator create(KeycloakSession session) { + return new DockerAuthenticator(); + } + + @Override + public void init(Config.Scope config) { + // no-op + } + + @Override + public void postInit(KeycloakSessionFactory factory) { + // no-op + } + + @Override + public void close() { + // no-op + } + + @Override + public String getId() { + return DockerAuthenticator.ID; + } + +} diff --git a/services/src/main/java/org/keycloak/protocol/docker/DockerEndpoint.java b/services/src/main/java/org/keycloak/protocol/docker/DockerEndpoint.java new file mode 100644 index 0000000000..8cf50e8bc6 --- /dev/null +++ b/services/src/main/java/org/keycloak/protocol/docker/DockerEndpoint.java @@ -0,0 +1,103 @@ +package org.keycloak.protocol.docker; + +import org.jboss.logging.Logger; +import org.keycloak.common.Profile; +import org.keycloak.events.EventBuilder; +import org.keycloak.events.EventType; +import org.keycloak.models.AuthenticationFlowModel; +import org.keycloak.models.ClientModel; +import org.keycloak.models.RealmModel; +import org.keycloak.protocol.AuthorizationEndpointBase; +import org.keycloak.protocol.oidc.endpoints.request.AuthorizationEndpointRequest; +import org.keycloak.protocol.oidc.endpoints.request.AuthorizationEndpointRequestParserProcessor; +import org.keycloak.services.ErrorResponseException; +import org.keycloak.services.Urls; +import org.keycloak.services.util.CacheControlUtil; +import org.keycloak.sessions.AuthenticationSessionModel; +import org.keycloak.sessions.CommonClientSessionModel; +import org.keycloak.utils.ProfileHelper; + +import javax.ws.rs.GET; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.core.Response; + +/** + * Implements a docker-client understandable format. + */ +public class DockerEndpoint extends AuthorizationEndpointBase { + protected static final Logger logger = Logger.getLogger(DockerEndpoint.class); + + private final EventType login; + private String account; + private String service; + private String scope; + private ClientModel client; + private AuthenticationSessionModel authenticationSession; + + public DockerEndpoint(final RealmModel realm, final EventBuilder event, final EventType login) { + super(realm, event); + this.login = login; + } + + @GET + public Response build() { + ProfileHelper.requireFeature(Profile.Feature.DOCKER); + + final MultivaluedMap params = uriInfo.getQueryParameters(); + + account = params.getFirst(DockerAuthV2Protocol.ACCOUNT_PARAM); + if (account == null) { + logger.debug("Account parameter not provided by docker auth. This is techincally required, but not actually used since " + + "username is provided by Basic auth header."); + } + service = params.getFirst(DockerAuthV2Protocol.SERVICE_PARAM); + if (service == null) { + throw new ErrorResponseException("invalid_request", "service parameter must be provided", Response.Status.BAD_REQUEST); + } + client = realm.getClientByClientId(service); + if (client == null) { + logger.errorv("Failed to lookup client given by service={0} parameter for realm: {1}.", service, realm.getName()); + throw new ErrorResponseException("invalid_client", "Client specified by 'service' parameter does not exist", Response.Status.BAD_REQUEST); + } + scope = params.getFirst(DockerAuthV2Protocol.SCOPE_PARAM); + + checkSsl(); + checkRealm(); + + final AuthorizationEndpointRequest authRequest = AuthorizationEndpointRequestParserProcessor.parseRequest(event, session, client, params); + AuthorizationEndpointChecks checks = getOrCreateAuthenticationSession(client, authRequest.getState()); + if (checks.response != null) { + return checks.response; + } + + authenticationSession = checks.authSession; + updateAuthenticationSession(); + + // So back button doesn't work + CacheControlUtil.noBackButtonCacheControlHeader(); + + return handleBrowserAuthenticationRequest(authenticationSession, new DockerAuthV2Protocol(session, realm, uriInfo, headers, event.event(login)), false, false); + } + + private void updateAuthenticationSession() { + authenticationSession.setProtocol(DockerAuthV2Protocol.LOGIN_PROTOCOL); + authenticationSession.setAction(CommonClientSessionModel.Action.AUTHENTICATE.name()); + + // Docker specific stuff + authenticationSession.setClientNote(DockerAuthV2Protocol.ACCOUNT_PARAM, account); + authenticationSession.setClientNote(DockerAuthV2Protocol.SERVICE_PARAM, service); + authenticationSession.setClientNote(DockerAuthV2Protocol.SCOPE_PARAM, scope); + authenticationSession.setClientNote(DockerAuthV2Protocol.ISSUER, Urls.realmIssuer(uriInfo.getBaseUri(), realm.getName())); + + } + + @Override + protected AuthenticationFlowModel getAuthenticationFlow() { + return realm.getDockerAuthenticationFlow(); + } + + @Override + protected boolean isNewRequest(final AuthenticationSessionModel authSession, final ClientModel clientFromRequest, final String requestState) { + return true; + } +} diff --git a/services/src/main/java/org/keycloak/protocol/docker/DockerKeyIdentifier.java b/services/src/main/java/org/keycloak/protocol/docker/DockerKeyIdentifier.java new file mode 100644 index 0000000000..384f2182a5 --- /dev/null +++ b/services/src/main/java/org/keycloak/protocol/docker/DockerKeyIdentifier.java @@ -0,0 +1,127 @@ +package org.keycloak.protocol.docker; + +import org.keycloak.models.utils.Base32; + +import java.security.Key; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Set; +import java.util.function.BiConsumer; +import java.util.function.BinaryOperator; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Collector; +import java.util.stream.Stream; + +/** + * The “kid” field has to be in a libtrust fingerprint compatible format. Such a format can be generated by following steps: + * 1) Take the DER encoded public key which the JWT token was signed against. + * 2) Create a SHA256 hash out of it and truncate to 240bits. + * 3) Split the result into 12 base32 encoded groups with : as delimiter. + * + * Ex: "kid": "PYYO:TEWU:V7JH:26JV:AQTZ:LJC3:SXVJ:XGHA:34F2:2LAQ:ZRMK:Z7Q6" + * + * @see https://docs.docker.com/registry/spec/auth/jwt/ + * @see https://github.com/docker/libtrust/blob/master/key.go#L24 + */ +public class DockerKeyIdentifier { + + private final String identifier; + + public DockerKeyIdentifier(final Key key) throws InstantiationException { + try { + final MessageDigest sha256 = MessageDigest.getInstance("SHA-256"); + final byte[] hashed = sha256.digest(key.getEncoded()); + final byte[] hashedTruncated = truncateToBitLength(240, hashed); + final String base32Id = Base32.encode(hashedTruncated); + identifier = byteStream(base32Id.getBytes()).collect(new DelimitingCollector()); + } catch (final NoSuchAlgorithmException e) { + throw new InstantiationException("Could not instantiate docker key identifier, no SHA-256 algorithm available."); + } + } + + // ugh. + private Stream byteStream(final byte[] bytes) { + final Collection colectionedBytes = new ArrayList<>(); + for (final byte aByte : bytes) { + colectionedBytes.add(aByte); + } + + return colectionedBytes.stream(); + } + + private byte[] truncateToBitLength(final int bitLength, final byte[] arrayToTruncate) { + if (bitLength % 8 != 0) { + throw new IllegalArgumentException("Bit length for truncation of byte array given as a number not divisible by 8"); + } + + final int numberOfBytes = bitLength / 8; + return Arrays.copyOfRange(arrayToTruncate, 0, numberOfBytes); + } + + @Override + public String toString() { + return identifier; + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (!(o instanceof DockerKeyIdentifier)) return false; + + final DockerKeyIdentifier that = (DockerKeyIdentifier) o; + + return identifier != null ? identifier.equals(that.identifier) : that.identifier == null; + + } + + @Override + public int hashCode() { + return identifier != null ? identifier.hashCode() : 0; + } + + // Could probably be generalized with size and delimiter arguments, but leaving it here for now until someone else needs it. + public static class DelimitingCollector implements Collector { + + @Override + public Supplier supplier() { + return () -> new StringBuilder(); + } + + @Override + public BiConsumer accumulator() { + return ((stringBuilder, aByte) -> { + if (needsDelimiter(4, ":", stringBuilder)) { + stringBuilder.append(":"); + } + + stringBuilder.append(new String(new byte[]{aByte})); + }); + } + + private static boolean needsDelimiter(final int maxLength, final String delimiter, final StringBuilder builder) { + final int lastDelimiter = builder.lastIndexOf(delimiter); + final int charsSinceLastDelimiter = builder.length() - lastDelimiter; + return charsSinceLastDelimiter > maxLength; + } + + @Override + public BinaryOperator combiner() { + return ((left, right) -> new StringBuilder(left.toString()).append(right.toString())); + } + + @Override + public Function finisher() { + return StringBuilder::toString; + } + + @Override + public Set characteristics() { + return Collections.emptySet(); + } + } +} diff --git a/services/src/main/java/org/keycloak/protocol/docker/DockerV2LoginProtocolService.java b/services/src/main/java/org/keycloak/protocol/docker/DockerV2LoginProtocolService.java new file mode 100644 index 0000000000..a0dad58129 --- /dev/null +++ b/services/src/main/java/org/keycloak/protocol/docker/DockerV2LoginProtocolService.java @@ -0,0 +1,70 @@ +package org.keycloak.protocol.docker; + +import org.jboss.resteasy.spi.ResteasyProviderFactory; +import org.keycloak.common.Profile; +import org.keycloak.events.EventBuilder; +import org.keycloak.events.EventType; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.protocol.oidc.TokenManager; +import org.keycloak.services.resources.RealmsResource; +import org.keycloak.utils.ProfileHelper; + +import javax.ws.rs.Path; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.UriBuilder; +import javax.ws.rs.core.UriInfo; + +public class DockerV2LoginProtocolService { + + private final RealmModel realm; + private final TokenManager tokenManager; + private final EventBuilder event; + + @Context + private UriInfo uriInfo; + + @Context + private KeycloakSession session; + + @Context + private HttpHeaders headers; + + public DockerV2LoginProtocolService(final RealmModel realm, final EventBuilder event) { + this.realm = realm; + this.tokenManager = new TokenManager(); + this.event = event; + } + + public static UriBuilder authProtocolBaseUrl(final UriInfo uriInfo) { + final UriBuilder baseUriBuilder = uriInfo.getBaseUriBuilder(); + return authProtocolBaseUrl(baseUriBuilder); + } + + public static UriBuilder authProtocolBaseUrl(final UriBuilder baseUriBuilder) { + return baseUriBuilder.path(RealmsResource.class).path("{realm}/protocol/" + DockerAuthV2Protocol.LOGIN_PROTOCOL); + } + + public static UriBuilder authUrl(final UriInfo uriInfo) { + final UriBuilder baseUriBuilder = uriInfo.getBaseUriBuilder(); + return authUrl(baseUriBuilder); + } + + public static UriBuilder authUrl(final UriBuilder baseUriBuilder) { + final UriBuilder uriBuilder = authProtocolBaseUrl(baseUriBuilder); + return uriBuilder.path(DockerV2LoginProtocolService.class, "auth"); + } + + /** + * Authorization endpoint + */ + @Path("auth") + public Object auth() { + ProfileHelper.requireFeature(Profile.Feature.DOCKER); + + final DockerEndpoint endpoint = new DockerEndpoint(realm, event, EventType.LOGIN); + ResteasyProviderFactory.getInstance().injectProperties(endpoint); + return endpoint; + } +} diff --git a/services/src/main/java/org/keycloak/protocol/docker/installation/DockerComposeYamlInstallationProvider.java b/services/src/main/java/org/keycloak/protocol/docker/installation/DockerComposeYamlInstallationProvider.java new file mode 100644 index 0000000000..72ade3116c --- /dev/null +++ b/services/src/main/java/org/keycloak/protocol/docker/installation/DockerComposeYamlInstallationProvider.java @@ -0,0 +1,148 @@ +package org.keycloak.protocol.docker.installation; + +import org.jboss.logging.Logger; +import org.keycloak.Config; +import org.keycloak.models.ClientModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.models.RealmModel; +import org.keycloak.protocol.ClientInstallationProvider; +import org.keycloak.protocol.docker.DockerAuthV2Protocol; +import org.keycloak.protocol.docker.installation.compose.DockerComposeZipContent; + +import javax.ws.rs.core.Response; +import java.io.BufferedReader; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.URI; +import java.net.URL; +import java.security.cert.Certificate; +import java.util.stream.Collectors; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +public class DockerComposeYamlInstallationProvider implements ClientInstallationProvider { + private static Logger log = Logger.getLogger(DockerComposeYamlInstallationProvider.class); + + public static final String ROOT_DIR = "keycloak-docker-compose-yaml/"; + + @Override + public ClientInstallationProvider create(final KeycloakSession session) { + return this; + } + + @Override + public void init(final Config.Scope config) { + // no-op + } + + @Override + public void postInit(final KeycloakSessionFactory factory) { + // no-op + } + + @Override + public void close() { + // no-op + } + + @Override + public String getId() { + return "docker-v2-compose-yaml"; + } + + @Override + public Response generateInstallation(final KeycloakSession session, final RealmModel realm, final ClientModel client, final URI serverBaseUri) { + final ByteArrayOutputStream byteStream = new ByteArrayOutputStream(); + final ZipOutputStream zipOutput = new ZipOutputStream(byteStream); + + try { + return generateInstallation(zipOutput, byteStream, session.keys().getActiveRsaKey(realm).getCertificate(), session.getContext().getAuthServerUrl().toURL(), realm.getName(), client.getClientId()); + } catch (final IOException e) { + try { + zipOutput.close(); + } catch (final IOException ex) { + // do nothing, already in an exception + } + try { + byteStream.close(); + } catch (final IOException ex) { + // do nothing, already in an exception + } + throw new RuntimeException("Error occurred during attempt to generate docker-compose yaml installation files", e); + } + } + + public Response generateInstallation(final ZipOutputStream zipOutput, final ByteArrayOutputStream byteStream, final Certificate realmCert, final URL realmBaseURl, + final String realmName, final String clientName) throws IOException { + final DockerComposeZipContent zipContent = new DockerComposeZipContent(realmCert, realmBaseURl, realmName, clientName); + + zipOutput.putNextEntry(new ZipEntry(ROOT_DIR)); + + // Write docker compose file + zipOutput.putNextEntry(new ZipEntry(ROOT_DIR + "docker-compose.yaml")); + zipOutput.write(zipContent.getYamlFile().generateDockerComposeFileBytes()); + zipOutput.closeEntry(); + + // Write data directory + zipOutput.putNextEntry(new ZipEntry(ROOT_DIR + zipContent.getDataDirectoryName() + "/")); + zipOutput.putNextEntry(new ZipEntry(ROOT_DIR + zipContent.getDataDirectoryName() + "/.gitignore")); + zipOutput.write("*".getBytes()); + zipOutput.closeEntry(); + + // Write certificates + final String certsDirectory = ROOT_DIR + zipContent.getCertsDirectory().getDirectoryName() + "/"; + zipOutput.putNextEntry(new ZipEntry(certsDirectory)); + zipOutput.putNextEntry(new ZipEntry(certsDirectory + zipContent.getCertsDirectory().getLocalhostCertFile().getKey())); + zipOutput.write(zipContent.getCertsDirectory().getLocalhostCertFile().getValue()); + zipOutput.closeEntry(); + zipOutput.putNextEntry(new ZipEntry(certsDirectory + zipContent.getCertsDirectory().getLocalhostKeyFile().getKey())); + zipOutput.write(zipContent.getCertsDirectory().getLocalhostKeyFile().getValue()); + zipOutput.closeEntry(); + zipOutput.putNextEntry(new ZipEntry(certsDirectory + zipContent.getCertsDirectory().getIdpTrustChainFile().getKey())); + zipOutput.write(zipContent.getCertsDirectory().getIdpTrustChainFile().getValue()); + zipOutput.closeEntry(); + + // Write README to .zip + zipOutput.putNextEntry(new ZipEntry(ROOT_DIR + "README.md")); + final String readmeContent = new BufferedReader(new InputStreamReader(DockerComposeYamlInstallationProvider.class.getResourceAsStream("/DockerComposeYamlReadme.md"))).lines().collect(Collectors.joining("\n")); + zipOutput.write(readmeContent.getBytes()); + zipOutput.closeEntry(); + + zipOutput.close(); + byteStream.close(); + + return Response.ok(byteStream.toByteArray(), getMediaType()).build(); + } + + @Override + public String getProtocol() { + return DockerAuthV2Protocol.LOGIN_PROTOCOL; + } + + @Override + public String getDisplayType() { + return "Docker Compose YAML"; + } + + @Override + public String getHelpText() { + return "Produces a zip file that can be used to stand up a development registry on localhost"; + } + + @Override + public String getFilename() { + return "keycloak-docker-compose-yaml.zip"; + } + + @Override + public String getMediaType() { + return "application/zip"; + } + + @Override + public boolean isDownloadOnly() { + return true; + } +} diff --git a/services/src/main/java/org/keycloak/protocol/docker/installation/DockerRegistryConfigFileInstallationProvider.java b/services/src/main/java/org/keycloak/protocol/docker/installation/DockerRegistryConfigFileInstallationProvider.java new file mode 100644 index 0000000000..ba4440a21c --- /dev/null +++ b/services/src/main/java/org/keycloak/protocol/docker/installation/DockerRegistryConfigFileInstallationProvider.java @@ -0,0 +1,81 @@ +package org.keycloak.protocol.docker.installation; + +import org.keycloak.Config; +import org.keycloak.models.ClientModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.models.RealmModel; +import org.keycloak.protocol.ClientInstallationProvider; +import org.keycloak.protocol.docker.DockerAuthV2Protocol; + +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import java.net.URI; + +public class DockerRegistryConfigFileInstallationProvider implements ClientInstallationProvider { + + @Override + public ClientInstallationProvider create(final KeycloakSession session) { + return this; + } + + @Override + public void init(final Config.Scope config) { + // no-op + } + + @Override + public void postInit(final KeycloakSessionFactory factory) { + // no-op + } + + @Override + public void close() { + // no-op + } + + @Override + public String getId() { + return "docker-v2-registry-config-file"; + } + + @Override + public Response generateInstallation(final KeycloakSession session, final RealmModel realm, final ClientModel client, final URI serverBaseUri) { + final StringBuilder responseString = new StringBuilder("auth:\n") + .append(" token:\n") + .append(" realm: ").append(serverBaseUri).append("/auth/realms/").append(realm.getName()).append("/protocol/").append(DockerAuthV2Protocol.LOGIN_PROTOCOL).append("/auth\n") + .append(" service: ").append(client.getClientId()).append("\n") + .append(" issuer: ").append(serverBaseUri).append("/auth/realms/").append(realm.getName()).append("\n"); + return Response.ok(responseString.toString(), MediaType.TEXT_PLAIN_TYPE).build(); + } + + @Override + public String getProtocol() { + return DockerAuthV2Protocol.LOGIN_PROTOCOL; + } + + @Override + public String getDisplayType() { + return "Registry Config File"; + } + + @Override + public String getHelpText() { + return "Provides a registry configuration file snippet for use with this client"; + } + + @Override + public String getFilename() { + return "config.yml"; + } + + @Override + public String getMediaType() { + return MediaType.TEXT_PLAIN; + } + + @Override + public boolean isDownloadOnly() { + return false; + } +} diff --git a/services/src/main/java/org/keycloak/protocol/docker/installation/DockerVariableOverrideInstallationProvider.java b/services/src/main/java/org/keycloak/protocol/docker/installation/DockerVariableOverrideInstallationProvider.java new file mode 100644 index 0000000000..055d9ac043 --- /dev/null +++ b/services/src/main/java/org/keycloak/protocol/docker/installation/DockerVariableOverrideInstallationProvider.java @@ -0,0 +1,81 @@ +package org.keycloak.protocol.docker.installation; + +import org.keycloak.Config; +import org.keycloak.models.ClientModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.models.RealmModel; +import org.keycloak.protocol.ClientInstallationProvider; +import org.keycloak.protocol.docker.DockerAuthV2Protocol; + +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import java.net.URI; + +public class DockerVariableOverrideInstallationProvider implements ClientInstallationProvider { + + @Override + public ClientInstallationProvider create(final KeycloakSession session) { + return this; + } + + @Override + public void init(final Config.Scope config) { + // no-op + } + + @Override + public void postInit(final KeycloakSessionFactory factory) { + // no-op + } + + @Override + public void close() { + // no-op + } + + @Override + public String getId() { + return "docker-v2-variable-override"; + } + + // TODO "auth" is not guaranteed to be the endpoint, fix it + @Override + public Response generateInstallation(final KeycloakSession session, final RealmModel realm, final ClientModel client, final URI serverBaseUri) { + final StringBuilder builder = new StringBuilder() + .append("-e REGISTRY_AUTH_TOKEN_REALM=").append(serverBaseUri).append("/auth/realms/").append(realm.getName()).append("/protocol/").append(DockerAuthV2Protocol.LOGIN_PROTOCOL).append("/auth \\\n") + .append("-e REGISTRY_AUTH_TOKEN_SERVICE=").append(client.getClientId()).append(" \\\n") + .append("-e REGISTRY_AUTH_TOKEN_ISSUER=").append(serverBaseUri).append("/auth/realms/").append(realm.getName()).append(" \\\n"); + return Response.ok(builder.toString(), MediaType.TEXT_PLAIN_TYPE).build(); + } + + @Override + public String getProtocol() { + return DockerAuthV2Protocol.LOGIN_PROTOCOL; + } + + @Override + public String getDisplayType() { + return "Variable Override"; + } + + @Override + public String getHelpText() { + return "Configures environment variable overrides, typically used with a docker-compose.yaml configuration for a docker registry"; + } + + @Override + public String getFilename() { + return "docker-env.txt"; + } + + @Override + public String getMediaType() { + return MediaType.TEXT_PLAIN; + } + + @Override + public boolean isDownloadOnly() { + return false; + } +} diff --git a/services/src/main/java/org/keycloak/protocol/docker/installation/compose/DockerCertFileUtils.java b/services/src/main/java/org/keycloak/protocol/docker/installation/compose/DockerCertFileUtils.java new file mode 100644 index 0000000000..66870899db --- /dev/null +++ b/services/src/main/java/org/keycloak/protocol/docker/installation/compose/DockerCertFileUtils.java @@ -0,0 +1,37 @@ +package org.keycloak.protocol.docker.installation.compose; + +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.cert.Certificate; +import java.security.cert.CertificateEncodingException; +import java.util.Base64; + +public final class DockerCertFileUtils { + public static final String BEGIN_CERT = "-----BEGIN CERTIFICATE-----"; + public static final String END_CERT = "-----END CERTIFICATE-----"; + public static final String BEGIN_PRIVATE_KEY = "-----BEGIN PRIVATE KEY-----"; + public static final String END_PRIVATE_KEY = "-----END PRIVATE KEY-----"; + public final static String LINE_SEPERATOR = System.getProperty("line.separator"); + + private DockerCertFileUtils() { + } + + public static String formatCrtFileContents(final Certificate certificate) throws CertificateEncodingException { + return encodeAndPrettify(BEGIN_CERT, certificate.getEncoded(), END_CERT); + } + + public static String formatPrivateKeyContents(final PrivateKey privateKey) { + return encodeAndPrettify(BEGIN_PRIVATE_KEY, privateKey.getEncoded(), END_PRIVATE_KEY); + } + + public static String formatPublicKeyContents(final PublicKey publicKey) { + return encodeAndPrettify(BEGIN_CERT, publicKey.getEncoded(), END_CERT); + } + + private static String encodeAndPrettify(final String header, final byte[] rawCrtText, final String footer) { + final Base64.Encoder encoder = Base64.getMimeEncoder(64, LINE_SEPERATOR.getBytes()); + final String encodedCertText = new String(encoder.encode(rawCrtText)); + final String prettified_cert = header + LINE_SEPERATOR + encodedCertText + LINE_SEPERATOR + footer; + return prettified_cert; + } +} diff --git a/services/src/main/java/org/keycloak/protocol/docker/installation/compose/DockerComposeCertsDirectory.java b/services/src/main/java/org/keycloak/protocol/docker/installation/compose/DockerComposeCertsDirectory.java new file mode 100644 index 0000000000..9d607f4be5 --- /dev/null +++ b/services/src/main/java/org/keycloak/protocol/docker/installation/compose/DockerComposeCertsDirectory.java @@ -0,0 +1,62 @@ +package org.keycloak.protocol.docker.installation.compose; + +import org.keycloak.common.util.CertificateUtils; + +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.SecureRandom; +import java.security.cert.Certificate; +import java.security.cert.CertificateEncodingException; +import java.util.AbstractMap; +import java.util.Map; + +public class DockerComposeCertsDirectory { + + private final String directoryName; + private final Map.Entry localhostCertFile; + private final Map.Entry localhostKeyFile; + private final Map.Entry idpTrustChainFile; + + public DockerComposeCertsDirectory(final String directoryName, final Certificate realmCert, final String registryCertFilename, final String registryKeyFilename, final String idpCertTrustChainFilename, final String realmName) { + this.directoryName = directoryName; + + final KeyPairGenerator keyGen; + try { + keyGen = KeyPairGenerator.getInstance("RSA"); + keyGen.initialize(2048, new SecureRandom()); + + final KeyPair keypair = keyGen.generateKeyPair(); + final PrivateKey privateKey = keypair.getPrivate(); + final Certificate certificate = CertificateUtils.generateV1SelfSignedCertificate(keypair, realmName); + + localhostCertFile = new AbstractMap.SimpleImmutableEntry<>(registryCertFilename, DockerCertFileUtils.formatCrtFileContents(certificate).getBytes()); + localhostKeyFile = new AbstractMap.SimpleImmutableEntry<>(registryKeyFilename, DockerCertFileUtils.formatPrivateKeyContents(privateKey).getBytes()); + idpTrustChainFile = new AbstractMap.SimpleEntry<>(idpCertTrustChainFilename, DockerCertFileUtils.formatCrtFileContents(realmCert).getBytes()); + + } catch (final NoSuchAlgorithmException e) { + // TODO throw error here descritively + throw new RuntimeException(e); + } catch (final CertificateEncodingException e) { + // TODO throw error here descritively + throw new RuntimeException(e); + } + } + + public String getDirectoryName() { + return directoryName; + } + + public Map.Entry getLocalhostCertFile() { + return localhostCertFile; + } + + public Map.Entry getLocalhostKeyFile() { + return localhostKeyFile; + } + + public Map.Entry getIdpTrustChainFile() { + return idpTrustChainFile; + } +} diff --git a/services/src/main/java/org/keycloak/protocol/docker/installation/compose/DockerComposeYamlFile.java b/services/src/main/java/org/keycloak/protocol/docker/installation/compose/DockerComposeYamlFile.java new file mode 100644 index 0000000000..1630ffaec0 --- /dev/null +++ b/services/src/main/java/org/keycloak/protocol/docker/installation/compose/DockerComposeYamlFile.java @@ -0,0 +1,70 @@ +package org.keycloak.protocol.docker.installation.compose; + +import java.io.ByteArrayOutputStream; +import java.io.PrintWriter; +import java.net.URL; + +/** + * Representation of the docker-compose.yaml file + */ +public class DockerComposeYamlFile { + + private final String registryDataDirName; + private final String localCertDirName; + private final String containerCertPath; + private final String localhostCrtFileName; + private final String localhostKeyFileName; + private final String authServerTrustChainFileName; + private final URL authServerUrl; + private final String realmName; + private final String serviceId; + + /** + * @param registryDataDirName Directory name to be used for both the container's storage directory, as well as the local data directory name + * @param localCertDirName Name of the (relative) local directory that holds the certs + * @param containerCertPath Path at which the local certs directory should be mounted on the container + * @param localhostCrtFileName SSL Cert file name for the registry + * @param localhostKeyFileName SSL Key file name for the registry + * @param authServerTrustChainFileName IDP trust chain, used for auth token validation + * @param authServerUrl Root URL for Keycloak, commonly something like http://localhost:8080/auth for dev environments + * @param realmName Name of the realm for which the docker client is configured + * @param serviceId Docker's Service ID, corresponds to Keycloak's client ID + */ + public DockerComposeYamlFile(final String registryDataDirName, final String localCertDirName, final String containerCertPath, final String localhostCrtFileName, final String localhostKeyFileName, final String authServerTrustChainFileName, final URL authServerUrl, final String realmName, final String serviceId) { + this.registryDataDirName = registryDataDirName; + this.localCertDirName = localCertDirName; + this.containerCertPath = containerCertPath; + this.localhostCrtFileName = localhostCrtFileName; + this.localhostKeyFileName = localhostKeyFileName; + this.authServerTrustChainFileName = authServerTrustChainFileName; + this.authServerUrl = authServerUrl; + this.realmName = realmName; + this.serviceId = serviceId; + } + + public byte[] generateDockerComposeFileBytes() { + final ByteArrayOutputStream output = new ByteArrayOutputStream(); + final PrintWriter writer = new PrintWriter(output); + + writer.print("registry:\n"); + writer.print(" image: registry:2\n"); + writer.print(" ports:\n"); + writer.print(" - 127.0.0.1:5000:5000\n"); + writer.print(" environment:\n"); + writer.print(" REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY: /" + registryDataDirName + "\n"); + writer.print(" REGISTRY_HTTP_TLS_CERTIFICATE: " + containerCertPath + "/" + localhostCrtFileName + "\n"); + writer.print(" REGISTRY_HTTP_TLS_KEY: " + containerCertPath + "/" + localhostKeyFileName + "\n"); + writer.print(" REGISTRY_AUTH_TOKEN_REALM: " + authServerUrl + "/realms/" + realmName + "/protocol/docker-v2/auth\n"); + writer.print(" REGISTRY_AUTH_TOKEN_SERVICE: " + serviceId + "\n"); + writer.print(" REGISTRY_AUTH_TOKEN_ISSUER: " + authServerUrl + "/realms/" + realmName + "\n"); + writer.print(" REGISTRY_AUTH_TOKEN_ROOTCERTBUNDLE: " + containerCertPath + "/" + authServerTrustChainFileName + "\n"); + writer.print(" volumes:\n"); + writer.print(" - ./" + registryDataDirName + ":/" + registryDataDirName + ":z\n"); + writer.print(" - ./" + localCertDirName + ":" + containerCertPath + ":z"); + + writer.flush(); + writer.close(); + + return output.toByteArray(); + } +} diff --git a/services/src/main/java/org/keycloak/protocol/docker/installation/compose/DockerComposeZipContent.java b/services/src/main/java/org/keycloak/protocol/docker/installation/compose/DockerComposeZipContent.java new file mode 100644 index 0000000000..a4d0ee2012 --- /dev/null +++ b/services/src/main/java/org/keycloak/protocol/docker/installation/compose/DockerComposeZipContent.java @@ -0,0 +1,35 @@ +package org.keycloak.protocol.docker.installation.compose; + +import java.net.URL; +import java.security.cert.Certificate; + +public class DockerComposeZipContent { + + private final DockerComposeYamlFile yamlFile; + private final String dataDirectoryName; + private final DockerComposeCertsDirectory certsDirectory; + + public DockerComposeZipContent(final Certificate realmCert, final URL realmBaseUrl, final String realmName, final String clientId) { + final String dataDirectoryName = "data"; + final String certsDirectoryName = "certs"; + final String registryCertFilename = "localhost.crt"; + final String registryKeyFilename = "localhost.key"; + final String idpCertTrustChainFilename = "localhost_trust_chain.pem"; + + this.yamlFile = new DockerComposeYamlFile(dataDirectoryName, certsDirectoryName, "/opt/" + certsDirectoryName, registryCertFilename, registryKeyFilename, idpCertTrustChainFilename, realmBaseUrl, realmName, clientId); + this.dataDirectoryName = dataDirectoryName; + this.certsDirectory = new DockerComposeCertsDirectory(certsDirectoryName, realmCert, registryCertFilename, registryKeyFilename, idpCertTrustChainFilename, realmName); + } + + public DockerComposeYamlFile getYamlFile() { + return yamlFile; + } + + public String getDataDirectoryName() { + return dataDirectoryName; + } + + public DockerComposeCertsDirectory getCertsDirectory() { + return certsDirectory; + } +} diff --git a/services/src/main/java/org/keycloak/protocol/docker/mapper/AllowAllDockerProtocolMapper.java b/services/src/main/java/org/keycloak/protocol/docker/mapper/AllowAllDockerProtocolMapper.java new file mode 100644 index 0000000000..398eeb61e1 --- /dev/null +++ b/services/src/main/java/org/keycloak/protocol/docker/mapper/AllowAllDockerProtocolMapper.java @@ -0,0 +1,52 @@ +package org.keycloak.protocol.docker.mapper; + +import org.keycloak.models.AuthenticatedClientSessionModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.ProtocolMapperModel; +import org.keycloak.models.UserSessionModel; +import org.keycloak.protocol.docker.DockerAuthV2Protocol; +import org.keycloak.representations.docker.DockerAccess; +import org.keycloak.representations.docker.DockerResponseToken; + +/** + * Populates token with requested scope. If more scopes are present than what has been requested, they will be removed. + */ +public class AllowAllDockerProtocolMapper extends DockerAuthV2ProtocolMapper implements DockerAuthV2AttributeMapper { + + public static final String PROVIDER_ID = "docker-v2-allow-all-mapper"; + + @Override + public String getDisplayType() { + return "Allow All"; + } + + @Override + public String getHelpText() { + return "Allows all grants, returning the full set of requested access attributes as permitted attributes."; + } + + @Override + public String getId() { + return PROVIDER_ID; + } + + @Override + public boolean appliesTo(final DockerResponseToken responseToken) { + return true; + } + + @Override + public DockerResponseToken transformDockerResponseToken(final DockerResponseToken responseToken, final ProtocolMapperModel mappingModel, + final KeycloakSession session, final UserSessionModel userSession, final AuthenticatedClientSessionModel clientSession) { + + responseToken.getAccessItems().clear(); + + final String requestedScope = clientSession.getNote(DockerAuthV2Protocol.SCOPE_PARAM); + if (requestedScope != null) { + final DockerAccess allRequestedAccess = new DockerAccess(requestedScope); + responseToken.getAccessItems().add(allRequestedAccess); + } + + return responseToken; + } +} diff --git a/services/src/main/java/org/keycloak/protocol/docker/mapper/DockerAuthV2AttributeMapper.java b/services/src/main/java/org/keycloak/protocol/docker/mapper/DockerAuthV2AttributeMapper.java new file mode 100644 index 0000000000..320686be96 --- /dev/null +++ b/services/src/main/java/org/keycloak/protocol/docker/mapper/DockerAuthV2AttributeMapper.java @@ -0,0 +1,15 @@ +package org.keycloak.protocol.docker.mapper; + +import org.keycloak.models.AuthenticatedClientSessionModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.ProtocolMapperModel; +import org.keycloak.models.UserSessionModel; +import org.keycloak.representations.docker.DockerResponseToken; + +public interface DockerAuthV2AttributeMapper { + + boolean appliesTo(DockerResponseToken responseToken); + + DockerResponseToken transformDockerResponseToken(DockerResponseToken responseToken, ProtocolMapperModel mappingModel, + KeycloakSession session, UserSessionModel userSession, AuthenticatedClientSessionModel clientSession); +} diff --git a/services/src/main/java/org/keycloak/protocol/docker/mapper/DockerAuthV2ProtocolMapper.java b/services/src/main/java/org/keycloak/protocol/docker/mapper/DockerAuthV2ProtocolMapper.java new file mode 100644 index 0000000000..69ccd004ed --- /dev/null +++ b/services/src/main/java/org/keycloak/protocol/docker/mapper/DockerAuthV2ProtocolMapper.java @@ -0,0 +1,51 @@ +package org.keycloak.protocol.docker.mapper; + +import org.keycloak.Config; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.protocol.ProtocolMapper; +import org.keycloak.protocol.docker.DockerAuthV2Protocol; +import org.keycloak.provider.ProviderConfigProperty; + +import java.util.Collections; +import java.util.List; + +public abstract class DockerAuthV2ProtocolMapper implements ProtocolMapper { + + public static final String DOCKER_AUTH_V2_CATEGORY = "Docker Auth Mapper"; + + @Override + public String getProtocol() { + return DockerAuthV2Protocol.LOGIN_PROTOCOL; + } + + @Override + public String getDisplayCategory() { + return DOCKER_AUTH_V2_CATEGORY; + } + + @Override + public List getConfigProperties() { + return Collections.emptyList(); + } + + @Override + public void close() { + // no-op + } + + @Override + public final ProtocolMapper create(final KeycloakSession session) { + throw new UnsupportedOperationException("The create method is not supported by this mapper"); + } + + @Override + public void init(final Config.Scope config) { + // no-op + } + + @Override + public void postInit(final KeycloakSessionFactory factory) { + // no-op + } +} diff --git a/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocolService.java b/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocolService.java index b53563600d..160013fefb 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocolService.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocolService.java @@ -171,7 +171,7 @@ public class OIDCLoginProtocolService { @Path("login-status-iframe.html") public Object getLoginStatusIframe() { - LoginStatusIframeEndpoint endpoint = new LoginStatusIframeEndpoint(realm); + LoginStatusIframeEndpoint endpoint = new LoginStatusIframeEndpoint(); ResteasyProviderFactory.getInstance().injectProperties(endpoint); return endpoint; } diff --git a/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java b/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java index 07aec65e6f..6a26c692d5 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java @@ -59,6 +59,7 @@ import org.keycloak.services.ErrorResponseException; import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.services.managers.AuthenticationSessionManager; import org.keycloak.services.managers.ClientSessionCode; +import org.keycloak.services.managers.UserSessionCrossDCManager; import org.keycloak.services.managers.UserSessionManager; import org.keycloak.sessions.AuthenticationSessionModel; import org.keycloak.util.TokenUtil; @@ -120,17 +121,10 @@ public class TokenManager { } public TokenValidation validateToken(KeycloakSession session, UriInfo uriInfo, ClientConnection connection, RealmModel realm, AccessToken oldToken, HttpHeaders headers) throws OAuthErrorException { - UserModel user = session.users().getUserById(oldToken.getSubject(), realm); - if (user == null) { - throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Invalid refresh token", "Unknown user"); - } - - if (!user.isEnabled()) { - throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "User disabled", "User disabled"); - } - UserSessionModel userSession = null; - if (TokenUtil.TOKEN_TYPE_OFFLINE.equals(oldToken.getType())) { + boolean offline = TokenUtil.TOKEN_TYPE_OFFLINE.equals(oldToken.getType()); + + if (offline) { UserSessionManager sessionManager = new UserSessionManager(session); userSession = sessionManager.findOfflineUserSession(realm, oldToken.getSessionState()); @@ -142,6 +136,8 @@ public class TokenManager { throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Offline session not active", "Offline session not active"); } + } else { + throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Offline user session not found", "Offline user session not found"); } } else { // Find userSession regularly for online tokens @@ -152,13 +148,28 @@ public class TokenManager { } } - if (userSession == null) { - throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Offline user session not found", "Offline user session not found"); + UserModel user = userSession.getUser(); + if (user == null) { + throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Invalid refresh token", "Unknown user"); + } + + if (!user.isEnabled()) { + throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "User disabled", "User disabled"); } ClientModel client = session.getContext().getClient(); AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessions().get(client.getId()); + // Can theoretically happen in cross-dc environment. Try to see if userSession with our client is available in remoteCache + if (clientSession == null) { + userSession = new UserSessionCrossDCManager(session).getUserSessionWithClient(realm, userSession.getId(), offline, client.getId()); + if (userSession != null) { + clientSession = userSession.getAuthenticatedClientSessions().get(client.getId()); + } else { + throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Session doesn't have required client", "Session doesn't have required client"); + } + } + if (!client.getClientId().equals(oldToken.getIssuedFor())) { throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Unmatching clients", "Unmatching clients"); } @@ -202,21 +213,15 @@ public class TokenManager { return false; } - UserSessionModel userSession = session.sessions().getUserSession(realm, token.getSessionState()); + UserSessionModel userSession = new UserSessionCrossDCManager(session).getUserSessionWithClient(realm, token.getSessionState(), false, client.getId()); if (AuthenticationManager.isSessionValid(realm, userSession)) { - AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessions().get(client.getId()); - if (clientSession != null) { - return true; - } + return true; } - userSession = session.sessions().getOfflineUserSession(realm, token.getSessionState()); + userSession = new UserSessionCrossDCManager(session).getUserSessionWithClient(realm, token.getSessionState(), true, client.getId()); if (AuthenticationManager.isOfflineSessionValid(realm, userSession)) { - AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessions().get(client.getId()); - if (clientSession != null) { - return true; - } + return true; } return false; @@ -678,6 +683,18 @@ public class TokenManager { this.clientSession = clientSession; } + public AccessToken getAccessToken() { + return accessToken; + } + + public RefreshToken getRefreshToken() { + return refreshToken; + } + + public IDToken getIdToken() { + return idToken; + } + public AccessTokenResponseBuilder accessToken(AccessToken accessToken) { this.accessToken = accessToken; return this; 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 3a7e4c0e36..38dbc8f5e4 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 @@ -49,7 +49,6 @@ import org.keycloak.util.TokenUtil; import javax.ws.rs.GET; import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.Response; - import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -169,21 +168,6 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase { return this; } - - private void checkSsl() { - if (!uriInfo.getBaseUri().getScheme().equals("https") && realm.getSslRequired().isRequired(clientConnection)) { - event.error(Errors.SSL_REQUIRED); - throw new ErrorPageException(session, Messages.HTTPS_REQUIRED); - } - } - - private void checkRealm() { - if (!realm.isEnabled()) { - event.error(Errors.REALM_DISABLED); - throw new ErrorPageException(session, Messages.REALM_NOT_ENABLED); - } - } - private void checkClient(String clientId) { if (clientId == null) { event.error(Errors.INVALID_REQUEST); @@ -288,24 +272,24 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase { private Response checkPKCEParams() { String codeChallenge = request.getCodeChallenge(); String codeChallengeMethod = request.getCodeChallengeMethod(); - + // PKCE not adopted to OAuth2 Implicit Grant and OIDC Implicit Flow, // adopted to OAuth2 Authorization Code Grant and OIDC Authorization Code Flow, Hybrid Flow // Namely, flows using authorization code. if (parsedResponseType.isImplicitFlow()) return null; - + if (codeChallenge == null && codeChallengeMethod != null) { logger.info("PKCE supporting Client without code challenge"); event.error(Errors.INVALID_REQUEST); return redirectErrorToClient(parsedResponseMode, OAuthErrorException.INVALID_REQUEST, "Missing parameter: code_challenge"); } - + // based on code_challenge value decide whether this client(RP) supports PKCE if (codeChallenge == null) { logger.debug("PKCE non-supporting Client"); return null; } - + if (codeChallengeMethod != null) { // https://tools.ietf.org/html/rfc7636#section-4.2 // plain or S256 @@ -319,13 +303,13 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase { // default code_challenge_method is plane codeChallengeMethod = OIDCLoginProtocol.PKCE_METHOD_PLAIN; } - + if (!isValidPkceCodeChallenge(codeChallenge)) { logger.infof("PKCE supporting Client with invalid code challenge specified in PKCE, codeChallenge = %s", codeChallenge); event.error(Errors.INVALID_REQUEST); return redirectErrorToClient(parsedResponseMode, OAuthErrorException.INVALID_REQUEST, "Invalid parameter: code_challenge"); } - + return null; } @@ -449,7 +433,7 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase { String flowId = flow.getId(); AuthenticationProcessor processor = createProcessor(authenticationSession, flowId, LoginActionsService.RESET_CREDENTIALS_PATH); - authenticationSession.setClientNote(APP_INITIATED_FLOW, LoginActionsService.REGISTRATION_PATH); + authenticationSession.setClientNote(APP_INITIATED_FLOW, LoginActionsService.RESET_CREDENTIALS_PATH); return processor.authenticate(); } diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LoginStatusIframeEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LoginStatusIframeEndpoint.java index b4f8622d5d..a478169701 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LoginStatusIframeEndpoint.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LoginStatusIframeEndpoint.java @@ -17,6 +17,7 @@ package org.keycloak.protocol.oidc.endpoints; +import org.keycloak.common.Version; import org.keycloak.common.util.UriUtils; import org.keycloak.models.ClientModel; import org.keycloak.models.KeycloakSession; @@ -28,8 +29,10 @@ import org.keycloak.utils.MediaType; import javax.ws.rs.GET; import javax.ws.rs.Path; +import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; +import javax.ws.rs.core.CacheControl; import javax.ws.rs.core.Context; import javax.ws.rs.core.Response; import javax.ws.rs.core.UriInfo; @@ -41,26 +44,26 @@ import java.util.Set; */ public class LoginStatusIframeEndpoint { - @Context - private UriInfo uriInfo; - @Context private KeycloakSession session; - private RealmModel realm; - - public LoginStatusIframeEndpoint(RealmModel realm) { - this.realm = realm; - } - @GET @Produces(MediaType.TEXT_HTML_UTF_8) - public Response getLoginStatusIframe(@QueryParam("client_id") String client_id, - @QueryParam("origin") String origin) { + public Response getLoginStatusIframe(@QueryParam("version") String version) { + CacheControl cacheControl; + if (version != null) { + if (!version.equals(Version.RESOURCES_VERSION)) { + return Response.status(Response.Status.NOT_FOUND).build(); + } + cacheControl = CacheControlUtil.getDefaultCacheControl(); + } else { + cacheControl = CacheControlUtil.noCache(); + } + InputStream resource = getClass().getClassLoader().getResourceAsStream("login-status-iframe.html"); if (resource != null) { P3PHelper.addP3PHeader(session); - return Response.ok(resource).cacheControl(CacheControlUtil.getDefaultCacheControl()).build(); + return Response.ok(resource).cacheControl(cacheControl).build(); } else { return Response.status(Response.Status.NOT_FOUND).build(); } @@ -70,6 +73,7 @@ public class LoginStatusIframeEndpoint { @Path("init") public Response preCheck(@QueryParam("client_id") String clientId, @QueryParam("origin") String origin) { try { + UriInfo uriInfo = session.getContext().getUri(); RealmModel realm = session.getContext().getRealm(); ClientModel client = session.realms().getClientByClientId(clientId, realm); if (client != null) { diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java index 4870415f23..fef17c6ca0 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java @@ -36,6 +36,7 @@ import org.keycloak.models.AuthenticatedClientSessionModel; import org.keycloak.models.ClientModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; +import org.keycloak.models.RoleModel; import org.keycloak.models.UserModel; import org.keycloak.models.UserSessionModel; import org.keycloak.protocol.oidc.OIDCLoginProtocol; @@ -52,6 +53,7 @@ import org.keycloak.services.managers.ClientManager; import org.keycloak.services.managers.ClientSessionCode; import org.keycloak.services.managers.RealmManager; import org.keycloak.services.resources.Cors; +import org.keycloak.services.resources.admin.permissions.AdminPermissions; import org.keycloak.sessions.AuthenticationSessionModel; import org.keycloak.util.TokenUtil; @@ -65,6 +67,7 @@ import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.Response; import javax.ws.rs.core.UriInfo; import java.util.Map; +import java.util.Objects; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.security.MessageDigest; @@ -80,7 +83,7 @@ public class TokenEndpoint { private Map clientAuthAttributes; private enum Action { - AUTHORIZATION_CODE, REFRESH_TOKEN, PASSWORD, CLIENT_CREDENTIALS + AUTHORIZATION_CODE, REFRESH_TOKEN, PASSWORD, CLIENT_CREDENTIALS, TOKEN_EXCHANGE } // https://tools.ietf.org/html/rfc7636#section-4.2 @@ -134,6 +137,8 @@ public class TokenEndpoint { return buildResourceOwnerPasswordCredentialsGrant(); case CLIENT_CREDENTIALS: return buildClientCredentialsGrant(); + case TOKEN_EXCHANGE: + return buildTokenExchange(); } throw new RuntimeException("Unknown action " + action); @@ -197,6 +202,10 @@ public class TokenEndpoint { } else if (grantType.equals(OAuth2Constants.CLIENT_CREDENTIALS)) { event.event(EventType.CLIENT_LOGIN); action = Action.CLIENT_CREDENTIALS; + } else if (grantType.equals(OAuth2Constants.TOKEN_EXCHANGE_GRANT_TYPE)) { + event.event(EventType.TOKEN_EXCHANGE); + action = Action.TOKEN_EXCHANGE; + } else { throw new ErrorResponseException(Errors.INVALID_REQUEST, "Invalid " + OIDCLoginProtocol.GRANT_TYPE_PARAM, Response.Status.BAD_REQUEST); } @@ -258,6 +267,7 @@ public class TokenEndpoint { } event.user(userSession.getUser()); + event.session(userSession.getId()); String redirectUri = clientSession.getNote(OIDCLoginProtocol.REDIRECT_URI_PARAM); @@ -376,6 +386,7 @@ public class TokenEndpoint { } } catch (OAuthErrorException e) { + logger.trace(e.getMessage(), e); event.error(Errors.INVALID_TOKEN); throw new ErrorResponseException(e.getError(), e.getDescription(), Response.Status.BAD_REQUEST); } @@ -398,9 +409,16 @@ public class TokenEndpoint { logger.debugf("Adapter Session '%s' saved in ClientSession for client '%s'. Host is '%s'", adapterSessionId, client.getClientId(), adapterSessionHost); event.detail(AdapterConstants.CLIENT_SESSION_STATE, adapterSessionId); - clientSession.setNote(AdapterConstants.CLIENT_SESSION_STATE, adapterSessionId); + String oldClientSessionState = clientSession.getNote(AdapterConstants.CLIENT_SESSION_STATE); + if (!adapterSessionId.equals(oldClientSessionState)) { + clientSession.setNote(AdapterConstants.CLIENT_SESSION_STATE, adapterSessionId); + } + event.detail(AdapterConstants.CLIENT_SESSION_HOST, adapterSessionHost); - clientSession.setNote(AdapterConstants.CLIENT_SESSION_HOST, adapterSessionHost); + String oldClientSessionHost = clientSession.getNote(AdapterConstants.CLIENT_SESSION_HOST); + if (!Objects.equals(adapterSessionHost, oldClientSessionHost)) { + clientSession.setNote(AdapterConstants.CLIENT_SESSION_HOST, adapterSessionHost); + } } } @@ -543,6 +561,100 @@ public class TokenEndpoint { return Cors.add(request, Response.ok(res, MediaType.APPLICATION_JSON_TYPE)).auth().allowedOrigins(uriInfo, client).allowedMethods("POST").exposedHeaders(Cors.ACCESS_CONTROL_ALLOW_METHODS).build(); } + public Response buildTokenExchange() { + event.detail(Details.AUTH_METHOD, "oauth_credentials"); + + String scope = formParams.getFirst(OAuth2Constants.SCOPE); + String subjectToken = formParams.getFirst(OAuth2Constants.SUBJECT_TOKEN); + String subjectTokenType = formParams.getFirst(OAuth2Constants.SUBJECT_TOKEN_TYPE); + AuthenticationManager.AuthResult authResult = AuthenticationManager.verifyIdentityToken(session, realm, uriInfo, clientConnection, true, true, false, subjectToken, headers); + if (authResult == null) { + event.error(Errors.INVALID_TOKEN); + throw new ErrorResponseException(OAuthErrorException.INVALID_TOKEN, "Invalid token", Response.Status.BAD_REQUEST); + } + + String audience = formParams.getFirst(OAuth2Constants.AUDIENCE); + if (audience == null) { + event.error(Errors.INVALID_REQUEST); + throw new ErrorResponseException("invalid_audience", "No audience specified", Response.Status.BAD_REQUEST); + + } + ClientModel targetClient = null; + if (audience != null) { + targetClient = realm.getClientByClientId(audience); + } + if (targetClient == null) { + event.error(Errors.INVALID_CLIENT); + throw new ErrorResponseException("invalid_client", "Client authentication ended, but client is null", Response.Status.BAD_REQUEST); + } + + if (targetClient.isConsentRequired()) { + event.error(Errors.CONSENT_DENIED); + throw new ErrorResponseException(OAuthErrorException.INVALID_CLIENT, "Client requires user consent", Response.Status.BAD_REQUEST); + } + + boolean exchangeFromAllowed = false; + for (String aud : authResult.getToken().getAudience()) { + ClientModel audClient = realm.getClientByClientId(aud); + if (audClient == null) continue; + if (audClient.equals(client)) { + exchangeFromAllowed = true; + break; + } + if (AdminPermissions.management(session, realm).clients().canExchangeFrom(client, audClient)) { + exchangeFromAllowed = true; + break; + } + } + if (!exchangeFromAllowed) { + logger.debug("Client does not have exchange rights for audience of provided token"); + event.error(Errors.NOT_ALLOWED); + throw new ErrorResponseException(OAuthErrorException.ACCESS_DENIED, "Client not allowed to exchange", Response.Status.FORBIDDEN); + } + if (!AdminPermissions.management(session, realm).clients().canExchangeTo(client, targetClient)) { + logger.debug("Client does not have exchange rights for target audience"); + event.error(Errors.NOT_ALLOWED); + throw new ErrorResponseException(OAuthErrorException.ACCESS_DENIED, "Client not allowed to exchange", Response.Status.FORBIDDEN); + } + + AuthenticationSessionModel authSession = new AuthenticationSessionManager(session).createAuthenticationSession(realm, targetClient, false); + authSession.setAuthenticatedUser(authResult.getUser()); + authSession.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL); + authSession.setClientNote(OIDCLoginProtocol.ISSUER, Urls.realmIssuer(uriInfo.getBaseUri(), realm.getName())); + authSession.setClientNote(OIDCLoginProtocol.SCOPE_PARAM, scope); + + UserSessionModel userSession = authResult.getSession(); + event.session(userSession); + + AuthenticationManager.setRolesAndMappersInSession(authSession); + AuthenticatedClientSessionModel clientSession = TokenManager.attachAuthenticationSession(session, userSession, authSession); + + // Notes about client details + userSession.setNote(ServiceAccountConstants.CLIENT_ID, client.getClientId()); + userSession.setNote(ServiceAccountConstants.CLIENT_HOST, clientConnection.getRemoteHost()); + userSession.setNote(ServiceAccountConstants.CLIENT_ADDRESS, clientConnection.getRemoteAddr()); + + updateUserSessionFromClientAuth(userSession); + + TokenManager.AccessTokenResponseBuilder responseBuilder = tokenManager.responseBuilder(realm, targetClient, event, session, userSession, clientSession) + .generateAccessToken() + .generateRefreshToken(); + responseBuilder.getAccessToken().issuedFor(client.getClientId()); + responseBuilder.getRefreshToken().issuedFor(client.getClientId()); + + String scopeParam = clientSession.getNote(OAuth2Constants.SCOPE); + if (TokenUtil.isOIDCRequest(scopeParam)) { + responseBuilder.generateIDToken(); + } + + AccessTokenResponse res = responseBuilder.build(); + + event.success(); + + return Cors.add(request, Response.ok(res, MediaType.APPLICATION_JSON_TYPE)).auth().allowedOrigins(uriInfo, client).allowedMethods("POST").exposedHeaders(Cors.ACCESS_CONTROL_ALLOW_METHODS).build(); + } + + // https://tools.ietf.org/html/rfc7636#section-4.1 private boolean isValidPkceCodeVerifier(String codeVerifier) { if (codeVerifier.length() < OIDCLoginProtocol.PKCE_CODE_VERIFIER_MIN_LENGTH) { 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 1b8817d7b7..9571fdeeab 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 @@ -42,6 +42,7 @@ import org.keycloak.services.ErrorResponseException; import org.keycloak.services.Urls; import org.keycloak.services.managers.AppAuthManager; import org.keycloak.services.managers.AuthenticationManager; +import org.keycloak.services.managers.UserSessionCrossDCManager; import org.keycloak.services.resources.Cors; import org.keycloak.utils.MediaType; @@ -139,18 +140,6 @@ public class UserInfoEndpoint { throw new ErrorResponseException(OAuthErrorException.INVALID_TOKEN, "Token invalid: " + e.getMessage(), Response.Status.UNAUTHORIZED); } - UserSessionModel userSession = findValidSession(token, event); - - UserModel userModel = userSession.getUser(); - if (userModel == null) { - event.error(Errors.USER_NOT_FOUND); - throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "User not found", Response.Status.BAD_REQUEST); - } - - event.user(userModel) - .detail(Details.USERNAME, userModel.getUsername()); - - ClientModel clientModel = realm.getClientByClientId(token.getIssuedFor()); if (clientModel == null) { event.error(Errors.CLIENT_NOT_FOUND); @@ -164,12 +153,21 @@ public class UserInfoEndpoint { throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "Client disabled", Response.Status.BAD_REQUEST); } - AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessions().get(clientModel.getId()); - if (clientSession == null) { - event.error(Errors.SESSION_EXPIRED); - throw new ErrorResponseException(OAuthErrorException.INVALID_TOKEN, "Session expired", Response.Status.UNAUTHORIZED); + UserSessionModel userSession = findValidSession(token, event, clientModel); + + UserModel userModel = userSession.getUser(); + if (userModel == null) { + event.error(Errors.USER_NOT_FOUND); + throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "User not found", Response.Status.BAD_REQUEST); } + event.user(userModel) + .detail(Details.USERNAME, userModel.getUsername()); + + + // Existence of authenticatedClientSession for our client already handled before + AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessions().get(clientModel.getId()); + AccessToken userInfo = new AccessToken(); tokenManager.transformUserInfoAccessToken(session, userInfo, realm, clientModel, userModel, userSession, clientSession); @@ -209,14 +207,14 @@ public class UserInfoEndpoint { } - private UserSessionModel findValidSession(AccessToken token, EventBuilder event) { - UserSessionModel userSession = session.sessions().getUserSession(realm, token.getSessionState()); + private UserSessionModel findValidSession(AccessToken token, EventBuilder event, ClientModel client) { + UserSessionModel userSession = new UserSessionCrossDCManager(session).getUserSessionWithClient(realm, token.getSessionState(), false, client.getId()); UserSessionModel offlineUserSession = null; if (AuthenticationManager.isSessionValid(realm, userSession)) { event.session(userSession); return userSession; } else { - offlineUserSession = session.sessions().getOfflineUserSession(realm, token.getSessionState()); + offlineUserSession = new UserSessionCrossDCManager(session).getUserSessionWithClient(realm, token.getSessionState(), true, client.getId()); if (AuthenticationManager.isOfflineSessionValid(realm, offlineUserSession)) { event.session(offlineUserSession); return offlineUserSession; @@ -225,7 +223,7 @@ public class UserInfoEndpoint { if (userSession == null && offlineUserSession == null) { event.error(Errors.USER_SESSION_NOT_FOUND); - throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "User session not found", Response.Status.BAD_REQUEST); + throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "User session not found or doesn't have client attached on it", Response.Status.UNAUTHORIZED); } if (userSession != null) { diff --git a/services/src/main/java/org/keycloak/protocol/oidc/mappers/FullNameMapper.java b/services/src/main/java/org/keycloak/protocol/oidc/mappers/FullNameMapper.java index 1e9b3e251f..c15609242d 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/mappers/FullNameMapper.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/mappers/FullNameMapper.java @@ -26,8 +26,10 @@ import org.keycloak.representations.IDToken; import java.util.ArrayList; import java.util.HashMap; +import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.Optional; /** * Set the 'name' claim to be first + last name. @@ -73,9 +75,12 @@ public class FullNameMapper extends AbstractOIDCProtocolMapper implements OIDCAc protected void setClaim(IDToken token, ProtocolMapperModel mappingModel, UserSessionModel userSession) { UserModel user = userSession.getUser(); - String first = user.getFirstName() == null ? "" : user.getFirstName() + " "; - String last = user.getLastName() == null ? "" : user.getLastName(); - token.getOtherClaims().put("name", first + last); + List parts = new LinkedList<>(); + Optional.ofNullable(user.getFirstName()).filter(s -> !s.isEmpty()).ifPresent(parts::add); + Optional.ofNullable(user.getLastName()).filter(s -> !s.isEmpty()).ifPresent(parts::add); + if (!parts.isEmpty()) { + token.getOtherClaims().put("name", String.join(" ", parts)); + } } public static ProtocolMapperModel create(String name, diff --git a/services/src/main/java/org/keycloak/protocol/oidc/utils/OIDCRedirectUriBuilder.java b/services/src/main/java/org/keycloak/protocol/oidc/utils/OIDCRedirectUriBuilder.java index f2fdd0c322..9027ed5367 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/utils/OIDCRedirectUriBuilder.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/utils/OIDCRedirectUriBuilder.java @@ -91,7 +91,7 @@ public abstract class OIDCRedirectUriBuilder { @Override public OIDCRedirectUriBuilder addParam(String paramName, String paramValue) { - String param = paramName + "=" + Encode.encodeQueryParam(paramValue); + String param = paramName + "=" + Encode.encodeQueryParamAsIs(paramValue); if (fragment == null) { fragment = new StringBuilder(param); } else { diff --git a/services/src/main/java/org/keycloak/protocol/saml/SamlProtocol.java b/services/src/main/java/org/keycloak/protocol/saml/SamlProtocol.java index a8218c17ac..66a7609568 100755 --- a/services/src/main/java/org/keycloak/protocol/saml/SamlProtocol.java +++ b/services/src/main/java/org/keycloak/protocol/saml/SamlProtocol.java @@ -190,16 +190,9 @@ public class SamlProtocol implements LoginProtocol { } binding.signatureAlgorithm(samlClient.getSignatureAlgorithm()).signWith(keyName, keys.getPrivateKey(), keys.getPublicKey(), keys.getCertificate()).signDocument(); } - if (samlClient.requiresEncryption()) { - PublicKey publicKey; - try { - publicKey = SamlProtocolUtils.getEncryptionValidationKey(client); - } catch (Exception e) { - logger.error("failed", e); - return ErrorPage.error(session, Messages.FAILED_TO_PROCESS_RESPONSE); - } - binding.encrypt(publicKey); - } + // There is no support for encrypting status messages in SAML. + // Only assertions, attributes, base ID and name ID can be encrypted + // See Chapter 6 of saml-core-2.0-os.pdf Document document = builder.buildDocument(); return buildErrorResponse(authSession, binding, document); } catch (Exception e) { @@ -457,7 +450,7 @@ public class SamlProtocol implements LoginProtocol { if (samlClient.requiresEncryption()) { PublicKey publicKey = null; try { - publicKey = SamlProtocolUtils.getEncryptionValidationKey(client); + publicKey = SamlProtocolUtils.getEncryptionKey(client); } catch (Exception e) { logger.error("failed", e); return ErrorPage.error(session, Messages.FAILED_TO_PROCESS_RESPONSE); diff --git a/services/src/main/java/org/keycloak/protocol/saml/SamlProtocolUtils.java b/services/src/main/java/org/keycloak/protocol/saml/SamlProtocolUtils.java index 026a54a599..7ab97a4a99 100755 --- a/services/src/main/java/org/keycloak/protocol/saml/SamlProtocolUtils.java +++ b/services/src/main/java/org/keycloak/protocol/saml/SamlProtocolUtils.java @@ -103,7 +103,7 @@ public class SamlProtocolUtils { * @return Public key for encryption. * @throws VerificationException */ - public static PublicKey getEncryptionValidationKey(ClientModel client) throws VerificationException { + public static PublicKey getEncryptionKey(ClientModel client) throws VerificationException { return getPublicKey(client, SamlConfigAttributes.SAML_ENCRYPTION_CERTIFICATE_ATTRIBUTE); } diff --git a/services/src/main/java/org/keycloak/protocol/saml/SamlService.java b/services/src/main/java/org/keycloak/protocol/saml/SamlService.java index e0ac524b8e..589dde3e6b 100755 --- a/services/src/main/java/org/keycloak/protocol/saml/SamlService.java +++ b/services/src/main/java/org/keycloak/protocol/saml/SamlService.java @@ -138,6 +138,13 @@ public class SamlService extends AuthorizationEndpointBase { protected Response handleSamlResponse(String samlResponse, String relayState) { event.event(EventType.LOGOUT); SAMLDocumentHolder holder = extractResponseDocument(samlResponse); + + if (! (holder.getSamlObject() instanceof StatusResponseType)) { + event.detail(Details.REASON, "invalid_saml_response"); + event.error(Errors.INVALID_SAML_RESPONSE); + return ErrorPage.error(session, Messages.INVALID_REQUEST); + } + StatusResponseType statusResponse = (StatusResponseType) holder.getSamlObject(); // validate destination if (statusResponse.getDestination() != null && !uriInfo.getAbsolutePath().toString().equals(statusResponse.getDestination())) { @@ -178,6 +185,12 @@ public class SamlService extends AuthorizationEndpointBase { SAML2Object samlObject = documentHolder.getSamlObject(); + if (! (samlObject instanceof RequestAbstractType)) { + event.event(EventType.LOGIN); + event.error(Errors.INVALID_SAML_AUTHN_REQUEST); + return ErrorPage.error(session, Messages.INVALID_REQUEST); + } + RequestAbstractType requestAbstractType = (RequestAbstractType) samlObject; String issuer = requestAbstractType.getIssuer().getValue(); ClientModel client = realm.getClientByClientId(issuer); diff --git a/services/src/main/java/org/keycloak/protocol/saml/SamlSessionUtils.java b/services/src/main/java/org/keycloak/protocol/saml/SamlSessionUtils.java index 0083fdc0dd..83b54dde73 100644 --- a/services/src/main/java/org/keycloak/protocol/saml/SamlSessionUtils.java +++ b/services/src/main/java/org/keycloak/protocol/saml/SamlSessionUtils.java @@ -24,6 +24,7 @@ import org.keycloak.models.ClientModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserSessionModel; +import org.keycloak.services.managers.UserSessionCrossDCManager; /** * @author Marek Posolda @@ -54,7 +55,9 @@ public class SamlSessionUtils { return null; } - UserSessionModel userSession = session.sessions().getUserSession(realm, parts[0]); + String userSessionId = parts[0]; + String clientUUID = parts[1]; + UserSessionModel userSession = new UserSessionCrossDCManager(session).getUserSessionWithClient(realm, userSessionId, false, clientUUID); if (userSession == null) { return null; } diff --git a/services/src/main/java/org/keycloak/protocol/saml/installation/SamlIDPDescriptorClientInstallation.java b/services/src/main/java/org/keycloak/protocol/saml/installation/SamlIDPDescriptorClientInstallation.java index b9c07eccc6..2261d520e6 100755 --- a/services/src/main/java/org/keycloak/protocol/saml/installation/SamlIDPDescriptorClientInstallation.java +++ b/services/src/main/java/org/keycloak/protocol/saml/installation/SamlIDPDescriptorClientInstallation.java @@ -127,7 +127,7 @@ public class SamlIDPDescriptorClientInstallation implements ClientInstallationPr @Override public String getHelpText() { - return "SAML Metadata IDSSODescriptor tailored for the client. This is special because not every client may require things like digital signatures"; + return "SAML Metadata IDPSSODescriptor tailored for the client. This is special because not every client may require things like digital signatures"; } @Override diff --git a/services/src/main/java/org/keycloak/protocol/saml/installation/SamlSPDescriptorClientInstallation.java b/services/src/main/java/org/keycloak/protocol/saml/installation/SamlSPDescriptorClientInstallation.java index bd109e6c9e..8a39de001f 100755 --- a/services/src/main/java/org/keycloak/protocol/saml/installation/SamlSPDescriptorClientInstallation.java +++ b/services/src/main/java/org/keycloak/protocol/saml/installation/SamlSPDescriptorClientInstallation.java @@ -47,8 +47,10 @@ public class SamlSPDescriptorClientInstallation implements ClientInstallationPro String nameIdFormat = samlClient.getNameIDFormat(); if (nameIdFormat == null) nameIdFormat = SamlProtocol.SAML_DEFAULT_NAMEID_FORMAT; String spCertificate = SPMetadataDescriptor.xmlKeyInfo(" ", null, samlClient.getClientSigningCertificate(), KeyTypes.SIGNING.value(), true); + String encCertificate = SPMetadataDescriptor.xmlKeyInfo(" ", null, samlClient.getClientEncryptingCertificate(), KeyTypes.ENCRYPTION.value(), true); return SPMetadataDescriptor.getSPDescriptor(JBossSAMLURIConstants.SAML_HTTP_POST_BINDING.get(), assertionUrl, logoutUrl, - samlClient.requiresClientSignature(), samlClient.requiresAssertionSignature(), client.getClientId(), nameIdFormat, spCertificate); + samlClient.requiresClientSignature(), samlClient.requiresAssertionSignature(), samlClient.requiresEncryption(), + client.getClientId(), nameIdFormat, spCertificate, encCertificate); } @Override diff --git a/services/src/main/java/org/keycloak/protocol/saml/profile/ecp/authenticator/HttpBasicAuthenticator.java b/services/src/main/java/org/keycloak/protocol/saml/profile/ecp/authenticator/HttpBasicAuthenticator.java old mode 100755 new mode 100644 index f21eff3bf0..f6821b6331 --- a/services/src/main/java/org/keycloak/protocol/saml/profile/ecp/authenticator/HttpBasicAuthenticator.java +++ b/services/src/main/java/org/keycloak/protocol/saml/profile/ecp/authenticator/HttpBasicAuthenticator.java @@ -1,182 +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.saml.profile.ecp.authenticator; import org.jboss.resteasy.spi.HttpRequest; -import org.keycloak.Config; import org.keycloak.authentication.AuthenticationFlowContext; import org.keycloak.authentication.AuthenticationFlowError; import org.keycloak.authentication.Authenticator; -import org.keycloak.authentication.AuthenticatorFactory; import org.keycloak.common.util.Base64; import org.keycloak.events.Errors; -import org.keycloak.models.AuthenticationExecutionModel.Requirement; import org.keycloak.models.KeycloakSession; -import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.RealmModel; import org.keycloak.models.UserCredentialModel; import org.keycloak.models.UserModel; -import org.keycloak.provider.ProviderConfigProperty; import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.Response; import java.io.IOException; import java.util.List; -/** - * @author Pedro Igor - */ -public class HttpBasicAuthenticator implements AuthenticatorFactory { +public class HttpBasicAuthenticator implements Authenticator { - public static final String PROVIDER_ID = "http-basic-authenticator"; + private static final String BASIC = "Basic"; + private static final String BASIC_PREFIX = BASIC + " "; @Override - public String getDisplayType() { - return "HTTP Basic Authentication"; + public void authenticate(final AuthenticationFlowContext context) { + final HttpRequest httpRequest = context.getHttpRequest(); + final HttpHeaders httpHeaders = httpRequest.getHttpHeaders(); + final String[] usernameAndPassword = getUsernameAndPassword(httpHeaders); + + context.attempted(); + + if (usernameAndPassword != null) { + final RealmModel realm = context.getRealm(); + final String username = usernameAndPassword[0]; + final UserModel user = context.getSession().users().getUserByUsername(username, realm); + + if (user != null) { + final String password = usernameAndPassword[1]; + final boolean valid = context.getSession().userCredentialManager().isValid(realm, user, UserCredentialModel.password(password)); + + if (valid) { + if (user.isEnabled()) { + userSuccessAction(context, user); + } else { + userDisabledAction(context, realm, user); + } + } else { + notValidCredentialsAction(context, realm, user); + } + } else { + nullUserAction(context, realm, username); + } + } + } + + protected void userSuccessAction(AuthenticationFlowContext context, UserModel user) { + context.getAuthenticationSession().setAuthenticatedUser(user); + context.success(); + } + + protected void userDisabledAction(AuthenticationFlowContext context, RealmModel realm, UserModel user) { + userSuccessAction(context, user); + } + + protected void nullUserAction(final AuthenticationFlowContext context, final RealmModel realm, final String user) { + // no-op by default + } + + protected void notValidCredentialsAction(final AuthenticationFlowContext context, final RealmModel realm, final UserModel user) { + context.getEvent().user(user); + context.getEvent().error(Errors.INVALID_USER_CREDENTIALS); + context.failure(AuthenticationFlowError.INVALID_USER, Response.status(Response.Status.UNAUTHORIZED) + .header(HttpHeaders.WWW_AUTHENTICATE, BASIC_PREFIX + "realm=\"" + realm.getName() + "\"") + .build()); + } + + private String[] getUsernameAndPassword(final HttpHeaders httpHeaders) { + final List authHeaders = httpHeaders.getRequestHeader(HttpHeaders.AUTHORIZATION); + + if (authHeaders == null || authHeaders.size() == 0) { + return null; + } + + String credentials = null; + + for (final String authHeader : authHeaders) { + if (authHeader.startsWith(BASIC_PREFIX)) { + final String[] split = authHeader.trim().split("\\s+"); + + if (split == null || split.length != 2) return null; + + credentials = split[1]; + } + } + + try { + return new String(Base64.decode(credentials)).split(":"); + } catch (final IOException e) { + throw new RuntimeException("Failed to parse credentials.", e); + } } @Override - public String getReferenceCategory() { - return null; + public void action(final AuthenticationFlowContext context) { + } @Override - public boolean isConfigurable() { + public boolean requiresUser() { return false; } @Override - public Requirement[] getRequirementChoices() { - return new Requirement[0]; - } - - @Override - public boolean isUserSetupAllowed() { + public boolean configuredFor(final KeycloakSession session, final RealmModel realm, final UserModel user) { return false; } @Override - public String getHelpText() { - return "Validates username and password from Authorization HTTP header"; - } - - @Override - public List getConfigProperties() { - return null; - } - - @Override - public Authenticator create(KeycloakSession session) { - return new Authenticator() { - - private static final String BASIC = "Basic"; - private static final String BASIC_PREFIX = BASIC + " "; - - @Override - public void authenticate(AuthenticationFlowContext context) { - HttpRequest httpRequest = context.getHttpRequest(); - HttpHeaders httpHeaders = httpRequest.getHttpHeaders(); - String[] usernameAndPassword = getUsernameAndPassword(httpHeaders); - - context.attempted(); - - if (usernameAndPassword != null) { - RealmModel realm = context.getRealm(); - UserModel user = context.getSession().users().getUserByUsername(usernameAndPassword[0], realm); - - if (user != null) { - String password = usernameAndPassword[1]; - boolean valid = context.getSession().userCredentialManager().isValid(realm, user, UserCredentialModel.password(password)); - - if (valid) { - context.getAuthenticationSession().setAuthenticatedUser(user); - context.success(); - } else { - context.getEvent().user(user); - context.getEvent().error(Errors.INVALID_USER_CREDENTIALS); - context.failure(AuthenticationFlowError.INVALID_USER, Response.status(Response.Status.UNAUTHORIZED) - .header(HttpHeaders.WWW_AUTHENTICATE, BASIC_PREFIX + "realm=\"" + realm.getName() + "\"") - .build()); - } - } - } - } - - private String[] getUsernameAndPassword(HttpHeaders httpHeaders) { - List authHeaders = httpHeaders.getRequestHeader(HttpHeaders.AUTHORIZATION); - - if (authHeaders == null || authHeaders.size() == 0) { - return null; - } - - String credentials = null; - - for (String authHeader : authHeaders) { - if (authHeader.startsWith(BASIC_PREFIX)) { - String[] split = authHeader.trim().split("\\s+"); - - if (split == null || split.length != 2) return null; - - credentials = split[1]; - } - } - - try { - return new String(Base64.decode(credentials)).split(":"); - } catch (IOException e) { - throw new RuntimeException("Failed to parse credentials.", e); - } - } - - @Override - public void action(AuthenticationFlowContext context) { - - } - - @Override - public boolean requiresUser() { - return false; - } - - @Override - public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) { - return false; - } - - @Override - public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) { - - } - - @Override - public void close() { - - } - }; - } - - @Override - public void init(Config.Scope config) { - - } - - @Override - public void postInit(KeycloakSessionFactory factory) { + public void setRequiredActions(final KeycloakSession session, final RealmModel realm, final UserModel user) { } @@ -184,9 +124,4 @@ public class HttpBasicAuthenticator implements AuthenticatorFactory { public void close() { } - - @Override - public String getId() { - return PROVIDER_ID; - } } diff --git a/services/src/main/java/org/keycloak/protocol/saml/profile/ecp/authenticator/HttpBasicAuthenticatorFactory.java b/services/src/main/java/org/keycloak/protocol/saml/profile/ecp/authenticator/HttpBasicAuthenticatorFactory.java new file mode 100755 index 0000000000..01adca2dc0 --- /dev/null +++ b/services/src/main/java/org/keycloak/protocol/saml/profile/ecp/authenticator/HttpBasicAuthenticatorFactory.java @@ -0,0 +1,115 @@ +/* + * 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.saml.profile.ecp.authenticator; + +import org.jboss.resteasy.spi.HttpRequest; +import org.keycloak.Config; +import org.keycloak.authentication.AuthenticationFlowContext; +import org.keycloak.authentication.AuthenticationFlowError; +import org.keycloak.authentication.Authenticator; +import org.keycloak.authentication.AuthenticatorFactory; +import org.keycloak.common.util.Base64; +import org.keycloak.events.Errors; +import org.keycloak.models.AuthenticationExecutionModel; +import org.keycloak.models.AuthenticationExecutionModel.Requirement; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserCredentialModel; +import org.keycloak.models.UserModel; +import org.keycloak.provider.ProviderConfigProperty; + +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.Response; +import java.io.IOException; +import java.util.List; + +/** + * @author Pedro Igor + */ +public class HttpBasicAuthenticatorFactory implements AuthenticatorFactory { + + public static final String PROVIDER_ID = "http-basic-authenticator"; + + @Override + public String getDisplayType() { + return "HTTP Basic Authentication"; + } + + @Override + public String getReferenceCategory() { + return "basic"; + } + + @Override + public boolean isConfigurable() { + return false; + } + + private static final AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = { + AuthenticationExecutionModel.Requirement.REQUIRED, + Requirement.ALTERNATIVE, + Requirement.OPTIONAL, + AuthenticationExecutionModel.Requirement.DISABLED + }; + + @Override + public AuthenticationExecutionModel.Requirement[] getRequirementChoices() { + return REQUIREMENT_CHOICES; + } + + @Override + public boolean isUserSetupAllowed() { + return false; + } + + @Override + public String getHelpText() { + return "Validates username and password from Authorization HTTP header"; + } + + @Override + public List getConfigProperties() { + return null; + } + + @Override + public Authenticator create(final KeycloakSession session) { + return new HttpBasicAuthenticator(); + } + + @Override + public void init(final Config.Scope config) { + + } + + @Override + public void postInit(final KeycloakSessionFactory factory) { + + } + + @Override + public void close() { + + } + + @Override + public String getId() { + return PROVIDER_ID; + } +} diff --git a/services/src/main/java/org/keycloak/services/DefaultKeycloakSessionFactory.java b/services/src/main/java/org/keycloak/services/DefaultKeycloakSessionFactory.java index 0869c5dfc3..21da694ca7 100755 --- a/services/src/main/java/org/keycloak/services/DefaultKeycloakSessionFactory.java +++ b/services/src/main/java/org/keycloak/services/DefaultKeycloakSessionFactory.java @@ -112,6 +112,7 @@ public class DefaultKeycloakSessionFactory implements KeycloakSessionFactory, Pr public void deploy(ProviderManager pm) { Map, Map> copy = getFactoriesCopy(); Map, Map> newFactories = loadFactories(pm); + List deployed = new LinkedList<>(); List undeployed = new LinkedList<>(); for (Map.Entry, Map> entry : newFactories.entrySet()) { @@ -120,6 +121,7 @@ public class DefaultKeycloakSessionFactory implements KeycloakSessionFactory, Pr copy.put(entry.getKey(), entry.getValue()); } else { for (ProviderFactory f : entry.getValue().values()) { + deployed.add(f); ProviderFactory old = current.remove(f.getId()); if (old != null) undeployed.add(old); } @@ -131,6 +133,9 @@ public class DefaultKeycloakSessionFactory implements KeycloakSessionFactory, Pr for (ProviderFactory factory : undeployed) { factory.close(); } + for (ProviderFactory factory : deployed) { + factory.postInit(this); + } } @Override 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 da693aa83c..d6683f1456 100755 --- a/services/src/main/java/org/keycloak/services/clientregistration/AbstractClientRegistrationProvider.java +++ b/services/src/main/java/org/keycloak/services/clientregistration/AbstractClientRegistrationProvider.java @@ -97,9 +97,12 @@ public abstract class AbstractClientRegistrationProvider implements ClientRegist auth.requireView(client); ClientRepresentation rep = ModelToRepresentation.toRepresentation(client); + if (client.getSecret() != null) { + rep.setSecret(client.getSecret()); + } if (auth.isRegistrationAccessToken()) { - String registrationAccessToken = ClientRegistrationTokenUtils.updateRegistrationAccessToken(session, client, auth.getRegistrationAuth()); + String registrationAccessToken = ClientRegistrationTokenUtils.updateTokenSignature(session, auth); rep.setRegistrationAccessToken(registrationAccessToken); } diff --git a/services/src/main/java/org/keycloak/services/clientregistration/ClientRegistrationAuth.java b/services/src/main/java/org/keycloak/services/clientregistration/ClientRegistrationAuth.java index dfed5aa220..88986b5448 100644 --- a/services/src/main/java/org/keycloak/services/clientregistration/ClientRegistrationAuth.java +++ b/services/src/main/java/org/keycloak/services/clientregistration/ClientRegistrationAuth.java @@ -60,6 +60,8 @@ public class ClientRegistrationAuth { private RealmModel realm; private JsonWebToken jwt; private ClientInitialAccessModel initialAccessModel; + private String kid; + private String token; public ClientRegistrationAuth(KeycloakSession session, ClientRegistrationProvider provider, EventBuilder event) { this.session = session; @@ -81,10 +83,13 @@ public class ClientRegistrationAuth { return; } - ClientRegistrationTokenUtils.TokenVerification tokenVerification = ClientRegistrationTokenUtils.verifyToken(session, realm, uri, split[1]); + token = split[1]; + + ClientRegistrationTokenUtils.TokenVerification tokenVerification = ClientRegistrationTokenUtils.verifyToken(session, realm, uri, token); if (tokenVerification.getError() != null) { throw unauthorized(tokenVerification.getError().getMessage()); } + kid = tokenVerification.getKid(); jwt = tokenVerification.getJwt(); if (isInitialAccessToken()) { @@ -95,6 +100,18 @@ public class ClientRegistrationAuth { } } + public String getToken() { + return token; + } + + public String getKid() { + return kid; + } + + public JsonWebToken getJwt() { + return jwt; + } + private boolean isBearerToken() { return jwt != null && TokenUtil.TOKEN_TYPE_BEARER.equals(jwt.getType()); } diff --git a/services/src/main/java/org/keycloak/services/clientregistration/ClientRegistrationTokenUtils.java b/services/src/main/java/org/keycloak/services/clientregistration/ClientRegistrationTokenUtils.java index e2d4846735..270ca2abe0 100755 --- a/services/src/main/java/org/keycloak/services/clientregistration/ClientRegistrationTokenUtils.java +++ b/services/src/main/java/org/keycloak/services/clientregistration/ClientRegistrationTokenUtils.java @@ -44,6 +44,27 @@ public class ClientRegistrationTokenUtils { public static final String TYPE_INITIAL_ACCESS_TOKEN = "InitialAccessToken"; public static final String TYPE_REGISTRATION_ACCESS_TOKEN = "RegistrationAccessToken"; + public static String updateTokenSignature(KeycloakSession session, ClientRegistrationAuth auth) { + KeyManager.ActiveRsaKey keys = session.keys().getActiveRsaKey(session.getContext().getRealm()); + + if (keys.getKid().equals(auth.getKid())) { + return auth.getToken(); + } else { + RegistrationAccessToken regToken = new RegistrationAccessToken(); + regToken.setRegistrationAuth(auth.getRegistrationAuth().toString().toLowerCase()); + + regToken.type(auth.getJwt().getType()); + regToken.id(auth.getJwt().getId()); + regToken.issuedAt(Time.currentTime()); + regToken.expiration(0); + regToken.issuer(auth.getJwt().getIssuer()); + regToken.audience(auth.getJwt().getIssuer()); + + String token = new JWSBuilder().kid(keys.getKid()).jsonContent(regToken).rsa256(keys.getPrivateKey()); + return token; + } + } + public static String updateRegistrationAccessToken(KeycloakSession session, ClientModel client, RegistrationAuth registrationAuth) { return updateRegistrationAccessToken(session, session.getContext().getRealm(), session.getContext().getUri(), client, registrationAuth); } @@ -75,7 +96,8 @@ public class ClientRegistrationTokenUtils { return TokenVerification.error(new RuntimeException("Invalid token", e)); } - PublicKey publicKey = session.keys().getRsaPublicKey(realm, input.getHeader().getKeyId()); + String kid = input.getHeader().getKeyId(); + PublicKey publicKey = session.keys().getRsaPublicKey(realm, kid); if (!RSAProvider.verify(input, publicKey)) { return TokenVerification.error(new RuntimeException("Failed verify token")); @@ -102,7 +124,7 @@ public class ClientRegistrationTokenUtils { return TokenVerification.error(new RuntimeException("Invalid type of token")); } - return TokenVerification.success(jwt); + return TokenVerification.success(kid, jwt); } private static String setupToken(JsonWebToken jwt, KeycloakSession session, RealmModel realm, UriInfo uri, String id, String type, int expiration) { @@ -127,22 +149,28 @@ public class ClientRegistrationTokenUtils { protected static class TokenVerification { + private final String kid; private final JsonWebToken jwt; private final RuntimeException error; - public static TokenVerification success(JsonWebToken jwt) { - return new TokenVerification(jwt, null); + public static TokenVerification success(String kid, JsonWebToken jwt) { + return new TokenVerification(kid, jwt, null); } public static TokenVerification error(RuntimeException error) { - return new TokenVerification(null, error); + return new TokenVerification(null,null, error); } - private TokenVerification(JsonWebToken jwt, RuntimeException error) { + private TokenVerification(String kid, JsonWebToken jwt, RuntimeException error) { + this.kid = kid; this.jwt = jwt; this.error = error; } + public String getKid() { + return kid; + } + public JsonWebToken getJwt() { return jwt; } diff --git a/services/src/main/java/org/keycloak/services/clientregistration/policy/RegistrationAuth.java b/services/src/main/java/org/keycloak/services/clientregistration/policy/RegistrationAuth.java index eca5ca198e..bad5bc435f 100644 --- a/services/src/main/java/org/keycloak/services/clientregistration/policy/RegistrationAuth.java +++ b/services/src/main/java/org/keycloak/services/clientregistration/policy/RegistrationAuth.java @@ -17,8 +17,6 @@ package org.keycloak.services.clientregistration.policy; -import org.keycloak.services.clientregistration.RegistrationAccessToken; - /** * @author Marek Posolda */ diff --git a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java index 6c917591c3..bc28fc4afd 100755 --- a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java +++ b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java @@ -192,16 +192,12 @@ public class AuthenticationManager { // Logout all clientSessions of this user and client public static void backchannelUserFromClient(KeycloakSession session, RealmModel realm, UserModel user, ClientModel client, UriInfo uriInfo, HttpHeaders headers) { - String clientId = client.getId(); - List userSessions = session.sessions().getUserSessions(realm, user); for (UserSessionModel userSession : userSessions) { - Collection clientSessions = userSession.getAuthenticatedClientSessions().values(); - for (AuthenticatedClientSessionModel clientSession : clientSessions) { - if (clientSession.getClient().getId().equals(clientId)) { - AuthenticationManager.backchannelLogoutClientSession(session, realm, clientSession, userSession, uriInfo, headers); - TokenManager.dettachClientSession(session.sessions(), realm, clientSession); - } + AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessions().get(client.getId()); + if (clientSession != null) { + AuthenticationManager.backchannelLogoutClientSession(session, realm, clientSession, userSession, uriInfo, headers); + TokenManager.dettachClientSession(session.sessions(), realm, clientSession); } } } diff --git a/services/src/main/java/org/keycloak/services/managers/CodeGenerateUtil.java b/services/src/main/java/org/keycloak/services/managers/CodeGenerateUtil.java index a975aa5cd7..3d0c9ca9fd 100644 --- a/services/src/main/java/org/keycloak/services/managers/CodeGenerateUtil.java +++ b/services/src/main/java/org/keycloak/services/managers/CodeGenerateUtil.java @@ -21,7 +21,6 @@ import java.util.HashMap; import java.util.Map; import org.jboss.logging.Logger; -import org.keycloak.OAuth2Constants; import org.keycloak.models.AuthenticatedClientSessionModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; @@ -120,9 +119,13 @@ class CodeGenerateUtil { String userSessionId = parts[2]; String clientUUID = parts[3]; - UserSessionModel userSession = session.sessions().getUserSession(realm, userSessionId); + UserSessionModel userSession = new UserSessionCrossDCManager(session).getUserSessionWithClientAndCodeToTokenAction(realm, userSessionId, clientUUID); if (userSession == null) { - return null; + // TODO:mposolda Temporary workaround needed to track if code is invalid or was already used. Will be good to remove once used OAuth codes are tracked through one-time cache + userSession = session.sessions().getUserSession(realm, userSessionId); + if (userSession == null) { + return null; + } } return userSession.getAuthenticatedClientSessions().get(clientUUID); diff --git a/services/src/main/java/org/keycloak/services/managers/ResourceAdminManager.java b/services/src/main/java/org/keycloak/services/managers/ResourceAdminManager.java index 9aa4b69293..3d71c2a663 100755 --- a/services/src/main/java/org/keycloak/services/managers/ResourceAdminManager.java +++ b/services/src/main/java/org/keycloak/services/managers/ResourceAdminManager.java @@ -135,20 +135,6 @@ public class ResourceAdminManager { } } - public void logoutUserFromClient(URI requestUri, RealmModel realm, ClientModel resource, UserModel user) { - List userSessions = session.sessions().getUserSessions(realm, user); - List ourAppClientSessions = new LinkedList<>(); - if (userSessions != null) { - for (UserSessionModel userSession : userSessions) { - AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessions().get(resource.getId()); - if (clientSession != null) { - ourAppClientSessions.add(clientSession); - } - } - } - - logoutClientSessions(requestUri, realm, resource, ourAppClientSessions); - } public boolean logoutClientSession(URI requestUri, RealmModel realm, ClientModel resource, AuthenticatedClientSessionModel clientSession) { return logoutClientSessions(requestUri, realm, resource, Arrays.asList(clientSession)); diff --git a/services/src/main/java/org/keycloak/services/managers/UserSessionCrossDCManager.java b/services/src/main/java/org/keycloak/services/managers/UserSessionCrossDCManager.java new file mode 100644 index 0000000000..11795e5456 --- /dev/null +++ b/services/src/main/java/org/keycloak/services/managers/UserSessionCrossDCManager.java @@ -0,0 +1,78 @@ +/* + * Copyright 2017 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.managers; + +import java.util.Map; + +import org.jboss.logging.Logger; +import org.keycloak.models.AuthenticatedClientSessionModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserSessionModel; +import org.keycloak.sessions.CommonClientSessionModel; + +/** + * @author Marek Posolda + */ +public class UserSessionCrossDCManager { + + private static final Logger logger = Logger.getLogger(UserSessionCrossDCManager.class); + + private final KeycloakSession kcSession; + + public UserSessionCrossDCManager(KeycloakSession session) { + this.kcSession = session; + } + + + // get userSession if it has "authenticatedClientSession" of specified client attached to it. Otherwise download it from remoteCache + public UserSessionModel getUserSessionWithClient(RealmModel realm, String id, boolean offline, String clientUUID) { + return kcSession.sessions().getUserSessionWithPredicate(realm, id, offline, userSession -> userSession.getAuthenticatedClientSessions().containsKey(clientUUID)); + } + + + // get userSession if it has "authenticatedClientSession" of specified client attached to it and there is "CODE_TO_TOKEN" action. Otherwise download it from remoteCache + // TODO Probably remove this method once AuthenticatedClientSession.getAction is removed and information is moved to OAuth code JWT instead + public UserSessionModel getUserSessionWithClientAndCodeToTokenAction(RealmModel realm, String id, String clientUUID) { + + return kcSession.sessions().getUserSessionWithPredicate(realm, id, false, (UserSessionModel userSession) -> { + + Map authSessions = userSession.getAuthenticatedClientSessions(); + if (!authSessions.containsKey(clientUUID)) { + return false; + } + + AuthenticatedClientSessionModel authSession = authSessions.get(clientUUID); + return CommonClientSessionModel.Action.CODE_TO_TOKEN.toString().equals(authSession.getAction()); + + }); + } + + + // Just check if userSession also exists on remoteCache. It can happen that logout happened on 2nd DC and userSession is already removed on remoteCache and this DC wasn't yet notified + public UserSessionModel getUserSessionIfExistsRemotely(RealmModel realm, String id) { + UserSessionModel userSession = kcSession.sessions().getUserSession(realm, id); + + // This will remove userSession "locally" if it doesn't exists on remoteCache + kcSession.sessions().getUserSessionWithPredicate(realm, id, false, (UserSessionModel userSession2) -> { + return userSession2 == null; + }); + + return kcSession.sessions().getUserSession(realm, id); + } +} diff --git a/services/src/main/java/org/keycloak/services/managers/UserStorageSyncManager.java b/services/src/main/java/org/keycloak/services/managers/UserStorageSyncManager.java index cec46f2d6f..797301cc2a 100755 --- a/services/src/main/java/org/keycloak/services/managers/UserStorageSyncManager.java +++ b/services/src/main/java/org/keycloak/services/managers/UserStorageSyncManager.java @@ -171,7 +171,7 @@ public class UserStorageSyncManager { } UserStorageProviderClusterEvent event = UserStorageProviderClusterEvent.createEvent(removed, realm.getId(), provider); - session.getProvider(ClusterProvider.class).notify(USER_STORAGE_TASK_KEY, event, false); + session.getProvider(ClusterProvider.class).notify(USER_STORAGE_TASK_KEY, event, false, ClusterProvider.DCNotify.ALL_DCS); } diff --git a/services/src/main/java/org/keycloak/services/messages/Messages.java b/services/src/main/java/org/keycloak/services/messages/Messages.java index 295f07b27e..180694ada4 100755 --- a/services/src/main/java/org/keycloak/services/messages/Messages.java +++ b/services/src/main/java/org/keycloak/services/messages/Messages.java @@ -35,6 +35,10 @@ public class Messages { public static final String EXPIRED_ACTION = "expiredActionMessage"; + public static final String EXPIRED_ACTION_TOKEN_NO_SESSION = "expiredActionTokenNoSessionMessage"; + + public static final String EXPIRED_ACTION_TOKEN_SESSION_EXISTS = "expiredActionTokenSessionExistsMessage"; + public static final String MISSING_FIRST_NAME = "missingFirstNameMessage"; public static final String MISSING_LAST_NAME = "missingLastNameMessage"; @@ -156,6 +160,12 @@ public class Messages { public static final String IDENTITY_PROVIDER_LINK_SUCCESS = "identityProviderLinkSuccess"; + public static final String CONFIRM_ACCOUNT_LINKING = "confirmAccountLinking"; + + public static final String CONFIRM_EMAIL_ADDRESS_VERIFICATION = "confirmEmailAddressVerification"; + + public static final String CONFIRM_EXECUTION_OF_ACTIONS = "confirmExecutionOfActions"; + public static final String STALE_CODE = "staleCodeMessage"; public static final String STALE_CODE_ACCOUNT = "staleCodeAccountMessage"; diff --git a/services/src/main/java/org/keycloak/services/resources/AccountService.java b/services/src/main/java/org/keycloak/services/resources/AccountService.java index 9dd9c4b55c..ac9bf807f6 100755 --- a/services/src/main/java/org/keycloak/services/resources/AccountService.java +++ b/services/src/main/java/org/keycloak/services/resources/AccountService.java @@ -206,7 +206,7 @@ public class AccountService extends AbstractSecuredLocalService { setReferrerOnPage(); - UserSessionModel userSession = auth.getClientSession().getUserSession(); + UserSessionModel userSession = auth.getSession(); AuthenticationSessionModel authSession = session.authenticationSessions().getAuthenticationSession(realm, userSession.getId()); if (authSession != null) { String forwardedError = authSession.getAuthNote(ACCOUNT_MGMT_FORWARDED_ERROR_NOTE); @@ -663,7 +663,7 @@ public class AccountService extends AbstractSecuredLocalService { EventBuilder errorEvent = event.clone().event(EventType.UPDATE_PASSWORD_ERROR) .client(auth.getClient()) - .user(auth.getClientSession().getUserSession().getUser()); + .user(auth.getSession().getUser()); if (requireCurrent) { if (Validation.isBlank(password)) { diff --git a/services/src/main/java/org/keycloak/services/resources/Cors.java b/services/src/main/java/org/keycloak/services/resources/Cors.java index f938a5f487..c9bfa030fe 100755 --- a/services/src/main/java/org/keycloak/services/resources/Cors.java +++ b/services/src/main/java/org/keycloak/services/resources/Cors.java @@ -133,7 +133,11 @@ public class Cors { return builder.build(); } - builder.header(ACCESS_CONTROL_ALLOW_ORIGIN, origin); + if (allowedOrigins != null && allowedOrigins.contains(ACCESS_CONTROL_ALLOW_ORIGIN_WILDCARD)) { + builder.header(ACCESS_CONTROL_ALLOW_ORIGIN, ACCESS_CONTROL_ALLOW_ORIGIN_WILDCARD); + } else { + builder.header(ACCESS_CONTROL_ALLOW_ORIGIN, origin); + } if (preflight) { if (allowedMethods != null) { @@ -178,7 +182,11 @@ public class Cors { logger.debug("build CORS headers and return"); - response.getOutputHeaders().add(ACCESS_CONTROL_ALLOW_ORIGIN, origin); + if (allowedOrigins.contains(ACCESS_CONTROL_ALLOW_ORIGIN_WILDCARD)) { + response.getOutputHeaders().add(ACCESS_CONTROL_ALLOW_ORIGIN, ACCESS_CONTROL_ALLOW_ORIGIN_WILDCARD); + } else { + response.getOutputHeaders().add(ACCESS_CONTROL_ALLOW_ORIGIN, origin); + } if (preflight) { if (allowedMethods != null) { diff --git a/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java b/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java index eed2858e73..7961163231 100755 --- a/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java +++ b/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java @@ -221,18 +221,6 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal } - // only allow origins from client. Not sure we need this as I don't believe cookies can be - // sent if CORS preflight requests can't execute. - String origin = headers.getRequestHeaders().getFirst("Origin"); - if (origin != null) { - String redirectOrigin = UriUtils.getOrigin(redirectUri); - if (!redirectOrigin.equals(origin)) { - event.error(Errors.ILLEGAL_ORIGIN); - throw new ErrorPageException(session, Messages.INVALID_REQUEST); - - } - } - AuthenticationManager.AuthResult cookieResult = AuthenticationManager.authenticateIdentityCookie(session, realmModel, true); String errorParam = "link_error"; if (cookieResult == null) { @@ -979,7 +967,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal return ParsedCodeContext.response(staleCodeError); } - SessionCodeChecks checks = new SessionCodeChecks(realmModel, uriInfo, clientConnection, session, event, code, null, clientId, LoginActionsService.AUTHENTICATE_PATH); + SessionCodeChecks checks = new SessionCodeChecks(realmModel, uriInfo, request, clientConnection, session, event, code, null, clientId, LoginActionsService.AUTHENTICATE_PATH); checks.initialVerify(); if (!checks.verifyActiveAndValidAction(AuthenticationSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.LOGIN)) { @@ -993,7 +981,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal Response errorResponse = checks.getResponse(); // Remove "code" from browser history - errorResponse = BrowserHistoryHelper.getInstance().saveResponseAndRedirect(session, authSession, errorResponse, true); + errorResponse = BrowserHistoryHelper.getInstance().saveResponseAndRedirect(session, authSession, errorResponse, true, request); return ParsedCodeContext.response(errorResponse); } } else { diff --git a/services/src/main/java/org/keycloak/services/resources/JsResource.java b/services/src/main/java/org/keycloak/services/resources/JsResource.java index 3e41dad355..404d3e4ce9 100755 --- a/services/src/main/java/org/keycloak/services/resources/JsResource.java +++ b/services/src/main/java/org/keycloak/services/resources/JsResource.java @@ -17,14 +17,15 @@ package org.keycloak.services.resources; -import org.keycloak.Config; import org.keycloak.common.Version; +import org.keycloak.services.util.CacheControlUtil; import org.keycloak.utils.MediaType; import javax.ws.rs.GET; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; import javax.ws.rs.core.CacheControl; import javax.ws.rs.core.Response; import java.io.InputStream; @@ -45,37 +46,29 @@ public class JsResource { @GET @Path("/keycloak.js") @Produces(MediaType.TEXT_PLAIN_JAVASCRIPT) - public Response getKeycloakJs() { - return getJs("keycloak.js"); + public Response getKeycloakJs(@QueryParam("version") String version) { + return getJs("keycloak.js", version); } @GET @Path("/{version}/keycloak.js") @Produces(MediaType.TEXT_PLAIN_JAVASCRIPT) public Response getKeycloakJsWithVersion(@PathParam("version") String version) { - if (!version.equals(Version.RESOURCES_VERSION)) { - return Response.status(Response.Status.NOT_FOUND).build(); - } - - return getKeycloakJs(); + return getJs("keycloak.js", version); } @GET @Path("/keycloak.min.js") @Produces(MediaType.TEXT_PLAIN_JAVASCRIPT) - public Response getKeycloakMinJs() { - return getJs("keycloak.min.js"); + public Response getKeycloakMinJs(@QueryParam("version") String version) { + return getJs("keycloak.min.js", version); } @GET @Path("/{version}/keycloak.min.js") @Produces(MediaType.TEXT_PLAIN_JAVASCRIPT) public Response getKeycloakMinJsWithVersion(@PathParam("version") String version) { - if (!version.equals(Version.RESOURCES_VERSION)) { - return Response.status(Response.Status.NOT_FOUND).build(); - } - - return getKeycloakMinJs(); + return getJs("keycloak.min.js", version); } /** @@ -86,46 +79,44 @@ public class JsResource { @GET @Path("/keycloak-authz.js") @Produces(MediaType.TEXT_PLAIN_JAVASCRIPT) - public Response getKeycloakAuthzJs() { - return getJs("keycloak-authz.js"); + public Response getKeycloakAuthzJs(@QueryParam("version") String version) { + return getJs("keycloak-authz.js", version); } @GET @Path("/{version}/keycloak-authz.js") @Produces(MediaType.TEXT_PLAIN_JAVASCRIPT) public Response getKeycloakAuthzJsWithVersion(@PathParam("version") String version) { - if (!version.equals(Version.RESOURCES_VERSION)) { - return Response.status(Response.Status.NOT_FOUND).build(); - } - - return getKeycloakAuthzJs(); + return getJs("keycloak-authz.js", version); } @GET @Path("/keycloak-authz.min.js") @Produces(MediaType.TEXT_PLAIN_JAVASCRIPT) - public Response getKeycloakAuthzMinJs() { - return getJs("keycloak-authz.min.js"); + public Response getKeycloakAuthzMinJs(@QueryParam("version") String version) { + return getJs("keycloak-authz.min.js", version); } @GET @Path("/{version}/keycloak-authz.min.js") @Produces(MediaType.TEXT_PLAIN_JAVASCRIPT) public Response getKeycloakAuthzMinJsWithVersion(@PathParam("version") String version) { - if (!version.equals(Version.RESOURCES_VERSION)) { - return Response.status(Response.Status.NOT_FOUND).build(); - } - - return getKeycloakAuthzMinJs(); + return getJs("keycloak-authz.min.js", version); } - private Response getJs(String name) { + private Response getJs(String name, String version) { + CacheControl cacheControl; + if (version != null) { + if (!version.equals(Version.RESOURCES_VERSION)) { + return Response.status(Response.Status.NOT_FOUND).build(); + } + cacheControl = CacheControlUtil.getDefaultCacheControl(); + } else { + cacheControl = CacheControlUtil.noCache(); + } + InputStream inputStream = getClass().getClassLoader().getResourceAsStream(name); if (inputStream != null) { - CacheControl cacheControl = new CacheControl(); - cacheControl.setNoTransform(false); - cacheControl.setMaxAge(Config.scope("theme").getInt("staticMaxAge", -1)); - return Response.ok(inputStream).type("text/javascript").cacheControl(cacheControl).build(); } else { return Response.status(Response.Status.NOT_FOUND).build(); 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 42a2fd8d7e..cf8910ccf0 100644 --- a/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java +++ b/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java @@ -78,6 +78,7 @@ import java.util.Map; import java.util.Properties; import java.util.Set; import java.util.StringTokenizer; +import java.util.concurrent.atomic.AtomicBoolean; /** * @author Bill Burke @@ -153,20 +154,20 @@ public class KeycloakApplication extends Application { exportImportManager[0].runExport(); } - boolean bootstrapAdminUser = false; - KeycloakSession session = sessionFactory.create(); - try { - session.getTransactionManager().begin(); - bootstrapAdminUser = new ApplianceBootstrap(session).isNoMasterUser(); + AtomicBoolean bootstrapAdminUser = new AtomicBoolean(false); + KeycloakModelUtils.runJobInTransaction(sessionFactory, new KeycloakSessionTask() { - session.getTransactionManager().commit(); - } finally { - session.close(); - } + @Override + public void run(KeycloakSession session) { + boolean shouldBootstrapAdmin = new ApplianceBootstrap(session).isNoMasterUser(); + bootstrapAdminUser.set(shouldBootstrapAdmin); - sessionFactory.publish(new PostMigrationEvent()); + sessionFactory.publish(new PostMigrationEvent(session)); + } - singletons.add(new WelcomeResource(bootstrapAdminUser)); + }); + + singletons.add(new WelcomeResource(bootstrapAdminUser.get())); setupScheduledTasks(sessionFactory); } catch (Throwable t) { diff --git a/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java b/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java index b1bd354207..f6dd8a43ab 100755 --- a/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java +++ b/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java @@ -16,7 +16,6 @@ */ package org.keycloak.services.resources; -import org.keycloak.authentication.actiontoken.DefaultActionToken; import org.keycloak.authentication.actiontoken.DefaultActionTokenKey; import org.jboss.logging.Logger; import org.jboss.resteasy.spi.HttpRequest; @@ -27,6 +26,7 @@ import org.keycloak.authentication.RequiredActionContextResult; import org.keycloak.authentication.RequiredActionFactory; import org.keycloak.authentication.RequiredActionProvider; import org.keycloak.TokenVerifier; +import org.keycloak.authentication.ExplainedVerificationException; import org.keycloak.authentication.actiontoken.*; import org.keycloak.authentication.actiontoken.resetcred.ResetCredentialsActionTokenHandler; import org.keycloak.authentication.authenticators.broker.AbstractIdpAuthenticator; @@ -42,6 +42,7 @@ import org.keycloak.events.Errors; import org.keycloak.events.EventBuilder; import org.keycloak.events.EventType; import org.keycloak.exceptions.TokenNotActiveException; +import org.keycloak.models.ActionTokenKeyModel; import org.keycloak.models.AuthenticationFlowModel; import org.keycloak.models.AuthenticatedClientSessionModel; import org.keycloak.models.ClientModel; @@ -59,6 +60,7 @@ import org.keycloak.protocol.LoginProtocol.Error; import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.protocol.oidc.utils.OIDCResponseMode; import org.keycloak.protocol.oidc.utils.OIDCResponseType; +import org.keycloak.representations.JsonWebToken; import org.keycloak.services.ErrorPage; import org.keycloak.services.ServicesLogger; import org.keycloak.services.Urls; @@ -179,7 +181,7 @@ public class LoginActionsService { } private SessionCodeChecks checksForCode(String code, String execution, String clientId, String flowPath) { - SessionCodeChecks res = new SessionCodeChecks(realm, uriInfo, clientConnection, session, event, code, execution, clientId, flowPath); + SessionCodeChecks res = new SessionCodeChecks(realm, uriInfo, request, clientConnection, session, event, code, execution, clientId, flowPath); res.initialVerify(); return res; } @@ -200,7 +202,7 @@ public class LoginActionsService { @GET public Response restartSession(@QueryParam("client_id") String clientId) { event.event(EventType.RESTART_AUTHENTICATION); - SessionCodeChecks checks = new SessionCodeChecks(realm, uriInfo, clientConnection, session, event, null, null, clientId, null); + SessionCodeChecks checks = new SessionCodeChecks(realm, uriInfo, request, clientConnection, session, event, null, null, clientId, null); AuthenticationSessionModel authSession = checks.initialVerifyAuthSession(); if (authSession == null) { @@ -286,7 +288,7 @@ public class LoginActionsService { authSession = processor.getAuthenticationSession(); // Could be changed (eg. Forked flow) } - return BrowserHistoryHelper.getInstance().saveResponseAndRedirect(session, authSession, response, action); + return BrowserHistoryHelper.getInstance().saveResponseAndRedirect(session, authSession, response, action, request); } /** @@ -342,7 +344,7 @@ public class LoginActionsService { } authSession = createAuthenticationSessionForClient(); - return processResetCredentials(false, null, authSession); + return processResetCredentials(false, null, authSession, null); } event.event(EventType.RESET_PASSWORD); @@ -386,7 +388,7 @@ public class LoginActionsService { } - return processResetCredentials(checks.isActionRequest(), execution, authSession); + return processResetCredentials(checks.isActionRequest(), execution, authSession, null); } /** @@ -405,7 +407,7 @@ public class LoginActionsService { return handleActionToken(key, execution, clientId); } - protected Response handleActionToken(String tokenString, String execution, String clientId) { + protected Response handleActionToken(String tokenString, String execution, String clientId) { T token; ActionTokenHandler handler; ActionTokenContext tokenContext; @@ -430,8 +432,8 @@ public class LoginActionsService { throw new ExplainedTokenVerificationException(null, Errors.NOT_ALLOWED, Messages.INVALID_REQUEST); } - TokenVerifier tokenVerifier = TokenVerifier.create(tokenString, DefaultActionToken.class); - DefaultActionToken aToken = tokenVerifier.getToken(); + TokenVerifier tokenVerifier = TokenVerifier.create(tokenString, DefaultActionTokenKey.class); + DefaultActionTokenKey aToken = tokenVerifier.getToken(); event .detail(Details.TOKEN_ID, aToken.getId()) @@ -469,12 +471,16 @@ public class LoginActionsService { flowPath = AUTHENTICATE_PATH; } AuthenticationProcessor.resetFlow(authSession, flowPath); - return processAuthentication(false, null, authSession, Messages.LOGIN_TIMEOUT); + + // Process correct flow + return processFlowFromPath(flowPath, authSession, Messages.EXPIRED_ACTION_TOKEN_SESSION_EXISTS); } - return handleActionTokenVerificationException(null, ex, Errors.EXPIRED_CODE, defaultErrorMessage); + return handleActionTokenVerificationException(null, ex, Errors.EXPIRED_CODE, Messages.EXPIRED_ACTION_TOKEN_NO_SESSION); } catch (ExplainedTokenVerificationException ex) { return handleActionTokenVerificationException(null, ex, ex.getErrorEvent(), ex.getMessage()); + } catch (ExplainedVerificationException ex) { + return handleActionTokenVerificationException(null, ex, ex.getErrorEvent(), ex.getMessage()); } catch (VerificationException ex) { return handleActionTokenVerificationException(null, ex, eventError, defaultErrorMessage); } @@ -483,7 +489,7 @@ public class LoginActionsService { tokenContext = new ActionTokenContext(session, realm, uriInfo, clientConnection, request, event, handler, execution, this::processFlow, this::brokerLoginFlow); try { - String tokenAuthSessionId = handler.getAuthenticationSessionIdFromToken(token); + String tokenAuthSessionId = handler.getAuthenticationSessionIdFromToken(token, tokenContext); if (tokenAuthSessionId != null) { // This can happen if the token contains ID but user opens the link in a new browser @@ -539,7 +545,19 @@ public class LoginActionsService { } } - private ActionTokenHandler resolveActionTokenHandler(String actionId) throws VerificationException { + private Response processFlowFromPath(String flowPath, AuthenticationSessionModel authSession, String errorMessage) { + if (AUTHENTICATE_PATH.equals(flowPath)) { + return processAuthentication(false, null, authSession, errorMessage); + } else if (REGISTRATION_PATH.equals(flowPath)) { + return processRegistration(false, null, authSession, errorMessage); + } else if (RESET_CREDENTIALS_PATH.equals(flowPath)) { + return processResetCredentials(false, null, authSession, errorMessage); + } else { + return ErrorPage.error(session, errorMessage == null ? Messages.INVALID_REQUEST : errorMessage); + } + } + + private ActionTokenHandler resolveActionTokenHandler(String actionId) throws VerificationException { if (actionId == null) { throw new VerificationException("Action token operation not set"); } @@ -562,10 +580,10 @@ public class LoginActionsService { return ErrorPage.error(session, errorMessage == null ? Messages.INVALID_CODE : errorMessage); } - protected Response processResetCredentials(boolean actionRequest, String execution, AuthenticationSessionModel authSession) { + protected Response processResetCredentials(boolean actionRequest, String execution, AuthenticationSessionModel authSession, String errorMessage) { AuthenticationProcessor authProcessor = new ResetCredentialsActionTokenHandler.ResetCredsAuthenticationProcessor(); - return processFlow(actionRequest, execution, authSession, RESET_CREDENTIALS_PATH, realm.getResetCredentialsFlow(), null, authProcessor); + return processFlow(actionRequest, execution, authSession, RESET_CREDENTIALS_PATH, realm.getResetCredentialsFlow(), errorMessage, authProcessor); } @@ -911,7 +929,7 @@ public class LoginActionsService { throw new RuntimeException("Unreachable"); } - return BrowserHistoryHelper.getInstance().saveResponseAndRedirect(session, authSession, response, true); + return BrowserHistoryHelper.getInstance().saveResponseAndRedirect(session, authSession, response, true, request); } } diff --git a/services/src/main/java/org/keycloak/services/resources/LoginActionsServiceChecks.java b/services/src/main/java/org/keycloak/services/resources/LoginActionsServiceChecks.java index 9edc513b44..e330d29b53 100644 --- a/services/src/main/java/org/keycloak/services/resources/LoginActionsServiceChecks.java +++ b/services/src/main/java/org/keycloak/services/resources/LoginActionsServiceChecks.java @@ -18,7 +18,6 @@ package org.keycloak.services.resources; import org.keycloak.TokenVerifier.Predicate; import org.keycloak.authentication.AuthenticationProcessor; -import org.keycloak.authentication.actiontoken.DefaultActionToken; import org.keycloak.authentication.ExplainedVerificationException; import org.keycloak.authentication.actiontoken.ActionTokenContext; import org.keycloak.authentication.actiontoken.ExplainedTokenVerificationException; @@ -152,7 +151,7 @@ public class LoginActionsServiceChecks { * Verifies whether the user given by ID both exists in the current realm. If yes, * it optionally also injects the user using the given function (e.g. into session context). */ - public static void checkIsUserValid(T token, ActionTokenContext context) throws VerificationException { + public static void checkIsUserValid(T token, ActionTokenContext context) throws VerificationException { try { checkIsUserValid(context.getSession(), context.getRealm(), token.getUserId(), context.getAuthenticationSession()::setAuthenticatedUser); } catch (ExplainedVerificationException ex) { @@ -178,7 +177,7 @@ public class LoginActionsServiceChecks { * Verifies whether the client denoted by client ID in token's {@code iss} ({@code issuedFor}) * field both exists and is enabled. */ - public static void checkIsClientValid(T token, ActionTokenContext context) throws VerificationException { + public static void checkIsClientValid(T token, ActionTokenContext context) throws VerificationException { String clientId = token.getIssuedFor(); AuthenticationSessionModel authSession = context.getAuthenticationSession(); ClientModel client = authSession == null ? null : authSession.getClient(); @@ -297,8 +296,9 @@ public class LoginActionsServiceChecks { return true; } - public static void checkTokenWasNotUsedYet(T token, ActionTokenContext context) throws VerificationException { + public static void checkTokenWasNotUsedYet(T token, ActionTokenContext context) throws VerificationException { ActionTokenStoreProvider actionTokenStore = context.getSession().getProvider(ActionTokenStoreProvider.class); + if (actionTokenStore.get(token) != null) { throw new ExplainedTokenVerificationException(token, Errors.EXPIRED_CODE, Messages.EXPIRED_ACTION); } diff --git a/services/src/main/java/org/keycloak/services/resources/RealmsResource.java b/services/src/main/java/org/keycloak/services/resources/RealmsResource.java index bb8de2d918..bc3f8dc19a 100755 --- a/services/src/main/java/org/keycloak/services/resources/RealmsResource.java +++ b/services/src/main/java/org/keycloak/services/resources/RealmsResource.java @@ -260,8 +260,7 @@ public class RealmsResource { WellKnownProvider wellKnown = session.getProvider(WellKnownProvider.class, providerName); if (wellKnown != null) { - ResponseBuilder responseBuilder = Response.ok(wellKnown.getConfig()) - .cacheControl(CacheControlUtil.getDefaultCacheControl()); + ResponseBuilder responseBuilder = Response.ok(wellKnown.getConfig()).cacheControl(CacheControlUtil.noCache()); return Cors.add(request, responseBuilder).allowedOrigins("*").auth().build(); } diff --git a/services/src/main/java/org/keycloak/services/resources/SessionCodeChecks.java b/services/src/main/java/org/keycloak/services/resources/SessionCodeChecks.java index 0f3ebbe158..b5011fb2cc 100644 --- a/services/src/main/java/org/keycloak/services/resources/SessionCodeChecks.java +++ b/services/src/main/java/org/keycloak/services/resources/SessionCodeChecks.java @@ -24,6 +24,7 @@ import javax.ws.rs.core.UriBuilder; import javax.ws.rs.core.UriInfo; import org.jboss.logging.Logger; +import org.jboss.resteasy.spi.HttpRequest; import org.keycloak.authentication.AuthenticationProcessor; import org.keycloak.common.ClientConnection; import org.keycloak.common.util.ObjectUtil; @@ -59,6 +60,7 @@ public class SessionCodeChecks { private final RealmModel realm; private final UriInfo uriInfo; + private final HttpRequest request; private final ClientConnection clientConnection; private final KeycloakSession session; private final EventBuilder event; @@ -69,9 +71,10 @@ public class SessionCodeChecks { private final String flowPath; - public SessionCodeChecks(RealmModel realm, UriInfo uriInfo, ClientConnection clientConnection, KeycloakSession session, EventBuilder event, String code, String execution, String clientId, String flowPath) { + public SessionCodeChecks(RealmModel realm, UriInfo uriInfo, HttpRequest request, ClientConnection clientConnection, KeycloakSession session, EventBuilder event, String code, String execution, String clientId, String flowPath) { this.realm = realm; this.uriInfo = uriInfo; + this.request = request; this.clientConnection = clientConnection; this.session = session; this.event = event; @@ -220,10 +223,17 @@ public class SessionCodeChecks { } } - if (ObjectUtil.isEqualOrBothNull(execution, lastExecFromSession)) { + if (execution == null || execution.equals(lastExecFromSession)) { // Allow refresh of previous page clientCode = new ClientSessionCode<>(session, realm, authSession); actionRequest = false; + + // Allow refresh, but rewrite browser history + if (execution == null && lastExecFromSession != null) { + logger.debugf("Parameter 'execution' is not in the request, but flow wasn't changed. Will update browser history"); + request.setAttribute(BrowserHistoryHelper.SHOULD_UPDATE_BROWSER_HISTORY, true); + } + return true; } else { response = showPageExpired(authSession); 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 e7d611eaeb..02063fa3e8 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 @@ -562,9 +562,9 @@ public class ClientResource { @NoCache public ManagementPermissionReference setManagementPermissionsEnabled(ManagementPermissionReference ref) { auth.clients().requireManage(client); - if (ref.isEnabled()) { - AdminPermissionManagement permissions = AdminPermissions.management(session, realm); - permissions.clients().setPermissionsEnabled(client, ref.isEnabled()); + AdminPermissionManagement permissions = AdminPermissions.management(session, realm); + permissions.clients().setPermissionsEnabled(client, ref.isEnabled()); + if (ref.isEnabled()) { return toMgmtRef(client, permissions); } else { return new ManagementPermissionReference(); 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 decb4da636..c0ea7df937 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 @@ -33,6 +33,7 @@ import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.models.utils.ModelToRepresentation; import org.keycloak.representations.idm.ClientRepresentation; +import org.keycloak.representations.idm.authorization.ResourceServerRepresentation; import org.keycloak.services.ErrorResponse; import org.keycloak.services.ErrorResponseException; import org.keycloak.services.ForbiddenException; @@ -188,7 +189,15 @@ public class ClientsResource { if (Profile.isFeatureEnabled(Profile.Feature.AUTHORIZATION)) { if (TRUE.equals(rep.getAuthorizationServicesEnabled())) { - getAuthorizationService(clientModel).enable(true); + AuthorizationService authorizationService = getAuthorizationService(clientModel); + + authorizationService.enable(true); + + ResourceServerRepresentation authorizationSettings = rep.getAuthorizationSettings(); + + if (authorizationSettings != null) { + authorizationService.resourceServer().importSettings(uriInfo, authorizationSettings); + } } } diff --git a/services/src/main/java/org/keycloak/services/resources/admin/GroupResource.java b/services/src/main/java/org/keycloak/services/resources/admin/GroupResource.java index 3de46b0e29..0c0ed89237 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/GroupResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/GroupResource.java @@ -263,9 +263,9 @@ public class GroupResource { @NoCache public ManagementPermissionReference setManagementPermissionsEnabled(ManagementPermissionReference ref) { auth.groups().requireManage(group); + AdminPermissionManagement permissions = AdminPermissions.management(session, realm); + permissions.groups().setPermissionsEnabled(group, ref.isEnabled()); if (ref.isEnabled()) { - AdminPermissionManagement permissions = AdminPermissions.management(session, realm); - permissions.groups().setPermissionsEnabled(group, ref.isEnabled()); return toMgmtRef(group, permissions); } else { return new ManagementPermissionReference(); diff --git a/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java b/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java index 28392f7f87..ebc89bea62 100644 --- a/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java @@ -16,6 +16,7 @@ */ package org.keycloak.services.resources.admin; +import com.fasterxml.jackson.core.type.TypeReference; import org.jboss.logging.Logger; import org.jboss.resteasy.annotations.cache.NoCache; import org.jboss.resteasy.spi.BadRequestException; @@ -29,6 +30,7 @@ import org.keycloak.services.resources.admin.permissions.AdminPermissions; import org.keycloak.common.ClientConnection; import org.keycloak.common.VerificationException; import org.keycloak.common.util.PemUtils; +import org.keycloak.email.EmailTemplateProvider; import org.keycloak.events.Event; import org.keycloak.events.EventQuery; import org.keycloak.events.EventStoreProvider; @@ -50,6 +52,7 @@ import org.keycloak.models.LDAPConstants; import org.keycloak.models.ModelDuplicateException; import org.keycloak.models.ModelException; import org.keycloak.models.RealmModel; +import org.keycloak.models.UserModel; import org.keycloak.models.UserSessionModel; import org.keycloak.models.cache.CacheRealmProvider; import org.keycloak.models.cache.UserCache; @@ -102,9 +105,9 @@ import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; -import java.util.regex.PatternSyntaxException; import static org.keycloak.models.utils.StripSecretsUtils.stripForExport; +import static org.keycloak.util.JsonSerialization.readValue; /** * Base resource class for the admin REST api of one realm @@ -811,6 +814,35 @@ public class RealmAdminResource { return result ? Response.noContent().build() : ErrorResponse.error("LDAP test error", Response.Status.BAD_REQUEST); } + /** + * Test SMTP connection with current logged in user + * + * @param config SMTP server configuration + * @return + * @throws Exception + */ + @Path("testSMTPConnection/{config}") + @POST + @NoCache + public Response testSMTPConnection(final @PathParam("config") String config) throws Exception { + Map settings = readValue(config, new TypeReference>() { + }); + + try { + UserModel user = auth.adminAuth().getUser(); + if (user.getEmail() == null) { + return ErrorResponse.error("Logged in user does not have an e-mail.", Response.Status.INTERNAL_SERVER_ERROR); + } + session.getProvider(EmailTemplateProvider.class).sendSmtpTestEmail(settings, user); + } catch (Exception e) { + e.printStackTrace(); + logger.errorf("Failed to send email \n %s", e.getCause()); + return ErrorResponse.error("Failed to send email", Response.Status.INTERNAL_SERVER_ERROR); + } + + return Response.noContent().build(); + } + @Path("identity-provider") public IdentityProvidersResource getIdentityProviderResource() { return new IdentityProvidersResource(realm, session, this.auth, adminEvent); diff --git a/services/src/main/java/org/keycloak/services/resources/admin/RoleContainerResource.java b/services/src/main/java/org/keycloak/services/resources/admin/RoleContainerResource.java index 79bb6c8f99..7ad9d2233f 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/RoleContainerResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/RoleContainerResource.java @@ -364,9 +364,9 @@ public class RoleContainerResource extends RoleResource { throw new NotFoundException("Could not find role"); } + AdminPermissionManagement permissions = AdminPermissions.management(session, realm); + permissions.roles().setPermissionsEnabled(role, ref.isEnabled()); if (ref.isEnabled()) { - AdminPermissionManagement permissions = AdminPermissions.management(session, realm); - permissions.roles().setPermissionsEnabled(role, ref.isEnabled()); return RoleByIdResource.toMgmtRef(role, permissions); } else { return new ManagementPermissionReference(); diff --git a/services/src/main/java/org/keycloak/services/resources/admin/UserResource.java b/services/src/main/java/org/keycloak/services/resources/admin/UserResource.java index bf3b236492..fbd318ae74 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/UserResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/UserResource.java @@ -704,6 +704,7 @@ public class UserResource { String link = builder.build(realm.getName()).toString(); this.session.getProvider(EmailTemplateProvider.class) + .setAttribute(Constants.TEMPLATE_ATTR_REQUIRED_ACTIONS, token.getRequiredActions()) .setRealm(realm) .setUser(user) .sendExecuteActions(link, TimeUnit.SECONDS.toMinutes(lifespan)); diff --git a/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java b/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java index 6ed677f92b..c7b9945fc2 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java @@ -52,6 +52,7 @@ import org.keycloak.models.UserSessionModel; import org.keycloak.models.*; import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.models.utils.ModelToRepresentation; +import org.keycloak.models.utils.RepresentationToModel; import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.protocol.oidc.utils.RedirectUtils; import org.keycloak.provider.ProviderFactory; @@ -162,8 +163,9 @@ public class UsersResource { try { UserModel user = session.users().addUser(realm, rep.getUsername()); Set emptySet = Collections.emptySet(); - UserResource.updateUserFromRep(user, rep, emptySet, realm, session, false); + UserResource.updateUserFromRep(user, rep, emptySet, realm, session, false); + RepresentationToModel.createCredentials(rep, session, realm, user); adminEvent.operation(OperationType.CREATE).resourcePath(uriInfo, user.getId()).representation(rep).success(); if (session.getTransactionManager().isActive()) { diff --git a/services/src/main/java/org/keycloak/services/resources/admin/info/ServerInfoAdminResource.java b/services/src/main/java/org/keycloak/services/resources/admin/info/ServerInfoAdminResource.java index 6c1779471a..a391c1d445 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/info/ServerInfoAdminResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/info/ServerInfoAdminResource.java @@ -129,6 +129,7 @@ public class ServerInfoAdminResource { for (String name : providerIds) { ProviderRepresentation provider = new ProviderRepresentation(); ProviderFactory pi = session.getKeycloakSessionFactory().getProviderFactory(spi.getProviderClass(), name); + provider.setOrder(pi.order()); if (ServerInfoAwareProviderFactory.class.isAssignableFrom(pi.getClass())) { provider.setOperationalInfo(((ServerInfoAwareProviderFactory) pi).getOperationalInfo()); } diff --git a/services/src/main/java/org/keycloak/services/resources/admin/permissions/AdminPermissionManagement.java b/services/src/main/java/org/keycloak/services/resources/admin/permissions/AdminPermissionManagement.java index 2a94132993..d8eb94a4b4 100644 --- a/services/src/main/java/org/keycloak/services/resources/admin/permissions/AdminPermissionManagement.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/permissions/AdminPermissionManagement.java @@ -18,6 +18,7 @@ package org.keycloak.services.resources.admin.permissions; import org.keycloak.authorization.AuthorizationProvider; import org.keycloak.authorization.model.ResourceServer; +import org.keycloak.models.ClientModel; /** * @author Bill Burke @@ -26,6 +27,10 @@ import org.keycloak.authorization.model.ResourceServer; public interface AdminPermissionManagement { public static final String MANAGE_SCOPE = "manage"; public static final String VIEW_SCOPE = "view"; + public static final String EXCHANGE_FROM_SCOPE="exchange-from"; + public static final String EXCHANGE_TO_SCOPE="exchange-to"; + + ClientModel getRealmManagementClient(); AuthorizationProvider authz(); diff --git a/services/src/main/java/org/keycloak/services/resources/admin/permissions/AdminPermissions.java b/services/src/main/java/org/keycloak/services/resources/admin/permissions/AdminPermissions.java index f809e1dad7..705b258c4b 100644 --- a/services/src/main/java/org/keycloak/services/resources/admin/permissions/AdminPermissions.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/permissions/AdminPermissions.java @@ -46,6 +46,10 @@ public class AdminPermissions { return new MgmtPermissions(session, auth); } + public static RealmsPermissionEvaluator realms(KeycloakSession session, RealmModel adminsRealm, UserModel admin) { + return new MgmtPermissions(session, adminsRealm, admin); + } + public static AdminPermissionManagement management(KeycloakSession session, RealmModel realm) { return new MgmtPermissions(session, realm); } diff --git a/services/src/main/java/org/keycloak/services/resources/admin/permissions/ClientPermissionManagement.java b/services/src/main/java/org/keycloak/services/resources/admin/permissions/ClientPermissionManagement.java index 8a6b76dd8e..ccf9679609 100644 --- a/services/src/main/java/org/keycloak/services/resources/admin/permissions/ClientPermissionManagement.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/permissions/ClientPermissionManagement.java @@ -41,6 +41,14 @@ public interface ClientPermissionManagement { Map getPermissions(ClientModel client); + boolean canExchangeFrom(ClientModel authorizedClient, ClientModel from); + + boolean canExchangeTo(ClientModel authorizedClient, ClientModel to); + + Policy exchangeFromPermission(ClientModel client); + + Policy exchangeToPermission(ClientModel client); + Policy mapRolesPermission(ClientModel client); Policy mapRolesClientScopePermission(ClientModel client); diff --git a/services/src/main/java/org/keycloak/services/resources/admin/permissions/ClientPermissions.java b/services/src/main/java/org/keycloak/services/resources/admin/permissions/ClientPermissions.java index 2b1e2340c4..bbb7bf4d29 100644 --- a/services/src/main/java/org/keycloak/services/resources/admin/permissions/ClientPermissions.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/permissions/ClientPermissions.java @@ -18,23 +18,35 @@ package org.keycloak.services.resources.admin.permissions; import org.jboss.logging.Logger; import org.keycloak.authorization.AuthorizationProvider; +import org.keycloak.authorization.attribute.Attributes; +import org.keycloak.authorization.common.ClientModelIdentity; +import org.keycloak.authorization.common.DefaultEvaluationContext; +import org.keycloak.authorization.identity.Identity; import org.keycloak.authorization.model.Policy; import org.keycloak.authorization.model.Resource; import org.keycloak.authorization.model.ResourceServer; import org.keycloak.authorization.model.Scope; +import org.keycloak.authorization.policy.evaluation.EvaluationContext; import org.keycloak.models.AdminRoles; import org.keycloak.models.ClientModel; import org.keycloak.models.ClientTemplateModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.RoleModel; +import org.keycloak.representations.AccessToken; import org.keycloak.services.ForbiddenException; +import java.util.Arrays; +import java.util.Collection; import java.util.HashMap; import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.Map; import java.util.Set; +import static org.keycloak.services.resources.admin.permissions.AdminPermissionManagement.EXCHANGE_FROM_SCOPE; +import static org.keycloak.services.resources.admin.permissions.AdminPermissionManagement.EXCHANGE_TO_SCOPE; + /** * Manages default policies for all users. * @@ -79,6 +91,14 @@ class ClientPermissions implements ClientPermissionEvaluator, ClientPermissionMa return MAP_ROLES_COMPOSITE_SCOPE + ".permission.client." + client.getId(); } + private String getExchangeToPermissionName(ClientModel client) { + return EXCHANGE_TO_SCOPE + ".permission.client." + client.getId(); + } + + private String getExchangeFromPermissionName(ClientModel client) { + return EXCHANGE_FROM_SCOPE + ".permission.client." + client.getId(); + } + private void initialize(ClientModel client) { ResourceServer server = root.findOrCreateResourceServer(client); Scope manageScope = manageScope(server); @@ -93,18 +113,11 @@ class ClientPermissions implements ClientPermissionEvaluator, ClientPermissionMa if (mapRoleScope == null) { mapRoleScope = authz.getStoreFactory().getScopeStore().create(MAP_ROLES_SCOPE, server); } - Scope mapRoleClientScope = authz.getStoreFactory().getScopeStore().findByName(MAP_ROLES_CLIENT_SCOPE, server.getId()); - if (mapRoleClientScope == null) { - mapRoleClientScope = authz.getStoreFactory().getScopeStore().create(MAP_ROLES_CLIENT_SCOPE, server); - } - Scope mapRoleCompositeScope = authz.getStoreFactory().getScopeStore().findByName(MAP_ROLES_COMPOSITE_SCOPE, server.getId()); - if (mapRoleCompositeScope == null) { - mapRoleCompositeScope = authz.getStoreFactory().getScopeStore().create(MAP_ROLES_COMPOSITE_SCOPE, server); - } - Scope configureScope = authz.getStoreFactory().getScopeStore().findByName(CONFIGURE_SCOPE, server.getId()); - if (configureScope == null) { - configureScope = authz.getStoreFactory().getScopeStore().create(CONFIGURE_SCOPE, server); - } + Scope mapRoleClientScope = root.initializeScope(MAP_ROLES_CLIENT_SCOPE, server); + Scope mapRoleCompositeScope = root.initializeScope(MAP_ROLES_COMPOSITE_SCOPE, server); + Scope configureScope = root.initializeScope(CONFIGURE_SCOPE, server); + Scope exchangeFromScope = root.initializeScope(EXCHANGE_FROM_SCOPE, server); + Scope exchangeToScope = root.initializeScope(EXCHANGE_TO_SCOPE, server); String resourceName = getResourceName(client); Resource resource = authz.getStoreFactory().getResourceStore().findByName(resourceName, server.getId()); @@ -118,6 +131,8 @@ class ClientPermissions implements ClientPermissionEvaluator, ClientPermissionMa scopeset.add(mapRoleScope); scopeset.add(mapRoleClientScope); scopeset.add(mapRoleCompositeScope); + scopeset.add(exchangeFromScope); + scopeset.add(exchangeToScope); resource.updateScopes(scopeset); } String managePermissionName = getManagePermissionName(client); @@ -150,6 +165,16 @@ class ClientPermissions implements ClientPermissionEvaluator, ClientPermissionMa if (mapRoleCompositePermission == null) { Helper.addEmptyScopePermission(authz, server, mapRoleCompositePermissionName, resource, mapRoleCompositeScope); } + String exchangeToPermissionName = getExchangeToPermissionName(client); + Policy exchangeToPermission = authz.getStoreFactory().getPolicyStore().findByName(exchangeToPermissionName, server.getId()); + if (exchangeToPermission == null) { + Helper.addEmptyScopePermission(authz, server, exchangeToPermissionName, resource, exchangeToScope); + } + String exchangeFromPermissionName = getExchangeFromPermissionName(client); + Policy exchangeFromPermission = authz.getStoreFactory().getPolicyStore().findByName(exchangeFromPermissionName, server.getId()); + if (exchangeFromPermission == null) { + Helper.addEmptyScopePermission(authz, server, exchangeFromPermissionName, resource, exchangeFromScope); + } } private void deletePolicy(String name, ResourceServer server) { @@ -169,6 +194,8 @@ class ClientPermissions implements ClientPermissionEvaluator, ClientPermissionMa deletePolicy(getMapRolesClientScopePermissionName(client), server); deletePolicy(getMapRolesCompositePermissionName(client), server); deletePolicy(getConfigurePermissionName(client), server); + deletePolicy(getExchangeToPermissionName(client), server); + deletePolicy(getExchangeFromPermissionName(client), server); Resource resource = authz.getStoreFactory().getResourceStore().findByName(getResourceName(client), server.getId());; if (resource != null) authz.getStoreFactory().getResourceStore().delete(resource.getId()); } @@ -196,6 +223,14 @@ class ClientPermissions implements ClientPermissionEvaluator, ClientPermissionMa return authz.getStoreFactory().getScopeStore().findByName(AdminPermissionManagement.MANAGE_SCOPE, server.getId()); } + private Scope exchangeFromScope(ResourceServer server) { + return authz.getStoreFactory().getScopeStore().findByName(EXCHANGE_FROM_SCOPE, server.getId()); + } + + private Scope exchangeToScope(ResourceServer server) { + return authz.getStoreFactory().getScopeStore().findByName(EXCHANGE_TO_SCOPE, server.getId()); + } + private Scope configureScope(ResourceServer server) { return authz.getStoreFactory().getScopeStore().findByName(CONFIGURE_SCOPE, server.getId()); } @@ -271,16 +306,119 @@ class ClientPermissions implements ClientPermissionEvaluator, ClientPermissionMa @Override public Map getPermissions(ClientModel client) { - Map scopes = new HashMap<>(); - scopes.put(MAP_ROLES_SCOPE, mapRolesPermission(client).getId()); - scopes.put(MAP_ROLES_CLIENT_SCOPE, mapRolesClientScopePermission(client).getId()); - scopes.put(MAP_ROLES_COMPOSITE_SCOPE, mapRolesCompositePermission(client).getId()); + initialize(client); + Map scopes = new LinkedHashMap<>(); scopes.put(AdminPermissionManagement.VIEW_SCOPE, viewPermission(client).getId()); scopes.put(AdminPermissionManagement.MANAGE_SCOPE, managePermission(client).getId()); scopes.put(CONFIGURE_SCOPE, configurePermission(client).getId()); + scopes.put(MAP_ROLES_SCOPE, mapRolesPermission(client).getId()); + scopes.put(MAP_ROLES_CLIENT_SCOPE, mapRolesClientScopePermission(client).getId()); + scopes.put(MAP_ROLES_COMPOSITE_SCOPE, mapRolesCompositePermission(client).getId()); + scopes.put(EXCHANGE_FROM_SCOPE, exchangeFromPermission(client).getId()); + scopes.put(EXCHANGE_TO_SCOPE, exchangeToPermission(client).getId()); return scopes; } + @Override + public boolean canExchangeFrom(ClientModel authorizedClient, ClientModel from) { + if (!authorizedClient.equals(from)) { + ResourceServer server = resourceServer(from); + if (server == null) { + logger.debug("No resource server set up for target client"); + return false; + } + + Resource resource = authz.getStoreFactory().getResourceStore().findByName(getResourceName(from), server.getId()); + if (resource == null) { + logger.debug("No resource object set up for target client"); + return false; + } + + Policy policy = authz.getStoreFactory().getPolicyStore().findByName(getExchangeFromPermissionName(from), server.getId()); + if (policy == null) { + logger.debug("No permission object set up for target client"); + return false; + } + + Set associatedPolicies = policy.getAssociatedPolicies(); + // if no policies attached to permission then just do default behavior + if (associatedPolicies == null || associatedPolicies.isEmpty()) { + logger.debug("No policies set up for permission on target client"); + return false; + } + + Scope scope = exchangeFromScope(server); + if (scope == null) { + logger.debug(EXCHANGE_FROM_SCOPE + " not initialized"); + return false; + } + ClientModelIdentity identity = new ClientModelIdentity(session, authorizedClient); + EvaluationContext context = new DefaultEvaluationContext(identity, session) { + @Override + public Map> getBaseAttributes() { + Map> attributes = super.getBaseAttributes(); + attributes.put("kc.client.id", Arrays.asList(authorizedClient.getClientId())); + return attributes; + } + + }; + return root.evaluatePermission(resource, scope, server, context); + } + return true; + } + + @Override + public boolean canExchangeTo(ClientModel authorizedClient, ClientModel to) { + + if (!authorizedClient.equals(to)) { + ResourceServer server = resourceServer(to); + if (server == null) { + logger.debug("No resource server set up for target client"); + return false; + } + + Resource resource = authz.getStoreFactory().getResourceStore().findByName(getResourceName(to), server.getId()); + if (resource == null) { + logger.debug("No resource object set up for target client"); + return false; + } + + Policy policy = authz.getStoreFactory().getPolicyStore().findByName(getExchangeToPermissionName(to), server.getId()); + if (policy == null) { + logger.debug("No permission object set up for target client"); + return false; + } + + Set associatedPolicies = policy.getAssociatedPolicies(); + // if no policies attached to permission then just do default behavior + if (associatedPolicies == null || associatedPolicies.isEmpty()) { + logger.debug("No policies set up for permission on target client"); + return false; + } + + Scope scope = exchangeToScope(server); + if (scope == null) { + logger.debug(EXCHANGE_TO_SCOPE + " not initialized"); + return false; + } + ClientModelIdentity identity = new ClientModelIdentity(session, authorizedClient); + EvaluationContext context = new DefaultEvaluationContext(identity, session) { + @Override + public Map> getBaseAttributes() { + Map> attributes = super.getBaseAttributes(); + attributes.put("kc.client.id", Arrays.asList(authorizedClient.getClientId())); + return attributes; + } + + }; + return root.evaluatePermission(resource, scope, server, context); + } + return true; + } + + + + @Override public boolean canManage(ClientModel client) { @@ -463,6 +601,20 @@ class ClientPermissions implements ClientPermissionEvaluator, ClientPermissionMa return root.evaluatePermission(resource, scope, server); } + @Override + public Policy exchangeFromPermission(ClientModel client) { + ResourceServer server = resourceServer(client); + if (server == null) return null; + return authz.getStoreFactory().getPolicyStore().findByName(getExchangeFromPermissionName(client), server.getId()); + } + + @Override + public Policy exchangeToPermission(ClientModel client) { + ResourceServer server = resourceServer(client); + if (server == null) return null; + return authz.getStoreFactory().getPolicyStore().findByName(getExchangeToPermissionName(client), server.getId()); + } + @Override public Policy mapRolesPermission(ClientModel client) { ResourceServer server = resourceServer(client); diff --git a/services/src/main/java/org/keycloak/services/resources/admin/permissions/GroupPermissions.java b/services/src/main/java/org/keycloak/services/resources/admin/permissions/GroupPermissions.java index a7e9f374be..b20d4626df 100644 --- a/services/src/main/java/org/keycloak/services/resources/admin/permissions/GroupPermissions.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/permissions/GroupPermissions.java @@ -31,6 +31,7 @@ import org.keycloak.services.ForbiddenException; import java.util.HashMap; import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.Map; import java.util.Set; @@ -184,9 +185,13 @@ class GroupPermissions implements GroupPermissionEvaluator, GroupPermissionManag authz.getStoreFactory().getPolicyStore().delete(manageMembersPermission.getId()); } Policy viewMembersPermission = viewMembersPermission(group); - if (manageMembersPermission == null) { + if (viewMembersPermission == null) { authz.getStoreFactory().getPolicyStore().delete(viewMembersPermission.getId()); } + Policy manageMembershipPermission = manageMembershipPermission(group); + if (manageMembershipPermission != null) { + authz.getStoreFactory().getPolicyStore().delete(manageMembershipPermission.getId()); + } Resource resource = groupResource(group); if (resource != null) authz.getStoreFactory().getResourceStore().delete(resource.getId()); } @@ -242,11 +247,12 @@ class GroupPermissions implements GroupPermissionEvaluator, GroupPermissionManag @Override public Map getPermissions(GroupModel group) { - Map scopes = new HashMap<>(); + initialize(group); + Map scopes = new LinkedHashMap<>(); scopes.put(AdminPermissionManagement.VIEW_SCOPE, viewPermission(group).getId()); scopes.put(AdminPermissionManagement.MANAGE_SCOPE, managePermission(group).getId()); - scopes.put(MANAGE_MEMBERS_SCOPE, manageMembersPermission(group).getId()); scopes.put(VIEW_MEMBERS_SCOPE, viewMembersPermission(group).getId()); + scopes.put(MANAGE_MEMBERS_SCOPE, manageMembersPermission(group).getId()); scopes.put(MANAGE_MEMBERSHIP_SCOPE, manageMembershipPermission(group).getId()); return scopes; } diff --git a/services/src/main/java/org/keycloak/services/resources/admin/permissions/MgmtPermissions.java b/services/src/main/java/org/keycloak/services/resources/admin/permissions/MgmtPermissions.java index 2df495322b..fe4a11fe93 100644 --- a/services/src/main/java/org/keycloak/services/resources/admin/permissions/MgmtPermissions.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/permissions/MgmtPermissions.java @@ -107,6 +107,14 @@ class MgmtPermissions implements AdminPermissionEvaluator, AdminPermissionManage this.identity = new KeycloakIdentity(auth.getToken(), session); } } + + MgmtPermissions(KeycloakSession session, RealmModel adminsRealm, UserModel admin) { + this.session = session; + this.admin = admin; + this.adminsRealm = adminsRealm; + this.identity = new UserModelIdentity(adminsRealm, admin); + } + MgmtPermissions(KeycloakSession session, RealmModel realm, RealmModel adminsRealm, UserModel admin) { this(session, realm); this.admin = admin; @@ -114,6 +122,7 @@ class MgmtPermissions implements AdminPermissionEvaluator, AdminPermissionManage this.identity = new UserModelIdentity(realm, admin); } + @Override public ClientModel getRealmManagementClient() { ClientModel client = null; if (realm.getName().equals(Config.getAdminRealm())) { @@ -268,6 +277,14 @@ class MgmtPermissions implements AdminPermissionEvaluator, AdminPermissionManage return scope; } + public Scope initializeScope(String name, ResourceServer server) { + Scope scope = authz.getStoreFactory().getScopeStore().findByName(name, server.getId()); + if (scope == null) { + scope = authz.getStoreFactory().getScopeStore().create(name, server); + } + return scope; + } + public Scope realmManageScope() { @@ -298,10 +315,14 @@ class MgmtPermissions implements AdminPermissionEvaluator, AdminPermissionManage } public boolean evaluatePermission(Resource resource, Scope scope, ResourceServer resourceServer, Identity identity) { + EvaluationContext context = new DefaultEvaluationContext(identity, session); + return evaluatePermission(resource, scope, resourceServer, context); + } + + public boolean evaluatePermission(Resource resource, Scope scope, ResourceServer resourceServer, EvaluationContext context) { RealmModel oldRealm = session.getContext().getRealm(); try { session.getContext().setRealm(realm); - EvaluationContext context = new DefaultEvaluationContext(identity, session); DecisionResult decisionCollector = new DecisionResult(); List permissions = Permissions.permission(resourceServer, resource, scope); PermissionEvaluator from = authz.evaluators().from(permissions, context); diff --git a/services/src/main/java/org/keycloak/services/resources/admin/permissions/RolePermissions.java b/services/src/main/java/org/keycloak/services/resources/admin/permissions/RolePermissions.java index 091d7a58f7..0e12861929 100644 --- a/services/src/main/java/org/keycloak/services/resources/admin/permissions/RolePermissions.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/permissions/RolePermissions.java @@ -36,6 +36,7 @@ import org.keycloak.services.ForbiddenException; import java.util.HashMap; import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.Map; import java.util.Set; @@ -87,7 +88,8 @@ class RolePermissions implements RolePermissionEvaluator, RolePermissionManageme @Override public Map getPermissions(RoleModel role) { - Map scopes = new HashMap<>(); + initialize(role); + Map scopes = new LinkedHashMap<>(); scopes.put(RolePermissionManagement.MAP_ROLE_SCOPE, mapRolePermission(role).getId()); scopes.put(RolePermissionManagement.MAP_ROLE_CLIENT_SCOPE_SCOPE, mapClientScopePermission(role).getId()); scopes.put(RolePermissionManagement.MAP_ROLE_COMPOSITE_SCOPE, mapCompositePermission(role).getId()); @@ -136,10 +138,13 @@ class RolePermissions implements RolePermissionEvaluator, RolePermissionManageme if (root.admin().hasRole(role)) return true; ClientModel adminClient = root.getRealmManagementClient(); + // is this an admin role in 'realm-management' client of the realm we are managing? if (adminClient.equals(role.getContainer())) { // if this is realm admin role, then check to see if admin has similar permissions // we do this so that the authz service is invoked - if (role.getName().equals(AdminRoles.MANAGE_CLIENTS)) { + if (role.getName().equals(AdminRoles.MANAGE_CLIENTS) + || role.getName().equals(AdminRoles.CREATE_CLIENT) + ) { if (!root.clients().canManage()) { return adminConflictMessage(role); } else { @@ -151,6 +156,9 @@ class RolePermissions implements RolePermissionEvaluator, RolePermissionManageme } else { return true; } + + } else if (role.getName().equals(AdminRoles.QUERY_REALMS)) { + return true; } else if (role.getName().equals(AdminRoles.QUERY_CLIENTS)) { return true; } else if (role.getName().equals(AdminRoles.QUERY_USERS)) { diff --git a/services/src/main/java/org/keycloak/services/resources/admin/permissions/UserPermissions.java b/services/src/main/java/org/keycloak/services/resources/admin/permissions/UserPermissions.java index 149e52678b..3ac26ed5fa 100644 --- a/services/src/main/java/org/keycloak/services/resources/admin/permissions/UserPermissions.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/permissions/UserPermissions.java @@ -34,6 +34,7 @@ import org.keycloak.services.ForbiddenException; import java.util.HashMap; import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.Map; import java.util.Set; @@ -121,9 +122,10 @@ class UserPermissions implements UserPermissionEvaluator, UserPermissionManageme @Override public Map getPermissions() { - Map scopes = new HashMap<>(); - scopes.put(AdminPermissionManagement.MANAGE_SCOPE, managePermission().getId()); + initialize(); + Map scopes = new LinkedHashMap<>(); scopes.put(AdminPermissionManagement.VIEW_SCOPE, viewPermission().getId()); + scopes.put(AdminPermissionManagement.MANAGE_SCOPE, managePermission().getId()); scopes.put(MAP_ROLES_SCOPE, mapRolesPermission().getId()); scopes.put(MANAGE_GROUP_MEMBERSHIP_SCOPE, manageGroupMembershipPermission().getId()); scopes.put(IMPERSONATE_SCOPE, adminImpersonatingPermission().getId()); @@ -157,32 +159,32 @@ class UserPermissions implements UserPermissionEvaluator, UserPermissionManageme ResourceServer server = root.realmResourceServer(); if (server == null) return; Policy policy = managePermission(); - if (policy == null) { + if (policy != null) { authz.getStoreFactory().getPolicyStore().delete(policy.getId()); } policy = viewPermission(); - if (policy == null) { + if (policy != null) { authz.getStoreFactory().getPolicyStore().delete(policy.getId()); } policy = mapRolesPermission(); - if (policy == null) { + if (policy != null) { authz.getStoreFactory().getPolicyStore().delete(policy.getId()); } policy = manageGroupMembershipPermission(); - if (policy == null) { + if (policy != null) { authz.getStoreFactory().getPolicyStore().delete(policy.getId()); } policy = adminImpersonatingPermission(); - if (policy == null) { + if (policy != null) { authz.getStoreFactory().getPolicyStore().delete(policy.getId()); } policy = userImpersonatedPermission(); - if (policy == null) { + if (policy != null) { authz.getStoreFactory().getPolicyStore().delete(policy.getId()); } diff --git a/services/src/main/java/org/keycloak/services/util/BrowserHistoryHelper.java b/services/src/main/java/org/keycloak/services/util/BrowserHistoryHelper.java index ef34b16116..6832f10985 100644 --- a/services/src/main/java/org/keycloak/services/util/BrowserHistoryHelper.java +++ b/services/src/main/java/org/keycloak/services/util/BrowserHistoryHelper.java @@ -24,6 +24,7 @@ import java.util.regex.Pattern; import javax.ws.rs.core.Response; import org.jboss.logging.Logger; +import org.jboss.resteasy.spi.HttpRequest; import org.keycloak.models.KeycloakSession; import org.keycloak.sessions.AuthenticationSessionModel; import org.keycloak.theme.BrowserSecurityHeaderSetup; @@ -44,13 +45,27 @@ import org.keycloak.utils.MediaType; */ public abstract class BrowserHistoryHelper { + // Request attribute, which specifies if flow was changed in this request (eg. click "register" from the login screen) + public static final String SHOULD_UPDATE_BROWSER_HISTORY = "SHOULD_UPDATE_BROWSER_HISTORY"; + protected static final Logger logger = Logger.getLogger(BrowserHistoryHelper.class); - public abstract Response saveResponseAndRedirect(KeycloakSession session, AuthenticationSessionModel authSession, Response response, boolean actionRequest); + public abstract Response saveResponseAndRedirect(KeycloakSession session, AuthenticationSessionModel authSession, Response response, boolean actionRequest, HttpRequest httpRequest); public abstract Response loadSavedResponse(KeycloakSession session, AuthenticationSessionModel authSession); + protected boolean shouldReplaceBrowserHistory(boolean actionRequest, HttpRequest httpRequest) { + if (actionRequest) { + return true; + } + + Boolean flowChanged = (Boolean) httpRequest.getAttribute(SHOULD_UPDATE_BROWSER_HISTORY); + return (flowChanged != null && flowChanged); + } + + + // Always rely on javascript for now public static BrowserHistoryHelper getInstance() { return new JavascriptHistoryReplace(); @@ -66,8 +81,8 @@ public abstract class BrowserHistoryHelper { private static final Pattern HEAD_END_PATTERN = Pattern.compile(""); @Override - public Response saveResponseAndRedirect(KeycloakSession session, AuthenticationSessionModel authSession, Response response, boolean actionRequest) { - if (!actionRequest) { + public Response saveResponseAndRedirect(KeycloakSession session, AuthenticationSessionModel authSession, Response response, boolean actionRequest, HttpRequest httpRequest) { + if (!shouldReplaceBrowserHistory(actionRequest, httpRequest)) { return response; } @@ -129,8 +144,8 @@ public abstract class BrowserHistoryHelper { private static final String CACHED_RESPONSE = "cached.response"; @Override - public Response saveResponseAndRedirect(KeycloakSession session, AuthenticationSessionModel authSession, Response response, boolean actionRequest) { - if (!actionRequest) { + public Response saveResponseAndRedirect(KeycloakSession session, AuthenticationSessionModel authSession, Response response, boolean actionRequest, HttpRequest httpRequest) { + if (!shouldReplaceBrowserHistory(actionRequest, httpRequest)) { return response; } @@ -179,7 +194,7 @@ public abstract class BrowserHistoryHelper { private static class NoOpHelper extends BrowserHistoryHelper { @Override - public Response saveResponseAndRedirect(KeycloakSession session, AuthenticationSessionModel authSession, Response response, boolean actionRequest) { + public Response saveResponseAndRedirect(KeycloakSession session, AuthenticationSessionModel authSession, Response response, boolean actionRequest, HttpRequest httpRequest) { return response; } diff --git a/services/src/main/java/org/keycloak/social/bitbucket/BitbucketIdentityProvider.java b/services/src/main/java/org/keycloak/social/bitbucket/BitbucketIdentityProvider.java new file mode 100755 index 0000000000..c7583b738e --- /dev/null +++ b/services/src/main/java/org/keycloak/social/bitbucket/BitbucketIdentityProvider.java @@ -0,0 +1,98 @@ +/* + * 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.social.bitbucket; + +import com.fasterxml.jackson.databind.JsonNode; +import org.keycloak.broker.oidc.AbstractOAuth2IdentityProvider; +import org.keycloak.broker.oidc.OAuth2IdentityProviderConfig; +import org.keycloak.broker.oidc.mappers.AbstractJsonUserAttributeMapper; +import org.keycloak.broker.oidc.util.JsonSimpleHttp; +import org.keycloak.broker.provider.BrokeredIdentityContext; +import org.keycloak.broker.provider.IdentityBrokerException; +import org.keycloak.broker.provider.util.SimpleHttp; +import org.keycloak.broker.social.SocialIdentityProvider; +import org.keycloak.models.KeycloakSession; + +/** + * @author Stian Thorgersen + */ +public class BitbucketIdentityProvider extends AbstractOAuth2IdentityProvider implements SocialIdentityProvider { + + public static final String AUTH_URL = "https://bitbucket.org/site/oauth2/authorize"; + public static final String TOKEN_URL = "https://bitbucket.org/site/oauth2/access_token"; + public static final String USER_URL = "https://api.bitbucket.org/2.0/user"; + public static final String EMAIL_SCOPE = "email"; + public static final String ACCOUNT_SCOPE = "account"; + public static final String DEFAULT_SCOPE = ACCOUNT_SCOPE; + + public BitbucketIdentityProvider(KeycloakSession session, OAuth2IdentityProviderConfig config) { + super(session, config); + config.setAuthorizationUrl(AUTH_URL); + config.setTokenUrl(TOKEN_URL); + String defaultScope = config.getDefaultScope(); + + if (defaultScope == null || defaultScope.trim().equals("")) { + config.setDefaultScope(ACCOUNT_SCOPE); + } + } + + @Override + protected BrokeredIdentityContext doGetFederatedIdentity(String accessToken) { + try { + JsonNode profile = JsonSimpleHttp.asJson(SimpleHttp.doGet(USER_URL, session).header("Authorization", "Bearer " + accessToken)); + + String type = getJsonProperty(profile, "type"); + if (type == null) { + throw new IdentityBrokerException("Could not obtain account information from bitbucket."); + + } + if (type.equals("error")) { + JsonNode errorNode = profile.get("error"); + if (errorNode != null) { + String errorMsg = getJsonProperty(errorNode, "message"); + throw new IdentityBrokerException("Could not obtain account information from bitbucket. Error: " + errorMsg); + } else { + throw new IdentityBrokerException("Could not obtain account information from bitbucket."); + } + } + if (!type.equals("user")) { + logger.debug("Unknown object type: " + type); + throw new IdentityBrokerException("Could not obtain account information from bitbucket."); + + } + BrokeredIdentityContext user = new BrokeredIdentityContext(getJsonProperty(profile, "account_id")); + + String username = getJsonProperty(profile, "username"); + user.setUsername(username); + user.setIdpConfig(getConfig()); + user.setIdp(this); + + AbstractJsonUserAttributeMapper.storeUserProfileForMapper(user, profile, getConfig().getAlias()); + + return user; + } catch (Exception e) { + if (e instanceof IdentityBrokerException) throw (IdentityBrokerException)e; + throw new IdentityBrokerException("Could not obtain user profile from github.", e); + } + } + + @Override + protected String getDefaultScopes() { + return DEFAULT_SCOPE; + } +} diff --git a/services/src/main/java/org/keycloak/social/bitbucket/BitbucketIdentityProviderFactory.java b/services/src/main/java/org/keycloak/social/bitbucket/BitbucketIdentityProviderFactory.java new file mode 100755 index 0000000000..b736d74181 --- /dev/null +++ b/services/src/main/java/org/keycloak/social/bitbucket/BitbucketIdentityProviderFactory.java @@ -0,0 +1,46 @@ +/* + * Copyright 2016 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.keycloak.social.bitbucket; + +import org.keycloak.broker.oidc.OAuth2IdentityProviderConfig; +import org.keycloak.broker.provider.AbstractIdentityProviderFactory; +import org.keycloak.broker.social.SocialIdentityProviderFactory; +import org.keycloak.models.IdentityProviderModel; +import org.keycloak.models.KeycloakSession; + +/** + * @author Pedro Igor + */ +public class BitbucketIdentityProviderFactory extends AbstractIdentityProviderFactory implements SocialIdentityProviderFactory { + + public static final String PROVIDER_ID = "bitbucket"; + + @Override + public String getName() { + return "BitBucket"; + } + + @Override + public BitbucketIdentityProvider create(KeycloakSession session, IdentityProviderModel model) { + return new BitbucketIdentityProvider(session, new OAuth2IdentityProviderConfig(model)); + } + + @Override + public String getId() { + return PROVIDER_ID; + } +} diff --git a/services/src/main/java/org/keycloak/social/gitlab/GitLabIdentityProvider.java b/services/src/main/java/org/keycloak/social/gitlab/GitLabIdentityProvider.java new file mode 100755 index 0000000000..a57704ff69 --- /dev/null +++ b/services/src/main/java/org/keycloak/social/gitlab/GitLabIdentityProvider.java @@ -0,0 +1,109 @@ +/* + * 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.social.gitlab; + +import com.fasterxml.jackson.databind.JsonNode; +import org.keycloak.broker.oidc.AbstractOAuth2IdentityProvider; +import org.keycloak.broker.oidc.OAuth2IdentityProviderConfig; +import org.keycloak.broker.oidc.OIDCIdentityProvider; +import org.keycloak.broker.oidc.OIDCIdentityProviderConfig; +import org.keycloak.broker.oidc.mappers.AbstractJsonUserAttributeMapper; +import org.keycloak.broker.oidc.util.JsonSimpleHttp; +import org.keycloak.broker.provider.BrokeredIdentityContext; +import org.keycloak.broker.provider.IdentityBrokerException; +import org.keycloak.broker.provider.util.SimpleHttp; +import org.keycloak.broker.social.SocialIdentityProvider; +import org.keycloak.models.KeycloakSession; +import org.keycloak.representations.AccessTokenResponse; +import org.keycloak.representations.IDToken; +import org.keycloak.representations.JsonWebToken; + +import java.io.IOException; + +/** + * @author Stian Thorgersen + */ +public class GitLabIdentityProvider extends OIDCIdentityProvider implements SocialIdentityProvider { + + public static final String AUTH_URL = "https://gitlab.com/oauth/authorize"; + public static final String TOKEN_URL = "https://gitlab.com/oauth/token"; + public static final String USER_INFO = "https://gitlab.com/api/v4/user"; + public static final String API_SCOPE = "api"; + + public GitLabIdentityProvider(KeycloakSession session, OIDCIdentityProviderConfig config) { + super(session, config); + config.setAuthorizationUrl(AUTH_URL); + config.setTokenUrl(TOKEN_URL); + config.setUserInfoUrl(USER_INFO); + + String defaultScope = config.getDefaultScope(); + + if (defaultScope.equals(SCOPE_OPENID)) { + config.setDefaultScope((API_SCOPE + " " + defaultScope).trim()); + } + } + + protected BrokeredIdentityContext extractIdentity(AccessTokenResponse tokenResponse, String accessToken, JsonWebToken idToken) throws IOException { + String id = idToken.getSubject(); + BrokeredIdentityContext identity = new BrokeredIdentityContext(id); + String name = (String)idToken.getOtherClaims().get(IDToken.NAME); + String preferredUsername = (String)idToken.getOtherClaims().get(IDToken.NICKNAME); + String email = (String)idToken.getOtherClaims().get(IDToken.EMAIL); + + if (getConfig().getDefaultScope().contains(API_SCOPE)) { + String userInfoUrl = getUserInfoUrl(); + if (userInfoUrl != null && !userInfoUrl.isEmpty() && (id == null || name == null || preferredUsername == null || email == null)) { + SimpleHttp request = JsonSimpleHttp.doGet(userInfoUrl, session) + .header("Authorization", "Bearer " + accessToken); + JsonNode userInfo = JsonSimpleHttp.asJson(request); + + name = getJsonProperty(userInfo, "name"); + preferredUsername = getJsonProperty(userInfo, "username"); + email = getJsonProperty(userInfo, "email"); + AbstractJsonUserAttributeMapper.storeUserProfileForMapper(identity, userInfo, getConfig().getAlias()); + } + } + identity.getContextData().put(FEDERATED_ACCESS_TOKEN_RESPONSE, tokenResponse); + identity.getContextData().put(VALIDATED_ID_TOKEN, idToken); + processAccessTokenResponse(identity, tokenResponse); + + identity.setId(id); + identity.setName(name); + identity.setEmail(email); + + identity.setBrokerUserId(getConfig().getAlias() + "." + id); + if (tokenResponse.getSessionState() != null) { + identity.setBrokerSessionId(getConfig().getAlias() + "." + tokenResponse.getSessionState()); + } + + if (preferredUsername == null) { + preferredUsername = email; + } + + if (preferredUsername == null) { + preferredUsername = id; + } + + identity.setUsername(preferredUsername); + return identity; + } + + + + +} diff --git a/services/src/main/java/org/keycloak/social/gitlab/GitLabIdentityProviderFactory.java b/services/src/main/java/org/keycloak/social/gitlab/GitLabIdentityProviderFactory.java new file mode 100755 index 0000000000..35e7a5e034 --- /dev/null +++ b/services/src/main/java/org/keycloak/social/gitlab/GitLabIdentityProviderFactory.java @@ -0,0 +1,47 @@ +/* + * Copyright 2016 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.keycloak.social.gitlab; + +import org.keycloak.broker.oidc.OAuth2IdentityProviderConfig; +import org.keycloak.broker.oidc.OIDCIdentityProviderConfig; +import org.keycloak.broker.provider.AbstractIdentityProviderFactory; +import org.keycloak.broker.social.SocialIdentityProviderFactory; +import org.keycloak.models.IdentityProviderModel; +import org.keycloak.models.KeycloakSession; + +/** + * @author Pedro Igor + */ +public class GitLabIdentityProviderFactory extends AbstractIdentityProviderFactory implements SocialIdentityProviderFactory { + + public static final String PROVIDER_ID = "gitlab"; + + @Override + public String getName() { + return "GitLab"; + } + + @Override + public GitLabIdentityProvider create(KeycloakSession session, IdentityProviderModel model) { + return new GitLabIdentityProvider(session, new OIDCIdentityProviderConfig(model)); + } + + @Override + public String getId() { + return PROVIDER_ID; + } +} diff --git a/services/src/main/resources/DockerComposeYamlReadme.md b/services/src/main/resources/DockerComposeYamlReadme.md new file mode 100644 index 0000000000..84dff48460 --- /dev/null +++ b/services/src/main/resources/DockerComposeYamlReadme.md @@ -0,0 +1,23 @@ +# Docker Compose YAML Installation +----------------------------------- + +*NOTE:* This installation method is intended for development use only. Please don't ever let this anywhere near prod! + +## Keycloak Realm Assumptions: + - Client configuration has not changed since the installtion files were generated. If you change your client configuration, be sure to grab a re-generated installtion .zip from the 'Installation' tab. + - Keycloak server is started with the 'docker' feature enabled. I.E. -Dkeycloak.profile.feature.docker=enabled + +## Running the Installation: + - Spin up a fully functional docker registry with: + + docker-compose up + + - Now you can login against the registry and perform normal operations: + + docker login -u $username -p $password localhost:5000 + + docker pull centos:7 + docker tag centos:7 localhost:5000/centos:7 + docker push localhost:5000/centos:7 + + ** Remember that users for the `docker login` command must be configured and available in the keycloak realm that hosts the docker client. \ No newline at end of file diff --git a/services/src/main/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory b/services/src/main/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory index 208f16dade..2b11382be1 100755 --- a/services/src/main/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory +++ b/services/src/main/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory @@ -34,6 +34,7 @@ org.keycloak.authentication.authenticators.broker.IdpConfirmLinkAuthenticatorFac org.keycloak.authentication.authenticators.broker.IdpEmailVerificationAuthenticatorFactory org.keycloak.authentication.authenticators.broker.IdpUsernamePasswordFormFactory org.keycloak.authentication.authenticators.browser.ConditionalOtpFormAuthenticatorFactory -org.keycloak.protocol.saml.profile.ecp.authenticator.HttpBasicAuthenticator +org.keycloak.protocol.saml.profile.ecp.authenticator.HttpBasicAuthenticatorFactory org.keycloak.authentication.authenticators.x509.X509ClientCertificateAuthenticatorFactory org.keycloak.authentication.authenticators.x509.ValidateX509CertificateUsernameFactory +org.keycloak.protocol.docker.DockerAuthenticatorFactory diff --git a/services/src/main/resources/META-INF/services/org.keycloak.broker.social.SocialIdentityProviderFactory b/services/src/main/resources/META-INF/services/org.keycloak.broker.social.SocialIdentityProviderFactory index 00a5e5163d..f2861abcdf 100755 --- a/services/src/main/resources/META-INF/services/org.keycloak.broker.social.SocialIdentityProviderFactory +++ b/services/src/main/resources/META-INF/services/org.keycloak.broker.social.SocialIdentityProviderFactory @@ -23,3 +23,5 @@ org.keycloak.social.stackoverflow.StackoverflowIdentityProviderFactory org.keycloak.social.twitter.TwitterIdentityProviderFactory org.keycloak.social.microsoft.MicrosoftIdentityProviderFactory org.keycloak.social.openshift.OpenshiftV3IdentityProviderFactory +org.keycloak.social.gitlab.GitLabIdentityProviderFactory +org.keycloak.social.bitbucket.BitbucketIdentityProviderFactory \ No newline at end of file diff --git a/services/src/main/resources/META-INF/services/org.keycloak.protocol.ClientInstallationProvider b/services/src/main/resources/META-INF/services/org.keycloak.protocol.ClientInstallationProvider index a0d8052082..f38a5c2366 100755 --- a/services/src/main/resources/META-INF/services/org.keycloak.protocol.ClientInstallationProvider +++ b/services/src/main/resources/META-INF/services/org.keycloak.protocol.ClientInstallationProvider @@ -22,4 +22,6 @@ org.keycloak.protocol.saml.installation.SamlSPDescriptorClientInstallation org.keycloak.protocol.saml.installation.SamlIDPDescriptorClientInstallation org.keycloak.protocol.saml.installation.ModAuthMellonClientInstallation org.keycloak.protocol.saml.installation.KeycloakSamlSubsystemInstallation - +org.keycloak.protocol.docker.installation.DockerVariableOverrideInstallationProvider +org.keycloak.protocol.docker.installation.DockerRegistryConfigFileInstallationProvider +org.keycloak.protocol.docker.installation.DockerComposeYamlInstallationProvider diff --git a/services/src/main/resources/META-INF/services/org.keycloak.protocol.LoginProtocolFactory b/services/src/main/resources/META-INF/services/org.keycloak.protocol.LoginProtocolFactory index 38e1b5a918..e954f2ee7a 100755 --- a/services/src/main/resources/META-INF/services/org.keycloak.protocol.LoginProtocolFactory +++ b/services/src/main/resources/META-INF/services/org.keycloak.protocol.LoginProtocolFactory @@ -16,4 +16,5 @@ # org.keycloak.protocol.oidc.OIDCLoginProtocolFactory -org.keycloak.protocol.saml.SamlProtocolFactory \ No newline at end of file +org.keycloak.protocol.saml.SamlProtocolFactory +org.keycloak.protocol.docker.DockerAuthV2ProtocolFactory \ No newline at end of file diff --git a/services/src/main/resources/META-INF/services/org.keycloak.protocol.ProtocolMapper b/services/src/main/resources/META-INF/services/org.keycloak.protocol.ProtocolMapper index 04f090ecf4..95b79cfb01 100755 --- a/services/src/main/resources/META-INF/services/org.keycloak.protocol.ProtocolMapper +++ b/services/src/main/resources/META-INF/services/org.keycloak.protocol.ProtocolMapper @@ -35,4 +35,5 @@ org.keycloak.protocol.saml.mappers.GroupMembershipMapper org.keycloak.protocol.oidc.mappers.UserClientRoleMappingMapper org.keycloak.protocol.oidc.mappers.UserRealmRoleMappingMapper org.keycloak.protocol.oidc.mappers.SHA256PairwiseSubMapper +org.keycloak.protocol.docker.mapper.AllowAllDockerProtocolMapper diff --git a/services/src/main/resources/scripts/authenticator-template.js b/services/src/main/resources/scripts/authenticator-template.js index 514337d533..53f1c13745 100644 --- a/services/src/main/resources/scripts/authenticator-template.js +++ b/services/src/main/resources/scripts/authenticator-template.js @@ -15,7 +15,7 @@ AuthenticationFlowError = Java.type("org.keycloak.authentication.AuthenticationF * session - current KeycloakSession {@see org.keycloak.models.KeycloakSession} * httpRequest - current HttpRequest {@see org.jboss.resteasy.spi.HttpRequest} * script - current script {@see org.keycloak.models.ScriptModel} - * authenticationSession - current client session {@see org.keycloak.sessions.AuthenticationSessionModel} + * authenticationSession - current authentication session {@see org.keycloak.sessions.AuthenticationSessionModel} * LOG - current logger {@see org.jboss.logging.Logger} * * You one can extract current http request headers via: diff --git a/services/src/test/java/org/keycloak/procotol/docker/installation/DockerComposeYamlInstallationProviderTest.java b/services/src/test/java/org/keycloak/procotol/docker/installation/DockerComposeYamlInstallationProviderTest.java new file mode 100644 index 0000000000..a5f494c20c --- /dev/null +++ b/services/src/test/java/org/keycloak/procotol/docker/installation/DockerComposeYamlInstallationProviderTest.java @@ -0,0 +1,193 @@ +package org.keycloak.procotol.docker.installation; + +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.IOUtils; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Ignore; +import org.junit.Test; +import org.keycloak.common.util.CertificateUtils; +import org.keycloak.common.util.PemUtils; +import org.keycloak.protocol.docker.installation.DockerComposeYamlInstallationProvider; + +import javax.ws.rs.core.Response; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.SecureRandom; +import java.security.cert.Certificate; +import java.security.cert.X509Certificate; +import java.util.Optional; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; +import java.util.zip.ZipOutputStream; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.IsEqual.equalTo; +import static org.hamcrest.core.IsNull.notNullValue; +import static org.junit.Assert.fail; +import static org.keycloak.protocol.docker.installation.DockerComposeYamlInstallationProvider.ROOT_DIR; + +public class DockerComposeYamlInstallationProviderTest { + + DockerComposeYamlInstallationProvider installationProvider; + static Certificate certificate; + + @BeforeClass + public static void setUp_beforeClass() throws NoSuchAlgorithmException { + final KeyPairGenerator keyGen; + keyGen = KeyPairGenerator.getInstance("RSA"); + keyGen.initialize(2048, new SecureRandom()); + + final KeyPair keypair = keyGen.generateKeyPair(); + certificate = CertificateUtils.generateV1SelfSignedCertificate(keypair, "test-realm"); + } + + @Before + public void setUp() { + installationProvider = new DockerComposeYamlInstallationProvider(); + } + + private Response fireInstallationProvider() throws IOException { + ByteArrayOutputStream byteStream = null; + ZipOutputStream zipOutput = null; + byteStream = new ByteArrayOutputStream(); + zipOutput = new ZipOutputStream(byteStream); + + return installationProvider.generateInstallation(zipOutput, byteStream, certificate, new URL("http://localhost:8080/auth"), "docker-test", "docker-registry"); + } + + @Test + @Ignore // Used only for smoke testing + public void writeToRealZip() throws IOException { + final Response response = fireInstallationProvider(); + final byte[] responseBytes = (byte[]) response.getEntity(); + FileUtils.writeByteArrayToFile(new File("target/keycloak-docker-compose-yaml.zip"), responseBytes); + } + + @Test + public void testAllTheZipThings() throws Exception { + final Response response = fireInstallationProvider(); + assertThat("compose YAML returned non-ok response", response.getStatus(), equalTo(Response.Status.OK.getStatusCode())); + + shouldIncludeDockerComposeYamlInZip(getZipResponseFromInstallProvider(response)); + shouldIncludeReadmeInZip(getZipResponseFromInstallProvider(response)); + shouldWriteBlankDataDirectoryInZip(getZipResponseFromInstallProvider(response)); + shouldWriteCertDirectoryInZip(getZipResponseFromInstallProvider(response)); + shouldWriteSslCertificateInZip(getZipResponseFromInstallProvider(response)); + shouldWritePrivateKeyInZip(getZipResponseFromInstallProvider(response)); + } + + public void shouldIncludeDockerComposeYamlInZip(ZipInputStream zipInput) throws Exception { + final Optional dockerComposeFileContents = getFileContents(zipInput, ROOT_DIR + "docker-compose.yaml"); + + assertThat("Could not find docker-compose.yaml file in zip archive response", dockerComposeFileContents.isPresent(), equalTo(true)); + final boolean zipFileContentEqualsTestFile = IOUtils.contentEquals(new ByteArrayInputStream(dockerComposeFileContents.get().getBytes()), new FileInputStream("src/test/resources/docker-compose-expected.yaml")); + assertThat("Invalid docker-compose file contents: \n" + dockerComposeFileContents.get(), zipFileContentEqualsTestFile, equalTo(true)); + } + + public void shouldIncludeReadmeInZip(ZipInputStream zipInput) throws Exception { + final Optional dockerComposeFileContents = getFileContents(zipInput, ROOT_DIR + "README.md"); + + assertThat("Could not find README.md file in zip archive response", dockerComposeFileContents.isPresent(), equalTo(true)); + } + + public void shouldWriteBlankDataDirectoryInZip(ZipInputStream zipInput) throws Exception { + ZipEntry zipEntry; + boolean dataDirFound = false; + while ((zipEntry = zipInput.getNextEntry()) != null) { + try { + if (zipEntry.getName().equals(ROOT_DIR + "data/")) { + dataDirFound = true; + assertThat("Zip entry for data directory is not the correct type", zipEntry.isDirectory(), equalTo(true)); + } + } finally { + zipInput.closeEntry(); + } + } + + assertThat("Could not find data directory", dataDirFound, equalTo(true)); + } + + public void shouldWriteCertDirectoryInZip(ZipInputStream zipInput) throws Exception { + ZipEntry zipEntry; + boolean certsDirFound = false; + while ((zipEntry = zipInput.getNextEntry()) != null) { + try { + if (zipEntry.getName().equals(ROOT_DIR + "certs/")) { + certsDirFound = true; + assertThat("Zip entry for cert directory is not the correct type", zipEntry.isDirectory(), equalTo(true)); + } + } finally { + zipInput.closeEntry(); + } + } + + assertThat("Could not find cert directory", certsDirFound, equalTo(true)); + } + + public void shouldWriteSslCertificateInZip(ZipInputStream zipInput) throws Exception { + final Optional localhostCertificateFileContents = getFileContents(zipInput, ROOT_DIR + "certs/localhost.crt"); + + assertThat("Could not find localhost certificate", localhostCertificateFileContents.isPresent(), equalTo(true)); + final X509Certificate x509Certificate = PemUtils.decodeCertificate(localhostCertificateFileContents.get()); + assertThat("Invalid x509 given by docker-compose YAML", x509Certificate, notNullValue()); + } + + public void shouldWritePrivateKeyInZip(ZipInputStream zipInput) throws Exception { + final Optional localhostPrivateKeyFileContents = getFileContents(zipInput, ROOT_DIR + "certs/localhost.key"); + + assertThat("Could not find localhost private key", localhostPrivateKeyFileContents.isPresent(), equalTo(true)); + final PrivateKey privateKey = PemUtils.decodePrivateKey(localhostPrivateKeyFileContents.get()); + assertThat("Invalid private Key given by docker-compose YAML", privateKey, notNullValue()); + } + + private ZipInputStream getZipResponseFromInstallProvider(Response response) throws IOException { + final Object responseEntity = response.getEntity(); + if (!(responseEntity instanceof byte[])) { + fail("Recieved non-byte[] entity for docker-compose YAML installation response"); + } + + return new ZipInputStream(new ByteArrayInputStream((byte[]) responseEntity)); + } + + private static Optional getFileContents(final ZipInputStream zipInputStream, final String fileName) throws IOException { + ZipEntry zipEntry; + while ((zipEntry = zipInputStream.getNextEntry()) != null) { + try { + if (zipEntry.getName().equals(fileName)) { + return Optional.of(readBytesToString(zipInputStream)); + } + } finally { + zipInputStream.closeEntry(); + } + } + + // fall-through case if file name not found: + return Optional.empty(); + } + + private static String readBytesToString(final InputStream inputStream) throws IOException { + final ByteArrayOutputStream output = new ByteArrayOutputStream(); + final byte[] buffer = new byte[4096]; + int bytesRead; + + try { + while ((bytesRead = inputStream.read(buffer)) != -1) { + output.write(buffer, 0, bytesRead); + } + } finally { + output.close(); + } + + return new String(output.toByteArray()); + } +} diff --git a/services/src/test/java/org/keycloak/procotol/docker/installation/DockerKeyIdentifierTest.java b/services/src/test/java/org/keycloak/procotol/docker/installation/DockerKeyIdentifierTest.java new file mode 100644 index 0000000000..0fa8cb9888 --- /dev/null +++ b/services/src/test/java/org/keycloak/procotol/docker/installation/DockerKeyIdentifierTest.java @@ -0,0 +1,41 @@ +package org.keycloak.procotol.docker.installation; + +import org.hamcrest.CoreMatchers; +import org.junit.Before; +import org.junit.Test; +import org.keycloak.models.utils.Base32; +import org.keycloak.protocol.docker.DockerKeyIdentifier; + +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.PublicKey; +import java.security.SecureRandom; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.MatcherAssert.assertThat; + +/** + * Docker gets really unhappy if the key identifier is not in the format documented here: + * @see https://github.com/docker/libtrust/blob/master/key.go#L24 + */ +public class DockerKeyIdentifierTest { + + String keyIdentifierString; + PublicKey publicKey; + + @Before + public void shouldBlah() throws Exception { + final KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA"); + keyGen.initialize(2048, new SecureRandom()); + + final KeyPair keypair = keyGen.generateKeyPair(); + publicKey = keypair.getPublic(); + final DockerKeyIdentifier identifier = new DockerKeyIdentifier(publicKey); + keyIdentifierString = identifier.toString(); + } + + @Test + public void shoulProduceExpectedKeyFormat() { + assertThat("Every 4 chars are not delimted by colon", keyIdentifierString.matches("([\\w]{4}:){11}[\\w]{4}"), equalTo(true)); + } +} diff --git a/services/src/test/java/org/keycloak/test/broker/saml/SAMLDataMarshallerTest.java b/services/src/test/java/org/keycloak/test/broker/saml/SAMLDataMarshallerTest.java index 9a686217c8..c8647f347a 100755 --- a/services/src/test/java/org/keycloak/test/broker/saml/SAMLDataMarshallerTest.java +++ b/services/src/test/java/org/keycloak/test/broker/saml/SAMLDataMarshallerTest.java @@ -25,17 +25,22 @@ import org.keycloak.dom.saml.v2.assertion.AssertionType; import org.keycloak.dom.saml.v2.assertion.AuthnStatementType; import org.keycloak.dom.saml.v2.assertion.NameIDType; import org.keycloak.dom.saml.v2.protocol.ResponseType; +import org.keycloak.saml.processing.core.parsers.saml.SAMLParser; +import java.io.InputStream; +import org.hamcrest.CoreMatchers; +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.junit.Assert.assertThat; /** * @author Marek Posolda */ public class SAMLDataMarshallerTest { - private static final String TEST_RESPONSE = "http://localhost:8082/auth/realms/realm-with-saml-idp-basichttp://localhost:8082/auth/realms/realm-with-saml-idp-basictest-userhttp://localhost:8081/auth/realms/realm-with-brokerurn:oasis:names:tc:SAML:2.0:ac:classes:unspecified617-666-7777test-user@localhostmanager"; + private static final String TEST_RESPONSE = "http://localhost:8082/auth/realms/realm-with-saml-idp-basichttp://localhost:8082/auth/realms/realm-with-saml-idp-basictest-userhttp://localhost:8081/auth/realms/realm-with-brokerurn:oasis:names:tc:SAML:2.0:ac:classes:unspecified617-666-7777test-user@localhostmanager"; - private static final String TEST_ASSERTION = "http://localhost:8082/auth/realms/realm-with-saml-idp-basictest-userhttp://localhost:8081/auth/realms/realm-with-brokerurn:oasis:names:tc:SAML:2.0:ac:classes:unspecified617-666-7777test-user@localhostmanager"; + private static final String TEST_ASSERTION = "http://localhost:8082/auth/realms/realm-with-saml-idp-basictest-userhttp://localhost:8081/auth/realms/realm-with-brokerurn:oasis:names:tc:SAML:2.0:ac:classes:unspecified617-666-7777test-user@localhostmanager"; - private static final String TEST_ASSERTION_WITH_NAME_ID = "http://localhost:8082/auth/realms/realm-with-saml-idp-basictest-userhttp://localhost:8081/auth/realms/realm-with-brokerurn:oasis:names:tc:SAML:2.0:ac:classes:unspecified617-666-7777test-user@localhostb2c6275838784dba219c92f53ea5493c8ef4da09"; + private static final String TEST_ASSERTION_WITH_NAME_ID = "http://localhost:8082/auth/realms/realm-with-saml-idp-basictest-userhttp://localhost:8081/auth/realms/realm-with-brokerurn:oasis:names:tc:SAML:2.0:ac:classes:unspecified617-666-7777test-user@localhostb2c6275838784dba219c92f53ea5493c8ef4da09"; private static final String TEST_AUTHN_TYPE = "urn:oasis:names:tc:SAML:2.0:ac:classes:unspecified"; @@ -95,4 +100,40 @@ public class SAMLDataMarshallerTest { String serialized = serializer.serialize(authnStatement); Assert.assertEquals(TEST_AUTHN_TYPE, serialized); } + + @Test + public void testSerializeWithNamespaceInSignatureElement() throws Exception { + SAMLParser parser = new SAMLParser(); + try (InputStream st = SAMLDataMarshallerTest.class.getResourceAsStream("saml-response-ds-ns-in-signature.xml")) { + Object parsedObject = parser.parse(st); + assertThat(parsedObject, instanceOf(ResponseType.class)); + + ResponseType response = (ResponseType) parsedObject; + + SAMLDataMarshaller serializer = new SAMLDataMarshaller(); + String serialized = serializer.serialize(response.getAssertions().get(0).getAssertion()); + + AssertionType deserialized = serializer.deserialize(serialized, AssertionType.class); + assertThat(deserialized, CoreMatchers.notNullValue()); + assertThat(deserialized.getID(), CoreMatchers.is("id-4r-Xj702KQsM0gJyu3Fqpuwfe-LvDrEcQZpxKrhC")); + } + } + + @Test + public void testSerializeWithNamespaceNotInSignatureElement() throws Exception { + SAMLParser parser = new SAMLParser(); + try (InputStream st = SAMLDataMarshallerTest.class.getResourceAsStream("saml-response-ds-ns-above-signature.xml")) { + Object parsedObject = parser.parse(st); + assertThat(parsedObject, instanceOf(ResponseType.class)); + + ResponseType response = (ResponseType) parsedObject; + + SAMLDataMarshaller serializer = new SAMLDataMarshaller(); + String serialized = serializer.serialize(response.getAssertions().get(0).getAssertion()); + + AssertionType deserialized = serializer.deserialize(serialized, AssertionType.class); + assertThat(deserialized, CoreMatchers.notNullValue()); + assertThat(deserialized.getID(), CoreMatchers.is("id-4r-Xj702KQsM0gJyu3Fqpuwfe-LvDrEcQZpxKrhC")); + } + } } diff --git a/services/src/test/resources/docker-compose-expected.yaml b/services/src/test/resources/docker-compose-expected.yaml new file mode 100644 index 0000000000..3c912de3f9 --- /dev/null +++ b/services/src/test/resources/docker-compose-expected.yaml @@ -0,0 +1,15 @@ +registry: + image: registry:2 + ports: + - 127.0.0.1:5000:5000 + environment: + REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY: /data + REGISTRY_HTTP_TLS_CERTIFICATE: /opt/certs/localhost.crt + REGISTRY_HTTP_TLS_KEY: /opt/certs/localhost.key + REGISTRY_AUTH_TOKEN_REALM: http://localhost:8080/auth/realms/docker-test/protocol/docker-v2/auth + REGISTRY_AUTH_TOKEN_SERVICE: docker-registry + REGISTRY_AUTH_TOKEN_ISSUER: http://localhost:8080/auth/realms/docker-test + REGISTRY_AUTH_TOKEN_ROOTCERTBUNDLE: /opt/certs/localhost_trust_chain.pem + volumes: + - ./data:/data:z + - ./certs:/opt/certs:z \ No newline at end of file diff --git a/services/src/test/resources/org/keycloak/test/broker/saml/saml-response-ds-ns-above-signature.xml b/services/src/test/resources/org/keycloak/test/broker/saml/saml-response-ds-ns-above-signature.xml new file mode 100644 index 0000000000..dfa74aa12a --- /dev/null +++ b/services/src/test/resources/org/keycloak/test/broker/saml/saml-response-ds-ns-above-signature.xml @@ -0,0 +1,89 @@ + + SSO + + + + + SSO + + + + + + + + + + + DIGEST + + + SIG_VAL + + + my_email@my_provider.com + + + + + + + http://SERVER/auth/realms/MY_REALM + + + + + urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport + + + + + Yadav + + + H183561 + + + my_email@my_provider.com + + + MY_NAME + + + + diff --git a/services/src/test/resources/org/keycloak/test/broker/saml/saml-response-ds-ns-in-signature.xml b/services/src/test/resources/org/keycloak/test/broker/saml/saml-response-ds-ns-in-signature.xml new file mode 100644 index 0000000000..8460b8ec5b --- /dev/null +++ b/services/src/test/resources/org/keycloak/test/broker/saml/saml-response-ds-ns-in-signature.xml @@ -0,0 +1,88 @@ + + SSO + + + + + SSO + + + + + + + + + + + DIGEST + + + SIG_VAL + + + my_email@my_provider.com + + + + + + + http://SERVER/auth/realms/MY_REALM + + + + + urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport + + + + + Yadav + + + H183561 + + + my_email@my_provider.com + + + MY_NAME + + + + diff --git a/testsuite/integration-arquillian/HOW-TO-RUN.md b/testsuite/integration-arquillian/HOW-TO-RUN.md index cd818c2b3c..e80479dc0c 100644 --- a/testsuite/integration-arquillian/HOW-TO-RUN.md +++ b/testsuite/integration-arquillian/HOW-TO-RUN.md @@ -61,7 +61,7 @@ More info: http://javahowto.blogspot.cz/2010/09/java-agentlibjdwp-for-attaching. Analogically, there is the same behaviour for JBoss based app server as for auth server. The default port is set to 5006. There are app server properties. -Dapp.server.debug.port=$PORT - -Dapp.server.debug.suspend=y + -Dapp.server.debug.suspend=y ## Testsuite logging @@ -262,6 +262,8 @@ The UI tests are focused on the Admin Console as well as on some login scenarios The tests also use some constants placed in [test-constants.properties](tests/base/src/test/resources/test-constants.properties). A different file can be specified by `-Dtestsuite.constants=path/to/different-test-constants.properties` +In case a custom `settings.xml` is used for Maven, you need to specify it also in `-Dkie.maven.settings.custom=path/to/settings.xml`. + #### Execution example ``` mvn -f testsuite/integration-arquillian/tests/other/console/pom.xml \ @@ -452,7 +454,7 @@ First compile the Infinispan/JDG test server via the following command: `mvn -Pcache-server-infinispan -f testsuite/integration-arquillian -DskipTests clean install` or - + `mvn -Pcache-server-jdg -f testsuite/integration-arquillian -DskipTests clean install` Then you can run the tests using the following command (adjust the test specification according to your needs): @@ -462,5 +464,159 @@ Then you can run the tests using the following command (adjust the test specific or `mvn -Pcache-server-jdg -Dtest=*.crossdc.* -pl testsuite/integration-arquillian/tests/base test` + +It can be useful to add additional system property to enable logging: + + -Dkeycloak.infinispan.logging.level=debug + + -_Someone using IntelliJ IDEA, please describe steps for that IDE_ +#### Run Cross-DC Tests from Intellij IDEA + +First we will manually download, configure and run infinispan server. Then we can run the tests from IDE against 1 server. It's more effective during +development as there is no need to restart infinispan server(s) among test runs. + +1) Download infinispan server 8.2.X from http://infinispan.org/download/ + +2) Edit `ISPN_SERVER_HOME/standalone/configuration/standalone.xml` and add these local-caches to the section under cache-container `local` : + + + + + + + + + + + + + +3) Run the server through `./standalone.sh` + +4) Setup MySQL database or some other shared database. + +5) Ensure that org.wildfly.arquillian:wildfly-arquillian-container-managed is on the classpath when running test. On Intellij, it can be +done by going to: View -> Tool Windows -> Maven projects. Then check profile "cache-server-infinispan". The tests will use this profile when executed. + +6) Run the LoginCrossDCTest (or any other test) with those properties. In shortcut, it's using MySQL database, disabled L1 lifespan and +connects to the remoteStore provided by infinispan server configured in previous steps: + + -Dauth.server.crossdc=true -Dauth.server.undertow.crossdc=true -Dcache.server.lifecycle.skip=true -Dkeycloak.connectionsJpa.url.crossdc=jdbc:mysql://localhost/keycloak + -Dkeycloak.connectionsJpa.driver.crossdc=com.mysql.jdbc.Driver -Dkeycloak.connectionsJpa.user=keycloak + -Dkeycloak.connectionsJpa.password=keycloak -Dkeycloak.connectionsInfinispan.clustered=true -Dkeycloak.connectionsInfinispan.l1Lifespan=0 + -Dkeycloak.connectionsInfinispan.remoteStorePort=11222 -Dkeycloak.connectionsInfinispan.remoteStorePort.2=11222 -Dkeycloak.connectionsInfinispan.sessionsOwners=1 + -Dsession.cache.owners=1 -Dkeycloak.infinispan.logging.level=debug -Dresources + +7) If you want to debug and test manually, the servers are running on these ports (Note that not all backend servers are running by default and some might be also unused by loadbalancer): + + Loadbalancer -> "http://localhost:8180/auth" + auth-server-undertow-cross-dc-0_1 -> "http://localhost:8101/auth" + auth-server-undertow-cross-dc-0_2-manual -> "http://localhost:8102/auth" + auth-server-undertow-cross-dc-1_1 -> "http://localhost:8111/auth" + auth-server-undertow-cross-dc-1_2-manual -> "http://localhost:8112/auth" + + +## Run Docker Authentication test + +First, validate that your machine has a valid docker installation and that it is available to the JVM running the test. +The exact steps to configure Docker depend on the operating system. + +By default, the test will run against Undertow based embedded Keycloak Server, thus no distribution build is required beforehand. +The exact command line arguments depend on the operating system. + +### General guidelines + +If docker daemon doesn't run locally, or if you're not running on Linux, you may need + to determine the IP of the bridge interface or local interface that Docker daemon can use to connect to Keycloak Server. + Then specify that IP as additional system property called *host.ip*, for example: + + -Dhost.ip=192.168.64.1 + +If using Docker for Mac, you can create an alias for your local network interface: + + sudo ifconfig lo0 alias 10.200.10.1/24 + +Then pass the IP as *host.ip*: + + -Dhost.ip=10.200.10.1 + + +If you're running a Docker fork that always lists a host component of an image on `docker images` (e.g. Fedora / RHEL Docker) +use `-Ddocker.io-prefix-explicit=true` argument when running the test. + + +### Fedora + +On Fedora one way to set up Docker server is the following: + + # install docker + sudo dnf install docker + + # configure docker + # remove --selinux-enabled from OPTIONS + sudo vi /etc/sysconfig/docker + + # create docker group and add your user (so docker wouldn't need root permissions) + sudo groupadd docker && sudo gpasswd -a ${USER} docker && sudo systemctl restart docker + newgrp docker + + # you need to login again after this + + + # make sure Docker is available + docker pull registry:2 + +You may also need to add an iptables rule to allow container to host traffic + + sudo iptables -I INPUT -i docker0 -j ACCEPT + +Then, run the test passing `-Ddocker.io-prefix-explicit=true`: + + mvn -f testsuite/integration-arquillian/tests/base/pom.xml \ + clean test \ + -Dtest=DockerClientTest \ + -Dkeycloak.profile.feature.docker=enabled \ + -Ddocker.io-prefix-explicit=true + + +### macOS + +On macOS all you need to do is install Docker for Mac, start it up, and check that it works: + + # make sure Docker is available + docker pull registry:2 + +Be especially careful to restart Docker server after every sleep / suspend to ensure system clock of Docker VM is synchronized with +that of the host operating system - Docker for Mac runs inside a VM. + + +Then, run the test passing `-Dhost.ip=IP` where IP corresponds to en0 interface or an alias for localhost: + + mvn -f testsuite/integration-arquillian/tests/base/pom.xml \ + clean test \ + -Dtest=DockerClientTest \ + -Dkeycloak.profile.feature.docker=enabled \ + -Dhost.ip=10.200.10.1 + + + +### Running Docker test against Keycloak Server distribution + +Make sure to build the distribution: + + mvn clean install -f distribution + +Then, before running the test, setup Keycloak Server distribution for the tests: + + mvn -f testsuite/integration-arquillian/servers/pom.xml \ + clean install \ + -Pauth-server-wildfly + +When running the test, add the following arguments to the command line: + + -Pauth-server-wildfly -Pauth-server-enable-disable-feature -Dfeature.name=docker -Dfeature.value=enabled diff --git a/testsuite/integration-arquillian/pom.xml b/testsuite/integration-arquillian/pom.xml index 7e36d1a5e7..0bcb2b899b 100644 --- a/testsuite/integration-arquillian/pom.xml +++ b/testsuite/integration-arquillian/pom.xml @@ -24,7 +24,7 @@ org.keycloak keycloak-testsuite-pom - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../pom.xml @@ -46,6 +46,7 @@ 2.0.1.Final 2.1.0.Alpha3 2.1.0.Alpha2 + 1.0.1.Final 1.2.0.Beta2 2.2.2 1.0.0.Alpha2 @@ -108,6 +109,12 @@ wildfly-arquillian-container-domain-managed ${arquillian-wildfly-container.version} + + org.jboss.arquillian.container + arquillian-wls-remote-12.1.x + ${arquillian-wls-container.version} + test + @@ -136,7 +143,6 @@ test-apps - test-utils servers tests diff --git a/testsuite/integration-arquillian/servers/app-server/jboss/as7/pom.xml b/testsuite/integration-arquillian/servers/app-server/jboss/as7/pom.xml index 51e243ca4f..99e193528a 100644 --- a/testsuite/integration-arquillian/servers/app-server/jboss/as7/pom.xml +++ b/testsuite/integration-arquillian/servers/app-server/jboss/as7/pom.xml @@ -21,7 +21,7 @@ org.keycloak.testsuite integration-arquillian-servers-app-server-jboss - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT 4.0.0 diff --git a/testsuite/integration-arquillian/servers/app-server/jboss/eap/pom.xml b/testsuite/integration-arquillian/servers/app-server/jboss/eap/pom.xml index 1764447bdd..7e7b10be53 100644 --- a/testsuite/integration-arquillian/servers/app-server/jboss/eap/pom.xml +++ b/testsuite/integration-arquillian/servers/app-server/jboss/eap/pom.xml @@ -21,7 +21,7 @@ org.keycloak.testsuite integration-arquillian-servers-app-server-jboss - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT 4.0.0 diff --git a/testsuite/integration-arquillian/servers/app-server/jboss/eap6-fuse/pom.xml b/testsuite/integration-arquillian/servers/app-server/jboss/eap6-fuse/pom.xml index 22663bdb8f..0a1eb4c36e 100644 --- a/testsuite/integration-arquillian/servers/app-server/jboss/eap6-fuse/pom.xml +++ b/testsuite/integration-arquillian/servers/app-server/jboss/eap6-fuse/pom.xml @@ -5,7 +5,7 @@ integration-arquillian-servers-app-server-jboss org.keycloak.testsuite - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT 4.0.0 diff --git a/testsuite/integration-arquillian/servers/app-server/jboss/eap6/pom.xml b/testsuite/integration-arquillian/servers/app-server/jboss/eap6/pom.xml index e3f057a483..e78f3e8403 100644 --- a/testsuite/integration-arquillian/servers/app-server/jboss/eap6/pom.xml +++ b/testsuite/integration-arquillian/servers/app-server/jboss/eap6/pom.xml @@ -21,7 +21,7 @@ org.keycloak.testsuite integration-arquillian-servers-app-server-jboss - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT 4.0.0 diff --git a/testsuite/integration-arquillian/servers/app-server/jboss/pom.xml b/testsuite/integration-arquillian/servers/app-server/jboss/pom.xml index 3775738b33..382612cfb8 100644 --- a/testsuite/integration-arquillian/servers/app-server/jboss/pom.xml +++ b/testsuite/integration-arquillian/servers/app-server/jboss/pom.xml @@ -21,7 +21,7 @@ org.keycloak.testsuite integration-arquillian-servers-app-server - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT 4.0.0 @@ -309,7 +309,7 @@ ${common.resources}/install-patch.${script.suffix} - ${app.server.home}/bin + ${app.server.jboss.home}/bin ${app.server.java.home} ${app.server.jboss.home} diff --git a/testsuite/integration-arquillian/servers/app-server/jboss/relative/eap/pom.xml b/testsuite/integration-arquillian/servers/app-server/jboss/relative/eap/pom.xml index 9bb5d9937b..4e47112555 100644 --- a/testsuite/integration-arquillian/servers/app-server/jboss/relative/eap/pom.xml +++ b/testsuite/integration-arquillian/servers/app-server/jboss/relative/eap/pom.xml @@ -21,7 +21,7 @@ org.keycloak.testsuite integration-arquillian-servers-app-server-jboss-relative - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT 4.0.0 diff --git a/testsuite/integration-arquillian/servers/app-server/jboss/relative/pom.xml b/testsuite/integration-arquillian/servers/app-server/jboss/relative/pom.xml index 2a5df753df..3014456dcc 100644 --- a/testsuite/integration-arquillian/servers/app-server/jboss/relative/pom.xml +++ b/testsuite/integration-arquillian/servers/app-server/jboss/relative/pom.xml @@ -21,7 +21,7 @@ org.keycloak.testsuite integration-arquillian-servers-app-server-jboss - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT 4.0.0 diff --git a/testsuite/integration-arquillian/servers/app-server/jboss/relative/wildfly/pom.xml b/testsuite/integration-arquillian/servers/app-server/jboss/relative/wildfly/pom.xml index fcf1ff3ae4..18e864e14b 100644 --- a/testsuite/integration-arquillian/servers/app-server/jboss/relative/wildfly/pom.xml +++ b/testsuite/integration-arquillian/servers/app-server/jboss/relative/wildfly/pom.xml @@ -21,7 +21,7 @@ org.keycloak.testsuite integration-arquillian-servers-app-server-jboss-relative - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT 4.0.0 diff --git a/testsuite/integration-arquillian/servers/app-server/jboss/wildfly/pom.xml b/testsuite/integration-arquillian/servers/app-server/jboss/wildfly/pom.xml index 68f00c9c28..a4c073ea9c 100644 --- a/testsuite/integration-arquillian/servers/app-server/jboss/wildfly/pom.xml +++ b/testsuite/integration-arquillian/servers/app-server/jboss/wildfly/pom.xml @@ -21,7 +21,7 @@ org.keycloak.testsuite integration-arquillian-servers-app-server-jboss - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT 4.0.0 diff --git a/testsuite/integration-arquillian/servers/app-server/jboss/wildfly10/pom.xml b/testsuite/integration-arquillian/servers/app-server/jboss/wildfly10/pom.xml index 4d37972cbf..50def5c4a9 100644 --- a/testsuite/integration-arquillian/servers/app-server/jboss/wildfly10/pom.xml +++ b/testsuite/integration-arquillian/servers/app-server/jboss/wildfly10/pom.xml @@ -21,7 +21,7 @@ org.keycloak.testsuite integration-arquillian-servers-app-server-jboss - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT 4.0.0 diff --git a/testsuite/integration-arquillian/servers/app-server/jboss/wildfly8/pom.xml b/testsuite/integration-arquillian/servers/app-server/jboss/wildfly8/pom.xml index 2f5f4b13d6..60845f88f7 100644 --- a/testsuite/integration-arquillian/servers/app-server/jboss/wildfly8/pom.xml +++ b/testsuite/integration-arquillian/servers/app-server/jboss/wildfly8/pom.xml @@ -21,7 +21,7 @@ org.keycloak.testsuite integration-arquillian-servers-app-server-jboss - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT 4.0.0 diff --git a/testsuite/integration-arquillian/servers/app-server/jboss/wildfly9/pom.xml b/testsuite/integration-arquillian/servers/app-server/jboss/wildfly9/pom.xml index 218453adbe..afdb346215 100644 --- a/testsuite/integration-arquillian/servers/app-server/jboss/wildfly9/pom.xml +++ b/testsuite/integration-arquillian/servers/app-server/jboss/wildfly9/pom.xml @@ -21,7 +21,7 @@ org.keycloak.testsuite integration-arquillian-servers-app-server-jboss - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT 4.0.0 diff --git a/testsuite/integration-arquillian/servers/app-server/karaf/fuse61/pom.xml b/testsuite/integration-arquillian/servers/app-server/karaf/fuse61/pom.xml index efd21cff52..0b565ec1f6 100644 --- a/testsuite/integration-arquillian/servers/app-server/karaf/fuse61/pom.xml +++ b/testsuite/integration-arquillian/servers/app-server/karaf/fuse61/pom.xml @@ -21,7 +21,7 @@ org.keycloak.testsuite integration-arquillian-servers-app-server-karaf - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT 4.0.0 diff --git a/testsuite/integration-arquillian/servers/app-server/karaf/fuse62/pom.xml b/testsuite/integration-arquillian/servers/app-server/karaf/fuse62/pom.xml index 6760aae970..2d555c462a 100644 --- a/testsuite/integration-arquillian/servers/app-server/karaf/fuse62/pom.xml +++ b/testsuite/integration-arquillian/servers/app-server/karaf/fuse62/pom.xml @@ -21,7 +21,7 @@ org.keycloak.testsuite integration-arquillian-servers-app-server-karaf - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT 4.0.0 diff --git a/testsuite/integration-arquillian/servers/app-server/karaf/fuse63/pom.xml b/testsuite/integration-arquillian/servers/app-server/karaf/fuse63/pom.xml index a5966c5e75..eccac119fd 100644 --- a/testsuite/integration-arquillian/servers/app-server/karaf/fuse63/pom.xml +++ b/testsuite/integration-arquillian/servers/app-server/karaf/fuse63/pom.xml @@ -21,7 +21,7 @@ org.keycloak.testsuite integration-arquillian-servers-app-server-karaf - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT 4.0.0 diff --git a/testsuite/integration-arquillian/servers/app-server/karaf/fuse63/src/main/resources/update-config-auth.cli b/testsuite/integration-arquillian/servers/app-server/karaf/fuse63/src/main/resources/update-config-auth.cli index 2c3bc66080..f343e283b5 100644 --- a/testsuite/integration-arquillian/servers/app-server/karaf/fuse63/src/main/resources/update-config-auth.cli +++ b/testsuite/integration-arquillian/servers/app-server/karaf/fuse63/src/main/resources/update-config-auth.cli @@ -4,5 +4,5 @@ config:update system-property -p hawtio.roles admin,user system-property -p hawtio.keycloakEnabled true system-property -p hawtio.realm keycloak -system-property -p hawtio.keycloakClientConfig \$\{karaf.base\}/etc/keycloak-hawtio-client.json +system-property -p hawtio.keycloakClientConfig file://\$\{karaf.base\}/etc/keycloak-hawtio-client.json system-property -p hawtio.rolePrincipalClasses org.keycloak.adapters.jaas.RolePrincipal,org.apache.karaf.jaas.boot.principal.RolePrincipal diff --git a/testsuite/integration-arquillian/servers/app-server/karaf/karaf3/pom.xml b/testsuite/integration-arquillian/servers/app-server/karaf/karaf3/pom.xml index 93cf564736..9b307e5d04 100644 --- a/testsuite/integration-arquillian/servers/app-server/karaf/karaf3/pom.xml +++ b/testsuite/integration-arquillian/servers/app-server/karaf/karaf3/pom.xml @@ -21,7 +21,7 @@ org.keycloak.testsuite integration-arquillian-servers-app-server-karaf - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT 4.0.0 diff --git a/testsuite/integration-arquillian/servers/app-server/karaf/pom.xml b/testsuite/integration-arquillian/servers/app-server/karaf/pom.xml index 15bf0976e8..e61143471c 100644 --- a/testsuite/integration-arquillian/servers/app-server/karaf/pom.xml +++ b/testsuite/integration-arquillian/servers/app-server/karaf/pom.xml @@ -21,7 +21,7 @@ org.keycloak.testsuite integration-arquillian-servers-app-server - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT 4.0.0 diff --git a/testsuite/integration-arquillian/servers/app-server/pom.xml b/testsuite/integration-arquillian/servers/app-server/pom.xml index 3120cc31d5..f0c8fbf8c8 100644 --- a/testsuite/integration-arquillian/servers/app-server/pom.xml +++ b/testsuite/integration-arquillian/servers/app-server/pom.xml @@ -21,7 +21,7 @@ org.keycloak.testsuite integration-arquillian-servers - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT 4.0.0 diff --git a/testsuite/integration-arquillian/servers/app-server/tomcat/pom.xml b/testsuite/integration-arquillian/servers/app-server/tomcat/pom.xml index cc741294f1..3d0d309065 100644 --- a/testsuite/integration-arquillian/servers/app-server/tomcat/pom.xml +++ b/testsuite/integration-arquillian/servers/app-server/tomcat/pom.xml @@ -21,7 +21,7 @@ org.keycloak.testsuite integration-arquillian-servers-app-server - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT 4.0.0 diff --git a/testsuite/integration-arquillian/servers/app-server/tomcat/tomcat7/pom.xml b/testsuite/integration-arquillian/servers/app-server/tomcat/tomcat7/pom.xml index fd4af48a4b..bbc1631b50 100644 --- a/testsuite/integration-arquillian/servers/app-server/tomcat/tomcat7/pom.xml +++ b/testsuite/integration-arquillian/servers/app-server/tomcat/tomcat7/pom.xml @@ -21,7 +21,7 @@ org.keycloak.testsuite integration-arquillian-servers-app-server-tomcat - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT 4.0.0 diff --git a/testsuite/integration-arquillian/servers/app-server/tomcat/tomcat8/pom.xml b/testsuite/integration-arquillian/servers/app-server/tomcat/tomcat8/pom.xml index 99b4353a5c..82f7d30cef 100644 --- a/testsuite/integration-arquillian/servers/app-server/tomcat/tomcat8/pom.xml +++ b/testsuite/integration-arquillian/servers/app-server/tomcat/tomcat8/pom.xml @@ -21,7 +21,7 @@ org.keycloak.testsuite integration-arquillian-servers-app-server-tomcat - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT 4.0.0 diff --git a/testsuite/integration-arquillian/servers/app-server/tomcat/tomcat9/pom.xml b/testsuite/integration-arquillian/servers/app-server/tomcat/tomcat9/pom.xml index 48b0234d3a..463dff3645 100644 --- a/testsuite/integration-arquillian/servers/app-server/tomcat/tomcat9/pom.xml +++ b/testsuite/integration-arquillian/servers/app-server/tomcat/tomcat9/pom.xml @@ -21,7 +21,7 @@ org.keycloak.testsuite integration-arquillian-servers-app-server-tomcat - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT 4.0.0 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 index d104e37cc8..276f389517 100644 --- 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 @@ -36,6 +36,15 @@ + + + + + + + + + org.keycloak.testsuite.integration-arquillian-testsuite-providers @@ -60,11 +69,12 @@ - + + diff --git a/testsuite/integration-arquillian/servers/auth-server/jboss/eap/pom.xml b/testsuite/integration-arquillian/servers/auth-server/jboss/eap/pom.xml index 0acda47c0d..426906d088 100644 --- a/testsuite/integration-arquillian/servers/auth-server/jboss/eap/pom.xml +++ b/testsuite/integration-arquillian/servers/auth-server/jboss/eap/pom.xml @@ -21,7 +21,7 @@ org.keycloak.testsuite integration-arquillian-servers-auth-server-jboss - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT 4.0.0 diff --git a/testsuite/integration-arquillian/servers/auth-server/jboss/pom.xml b/testsuite/integration-arquillian/servers/auth-server/jboss/pom.xml index 775aa5a0bd..c7059ad4bf 100644 --- a/testsuite/integration-arquillian/servers/auth-server/jboss/pom.xml +++ b/testsuite/integration-arquillian/servers/auth-server/jboss/pom.xml @@ -21,7 +21,7 @@ org.keycloak.testsuite integration-arquillian-servers-auth-server - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT 4.0.0 @@ -100,7 +100,6 @@ ${project.build.directory}/unpacked - **/product.conf diff --git a/testsuite/integration-arquillian/servers/auth-server/jboss/wildfly/pom.xml b/testsuite/integration-arquillian/servers/auth-server/jboss/wildfly/pom.xml index e1c64cfb73..675104f219 100644 --- a/testsuite/integration-arquillian/servers/auth-server/jboss/wildfly/pom.xml +++ b/testsuite/integration-arquillian/servers/auth-server/jboss/wildfly/pom.xml @@ -21,7 +21,7 @@ org.keycloak.testsuite integration-arquillian-servers-auth-server-jboss - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT 4.0.0 @@ -30,6 +30,14 @@ integration-arquillian-servers-auth-server-wildfly Auth Server - JBoss - Wildfly + + + + org.keycloak + keycloak-server-dist + zip + + wildfly diff --git a/testsuite/integration-arquillian/servers/auth-server/pom.xml b/testsuite/integration-arquillian/servers/auth-server/pom.xml index 5fdb02c8c5..31f2aa560a 100644 --- a/testsuite/integration-arquillian/servers/auth-server/pom.xml +++ b/testsuite/integration-arquillian/servers/auth-server/pom.xml @@ -21,7 +21,7 @@ org.keycloak.testsuite integration-arquillian-servers - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT 4.0.0 diff --git a/testsuite/integration-arquillian/servers/auth-server/services/pom.xml b/testsuite/integration-arquillian/servers/auth-server/services/pom.xml index f154e414d0..6387079b1f 100644 --- a/testsuite/integration-arquillian/servers/auth-server/services/pom.xml +++ b/testsuite/integration-arquillian/servers/auth-server/services/pom.xml @@ -21,7 +21,7 @@ org.keycloak.testsuite integration-arquillian-servers-auth-server - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT 4.0.0 diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/pom.xml b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/pom.xml index d29bb1a00a..8ad166f5d5 100644 --- a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/pom.xml +++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/pom.xml @@ -24,7 +24,7 @@ org.keycloak.testsuite integration-arquillian-servers-auth-server-services - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT integration-arquillian-testsuite-providers 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 f18bbbdf82..d5395f2bbf 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 @@ -64,6 +64,7 @@ import org.keycloak.testsuite.runonserver.FetchOnServer; import org.keycloak.testsuite.runonserver.RunOnServer; import org.keycloak.testsuite.runonserver.SerializationUtil; import org.keycloak.util.JsonSerialization; +import org.keycloak.utils.MediaType; import javax.ws.rs.Consumes; import javax.ws.rs.GET; @@ -74,7 +75,6 @@ import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; -import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import java.text.ParseException; import java.text.SimpleDateFormat; @@ -169,6 +169,25 @@ public class TestingResourceProvider implements RealmResourceProvider { return Response.ok().build(); } + @GET + @Path("/get-client-sessions-count") + @Produces(MediaType.APPLICATION_JSON) + public Integer getClientSessionsCountInUserSession(@QueryParam("realm") final String name, @QueryParam("session") final String sessionId) { + + RealmManager realmManager = new RealmManager(session); + RealmModel realm = realmManager.getRealmByName(name); + if (realm == null) { + throw new NotFoundException("Realm not found"); + } + + UserSessionModel sessionModel = session.sessions().getUserSession(realm, sessionId); + if (sessionModel == null) { + throw new NotFoundException("Session not found"); + } + + return sessionModel.getAuthenticatedClientSessions().size(); + } + @GET @Path("/time-offset") @Produces(MediaType.APPLICATION_JSON) @@ -655,8 +674,8 @@ public class TestingResourceProvider implements RealmResourceProvider { @POST @Path("/run-on-server") - @Consumes(MediaType.TEXT_PLAIN) - @Produces(MediaType.TEXT_PLAIN) + @Consumes(MediaType.TEXT_PLAIN_UTF_8) + @Produces(MediaType.TEXT_PLAIN_UTF_8) public String runOnServer(String runOnServer) throws Exception { try { ClassLoader cl = ModuleUtil.isModules() ? ModuleUtil.getClassLoader() : getClass().getClassLoader(); diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/representation/JGroupsStats.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/representation/JGroupsStats.java new file mode 100644 index 0000000000..e14609a345 --- /dev/null +++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/representation/JGroupsStats.java @@ -0,0 +1,84 @@ +/* + * 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.representation; + +import java.text.NumberFormat; + +/** + * @author Marek Posolda + */ +public class JGroupsStats { + + private static final NumberFormat NUMBER_FORMAT = NumberFormat.getInstance(); + + static { + NUMBER_FORMAT.setGroupingUsed(true); + } + + private long sentBytes; + private long sentMessages; + private long receivedBytes; + private long receivedMessages; + + public JGroupsStats() { + } + + public JGroupsStats(long sentBytes, long sentMessages, long receivedBytes, long receivedMessages) { + this.sentBytes = sentBytes; + this.sentMessages = sentMessages; + this.receivedBytes = receivedBytes; + this.receivedMessages = receivedMessages; + } + + public long getSentBytes() { + return sentBytes; + } + + public void setSentBytes(long sentBytes) { + this.sentBytes = sentBytes; + } + + public long getSentMessages() { + return sentMessages; + } + + public void setSentMessages(long sentMessages) { + this.sentMessages = sentMessages; + } + + public long getReceivedBytes() { + return receivedBytes; + } + + public void setReceivedBytes(long receivedBytes) { + this.receivedBytes = receivedBytes; + } + + public long getReceivedMessages() { + return receivedMessages; + } + + public void setReceivedMessages(long receivedMessages) { + this.receivedMessages = receivedMessages; + } + + public String statsAsString() { + return String.format("sentBytes: %s, sentMessages: %d, receivedBytes: %s, receivedMessages: %d", + NUMBER_FORMAT.format(sentBytes), sentMessages, NUMBER_FORMAT.format(receivedBytes), receivedMessages); + } +} diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/representation/RemoteCacheStats.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/representation/RemoteCacheStats.java new file mode 100644 index 0000000000..272cbc5fb5 --- /dev/null +++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/representation/RemoteCacheStats.java @@ -0,0 +1,67 @@ +/* + * Copyright 2017 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.representation; + +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.JsonProperty; +import org.infinispan.client.hotrod.ServerStatistics; + +/** + * @author Marek Posolda + */ +public class RemoteCacheStats { + + @JsonProperty(ServerStatistics.STORES) + private Integer stores; + + @JsonProperty("globalStores") + private Integer globalStores; + + private Map otherStats = new HashMap<>(); + + + public Integer getStores() { + return stores; + } + + public void setStores(Integer stores) { + this.stores = stores; + } + + public Integer getGlobalStores() { + return globalStores; + } + + public void setGlobalStores(Integer globalStores) { + this.globalStores = globalStores; + } + + @JsonAnyGetter + public Map getOtherStats() { + return otherStats; + } + + @JsonAnySetter + public void setOtherStats(String name, String value) { + otherStats.put(name, value); + } +} diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/resource/TestCacheResource.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/resource/TestCacheResource.java index b6f0b81b45..9847b27dcb 100644 --- a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/resource/TestCacheResource.java +++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/resource/TestCacheResource.java @@ -17,6 +17,8 @@ package org.keycloak.testsuite.rest.resource; +import java.util.HashMap; +import java.util.Map; import java.util.Set; import java.util.stream.Collectors; @@ -28,8 +30,15 @@ import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType; import org.infinispan.Cache; +import org.infinispan.client.hotrod.RemoteCache; +import org.infinispan.remoting.transport.Transport; +import org.infinispan.remoting.transport.jgroups.JGroupsTransport; +import org.jgroups.JChannel; import org.keycloak.connections.infinispan.InfinispanConnectionProvider; import org.keycloak.models.KeycloakSession; +import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity; +import org.keycloak.models.sessions.infinispan.util.InfinispanUtil; +import org.keycloak.testsuite.rest.representation.JGroupsStats; /** * @author Marek Posolda @@ -77,4 +86,55 @@ public class TestCacheResource { public void clear() { cache.clear(); } + + @GET + @Path("/jgroups-stats") + @Produces(MediaType.APPLICATION_JSON) + public JGroupsStats getJgroupsStats() { + Transport transport = cache.getCacheManager().getTransport(); + if (transport == null) { + return new JGroupsStats(0, 0, 0, 0); + } else { + try { + // Need to use reflection due some incompatibilities between ispn 8.2.6 and 9.0.1 + JChannel channel = (JChannel) transport.getClass().getMethod("getChannel").invoke(transport); + + return new JGroupsStats(channel.getSentBytes(), channel.getSentMessages(), channel.getReceivedBytes(), channel.getReceivedMessages()); + } catch (Exception nsme) { + throw new RuntimeException(nsme); + } + } + } + + + @GET + @Path("/remote-cache-stats") + @Produces(MediaType.APPLICATION_JSON) + public Map getRemoteCacheStats() { + RemoteCache remoteCache = InfinispanUtil.getRemoteCache(cache); + if (remoteCache == null) { + return new HashMap<>(); + } else { + return remoteCache.stats().getStatsMap(); + } + } + + + @GET + @Path("/remote-cache-last-session-refresh/{user-session-id}") + @Produces(MediaType.APPLICATION_JSON) + public int getRemoteCacheLastSessionRefresh(@PathParam("user-session-id") String userSessionId) { + RemoteCache remoteCache = InfinispanUtil.getRemoteCache(cache); + if (remoteCache == null) { + return -1; + } else { + UserSessionEntity userSession = (UserSessionEntity) remoteCache.get(userSessionId); + if (userSession == null) { + return -1; + } else { + return userSession.getLastSessionRefresh(); + } + } + } + } diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/runonserver/SerializationUtil.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/runonserver/SerializationUtil.java index cd1b21665d..1f56a807ae 100644 --- a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/runonserver/SerializationUtil.java +++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/runonserver/SerializationUtil.java @@ -50,7 +50,7 @@ public class SerializationUtil { oos.writeObject(t); oos.close(); - return Base64.encodeBytes(os.toByteArray()); + return "EXCEPTION:" + Base64.encodeBytes(os.toByteArray()); } catch (Exception e) { throw new RuntimeException(e); } @@ -58,6 +58,7 @@ public class SerializationUtil { public static Throwable decodeException(String result) { try { + result = result.substring("EXCEPTION:".length()); byte[] bytes = Base64.decode(result); ByteArrayInputStream is = new ByteArrayInputStream(bytes); ObjectInputStream ois = new ObjectInputStream(is); diff --git a/testsuite/integration-arquillian/servers/auth-server/undertow/pom.xml b/testsuite/integration-arquillian/servers/auth-server/undertow/pom.xml index fdcb092fdc..ba495704ca 100644 --- a/testsuite/integration-arquillian/servers/auth-server/undertow/pom.xml +++ b/testsuite/integration-arquillian/servers/auth-server/undertow/pom.xml @@ -21,7 +21,7 @@ org.keycloak.testsuite integration-arquillian-servers-auth-server - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT 4.0.0 diff --git a/testsuite/integration-arquillian/servers/auth-server/undertow/src/main/java/org/keycloak/testsuite/arquillian/undertow/lb/SimpleUndertowLoadBalancer.java b/testsuite/integration-arquillian/servers/auth-server/undertow/src/main/java/org/keycloak/testsuite/arquillian/undertow/lb/SimpleUndertowLoadBalancer.java index 3b533f1349..1a4b1183cf 100644 --- a/testsuite/integration-arquillian/servers/auth-server/undertow/src/main/java/org/keycloak/testsuite/arquillian/undertow/lb/SimpleUndertowLoadBalancer.java +++ b/testsuite/integration-arquillian/servers/auth-server/undertow/src/main/java/org/keycloak/testsuite/arquillian/undertow/lb/SimpleUndertowLoadBalancer.java @@ -208,10 +208,10 @@ public class SimpleUndertowLoadBalancer { if (stickyHost != null) { if (!stickyHost.isAvailable()) { - log.infof("Sticky host %s not available. Trying different hosts", stickyHost.getUri()); + log.debugf("Sticky host %s not available. Trying different hosts", stickyHost.getUri()); return null; } else { - log.infof("Sticky host %s found and looks available", stickyHost.getUri()); + log.debugf("Sticky host %s found and looks available", stickyHost.getUri()); } } @@ -259,7 +259,7 @@ public class SimpleUndertowLoadBalancer { } else { // Host was restored if (!host.isAvailable()) { - log.infof("Host %s available again", host.getUri()); + log.infof("Host %s available again after failover", host.getUri()); host.clearError(); } } diff --git a/testsuite/integration-arquillian/servers/cache-server/jboss/common/add-keycloak-caches.xsl b/testsuite/integration-arquillian/servers/cache-server/jboss/common/add-keycloak-caches.xsl index 540b4b5f57..b6fbd2e53c 100644 --- a/testsuite/integration-arquillian/servers/cache-server/jboss/common/add-keycloak-caches.xsl +++ b/testsuite/integration-arquillian/servers/cache-server/jboss/common/add-keycloak-caches.xsl @@ -29,8 +29,16 @@ /*[local-name()='cache-container' and starts-with(namespace-uri(), $nsCacheServer) and @name='local']"> - - + + + + + + + + + + @@ -38,8 +46,17 @@ /*[local-name()='cache-container' and starts-with(namespace-uri(), $nsCacheServer) and @name='clustered']"> - - + + + + + + + + + + + diff --git a/testsuite/integration-arquillian/servers/cache-server/jboss/infinispan/pom.xml b/testsuite/integration-arquillian/servers/cache-server/jboss/infinispan/pom.xml index 3ac23acf1f..73735e24ee 100644 --- a/testsuite/integration-arquillian/servers/cache-server/jboss/infinispan/pom.xml +++ b/testsuite/integration-arquillian/servers/cache-server/jboss/infinispan/pom.xml @@ -21,7 +21,7 @@ org.keycloak.testsuite integration-arquillian-servers-cache-server-jboss - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT 4.0.0 diff --git a/testsuite/integration-arquillian/servers/cache-server/jboss/jdg/pom.xml b/testsuite/integration-arquillian/servers/cache-server/jboss/jdg/pom.xml index 8fa5902e02..74b792f150 100644 --- a/testsuite/integration-arquillian/servers/cache-server/jboss/jdg/pom.xml +++ b/testsuite/integration-arquillian/servers/cache-server/jboss/jdg/pom.xml @@ -21,7 +21,7 @@ org.keycloak.testsuite integration-arquillian-servers-cache-server-jboss - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT 4.0.0 diff --git a/testsuite/integration-arquillian/servers/cache-server/jboss/pom.xml b/testsuite/integration-arquillian/servers/cache-server/jboss/pom.xml index 9c2d1f9909..5b0401667f 100644 --- a/testsuite/integration-arquillian/servers/cache-server/jboss/pom.xml +++ b/testsuite/integration-arquillian/servers/cache-server/jboss/pom.xml @@ -21,7 +21,7 @@ org.keycloak.testsuite integration-arquillian-servers-cache-server - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT 4.0.0 diff --git a/testsuite/integration-arquillian/servers/cache-server/pom.xml b/testsuite/integration-arquillian/servers/cache-server/pom.xml index 3f5a5a07ab..3a739e7433 100644 --- a/testsuite/integration-arquillian/servers/cache-server/pom.xml +++ b/testsuite/integration-arquillian/servers/cache-server/pom.xml @@ -21,7 +21,7 @@ org.keycloak.testsuite integration-arquillian-servers - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT 4.0.0 diff --git a/testsuite/integration-arquillian/servers/migration/pom.xml b/testsuite/integration-arquillian/servers/migration/pom.xml index d2bda8101f..a8fd5ab4fd 100644 --- a/testsuite/integration-arquillian/servers/migration/pom.xml +++ b/testsuite/integration-arquillian/servers/migration/pom.xml @@ -21,7 +21,7 @@ org.keycloak.testsuite integration-arquillian-servers - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT 4.0.0 diff --git a/testsuite/integration-arquillian/servers/pom.xml b/testsuite/integration-arquillian/servers/pom.xml index 0b7e899fbe..e7336383a4 100644 --- a/testsuite/integration-arquillian/servers/pom.xml +++ b/testsuite/integration-arquillian/servers/pom.xml @@ -21,7 +21,7 @@ org.keycloak.testsuite integration-arquillian - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT 4.0.0 @@ -47,7 +47,7 @@ 6.2.1.redhat-084 - 9.0.1.Final + 8.4.0.Final-redhat-2 16 diff --git a/testsuite/integration-arquillian/servers/wildfly-balancer/pom.xml b/testsuite/integration-arquillian/servers/wildfly-balancer/pom.xml index dd5f7d55b3..3a7837903c 100644 --- a/testsuite/integration-arquillian/servers/wildfly-balancer/pom.xml +++ b/testsuite/integration-arquillian/servers/wildfly-balancer/pom.xml @@ -21,7 +21,7 @@ org.keycloak.testsuite integration-arquillian-servers - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT 4.0.0 diff --git a/testsuite/integration-arquillian/test-apps/app-profile-jee/pom.xml b/testsuite/integration-arquillian/test-apps/app-profile-jee/pom.xml index d72b490a94..78e40d0a34 100644 --- a/testsuite/integration-arquillian/test-apps/app-profile-jee/pom.xml +++ b/testsuite/integration-arquillian/test-apps/app-profile-jee/pom.xml @@ -5,7 +5,7 @@ org.keycloak.testsuite integration-arquillian-test-apps - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT keycloak-test-app-profile-jee diff --git a/testsuite/integration-arquillian/test-apps/cors/angular-product/pom.xml b/testsuite/integration-arquillian/test-apps/cors/angular-product/pom.xml index f812a9f7d1..adceab1453 100755 --- a/testsuite/integration-arquillian/test-apps/cors/angular-product/pom.xml +++ b/testsuite/integration-arquillian/test-apps/cors/angular-product/pom.xml @@ -21,7 +21,7 @@ org.keycloak.testsuite integration-arquillian-test-apps-cors-parent - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT 4.0.0 diff --git a/testsuite/integration-arquillian/test-apps/cors/angular-product/src/main/webapp/index.html b/testsuite/integration-arquillian/test-apps/cors/angular-product/src/main/webapp/index.html index ed3da59e0d..516ddc145f 100755 --- a/testsuite/integration-arquillian/test-apps/cors/angular-product/src/main/webapp/index.html +++ b/testsuite/integration-arquillian/test-apps/cors/angular-product/src/main/webapp/index.html @@ -97,6 +97,7 @@ +
    {{headers}}
    diff --git a/testsuite/integration-arquillian/test-apps/cors/angular-product/src/main/webapp/js/app.js b/testsuite/integration-arquillian/test-apps/cors/angular-product/src/main/webapp/js/app.js index e09b058244..eb760a987d 100755 --- a/testsuite/integration-arquillian/test-apps/cors/angular-product/src/main/webapp/js/app.js +++ b/testsuite/integration-arquillian/test-apps/cors/angular-product/src/main/webapp/js/app.js @@ -73,9 +73,9 @@ module.controller('GlobalCtrl', function($scope, $http) { $scope.realm = []; $scope.version = []; $scope.reloadData = function() { - $http.get(getAppServerUrl("localhost-db") + "/cors-database/products").success(function(data) { + $http.get(getAppServerUrl("localhost-db") + "/cors-database/products").success(function(data, status, headers, config) { $scope.products = angular.fromJson(data); - + $scope.headers = headers(); }); }; diff --git a/testsuite/integration-arquillian/test-apps/cors/database-service/pom.xml b/testsuite/integration-arquillian/test-apps/cors/database-service/pom.xml index 2f7f167250..d4b2e4e03b 100755 --- a/testsuite/integration-arquillian/test-apps/cors/database-service/pom.xml +++ b/testsuite/integration-arquillian/test-apps/cors/database-service/pom.xml @@ -21,7 +21,7 @@ org.keycloak.testsuite integration-arquillian-test-apps-cors-parent - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT 4.0.0 diff --git a/testsuite/integration-arquillian/test-apps/cors/database-service/src/main/java/org/keycloak/example/oauth/ProductService.java b/testsuite/integration-arquillian/test-apps/cors/database-service/src/main/java/org/keycloak/example/oauth/ProductService.java index 69cb58feaf..321f1effec 100755 --- a/testsuite/integration-arquillian/test-apps/cors/database-service/src/main/java/org/keycloak/example/oauth/ProductService.java +++ b/testsuite/integration-arquillian/test-apps/cors/database-service/src/main/java/org/keycloak/example/oauth/ProductService.java @@ -19,9 +19,11 @@ package org.keycloak.example.oauth; import org.jboss.resteasy.annotations.cache.NoCache; +import javax.servlet.http.HttpServletResponse; import javax.ws.rs.GET; import javax.ws.rs.Path; import javax.ws.rs.Produces; +import javax.ws.rs.core.Context; import java.util.ArrayList; import java.util.List; @@ -31,6 +33,10 @@ import java.util.List; */ @Path("products") public class ProductService { + + @Context + private HttpServletResponse response; + @GET @Produces("application/json") @NoCache @@ -39,6 +45,8 @@ public class ProductService { rtn.add("iphone"); rtn.add("ipad"); rtn.add("ipod"); + + response.addHeader("X-Custom1", "some-value"); return rtn; } } diff --git a/testsuite/integration-arquillian/test-apps/cors/database-service/src/main/webapp/WEB-INF/keycloak.json b/testsuite/integration-arquillian/test-apps/cors/database-service/src/main/webapp/WEB-INF/keycloak.json index 493176dcac..993d69c6d5 100755 --- a/testsuite/integration-arquillian/test-apps/cors/database-service/src/main/webapp/WEB-INF/keycloak.json +++ b/testsuite/integration-arquillian/test-apps/cors/database-service/src/main/webapp/WEB-INF/keycloak.json @@ -5,5 +5,6 @@ "auth-server-url": "http://localhost-auth:8180/auth", "bearer-only" : true, "ssl-required": "external", - "enable-cors": true + "enable-cors": true, + "cors-exposed-headers": "X-Custom1" } diff --git a/testsuite/integration-arquillian/test-apps/cors/pom.xml b/testsuite/integration-arquillian/test-apps/cors/pom.xml index 2265c43389..052505c95a 100644 --- a/testsuite/integration-arquillian/test-apps/cors/pom.xml +++ b/testsuite/integration-arquillian/test-apps/cors/pom.xml @@ -5,7 +5,7 @@ integration-arquillian-test-apps org.keycloak.testsuite - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT 4.0.0 diff --git a/testsuite/integration-arquillian/test-apps/hello-world-authz-service/pom.xml b/testsuite/integration-arquillian/test-apps/hello-world-authz-service/pom.xml index 5c17a5eee2..ccddbb2216 100755 --- a/testsuite/integration-arquillian/test-apps/hello-world-authz-service/pom.xml +++ b/testsuite/integration-arquillian/test-apps/hello-world-authz-service/pom.xml @@ -24,7 +24,7 @@ org.keycloak.testsuite integration-arquillian-test-apps - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT hello-world-authz-service diff --git a/testsuite/integration-arquillian/test-apps/js-console/pom.xml b/testsuite/integration-arquillian/test-apps/js-console/pom.xml index 45799221d3..43e0bc35bf 100755 --- a/testsuite/integration-arquillian/test-apps/js-console/pom.xml +++ b/testsuite/integration-arquillian/test-apps/js-console/pom.xml @@ -21,7 +21,7 @@ org.keycloak.testsuite integration-arquillian-test-apps - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT 4.0.0 diff --git a/testsuite/integration-arquillian/test-apps/js-database/pom.xml b/testsuite/integration-arquillian/test-apps/js-database/pom.xml index 4c85860012..729524ad62 100644 --- a/testsuite/integration-arquillian/test-apps/js-database/pom.xml +++ b/testsuite/integration-arquillian/test-apps/js-database/pom.xml @@ -5,7 +5,7 @@ integration-arquillian-test-apps org.keycloak.testsuite - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT 4.0.0 diff --git a/testsuite/integration-arquillian/test-apps/photoz/photoz-authz-policy/pom.xml b/testsuite/integration-arquillian/test-apps/photoz/photoz-authz-policy/pom.xml index 9ea84a48d2..bb401d2a78 100755 --- a/testsuite/integration-arquillian/test-apps/photoz/photoz-authz-policy/pom.xml +++ b/testsuite/integration-arquillian/test-apps/photoz/photoz-authz-policy/pom.xml @@ -6,7 +6,7 @@ org.keycloak.testsuite integration-arquillian-test-apps-photoz-parent - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../pom.xml diff --git a/testsuite/integration-arquillian/test-apps/photoz/photoz-html5-client/pom.xml b/testsuite/integration-arquillian/test-apps/photoz/photoz-html5-client/pom.xml index ea54b521a6..c1f9d2727f 100755 --- a/testsuite/integration-arquillian/test-apps/photoz/photoz-html5-client/pom.xml +++ b/testsuite/integration-arquillian/test-apps/photoz/photoz-html5-client/pom.xml @@ -5,7 +5,7 @@ org.keycloak.testsuite integration-arquillian-test-apps-photoz-parent - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../pom.xml 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 010eb28ae2..0bd38d9340 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 @@ -6,7 +6,7 @@ org.keycloak.testsuite integration-arquillian-test-apps-photoz-parent - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../pom.xml diff --git a/testsuite/integration-arquillian/test-apps/photoz/pom.xml b/testsuite/integration-arquillian/test-apps/photoz/pom.xml index 40ca55296d..5d5431a40c 100755 --- a/testsuite/integration-arquillian/test-apps/photoz/pom.xml +++ b/testsuite/integration-arquillian/test-apps/photoz/pom.xml @@ -6,7 +6,7 @@ org.keycloak.testsuite integration-arquillian-test-apps - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT integration-arquillian-test-apps-photoz-parent diff --git a/testsuite/integration-arquillian/test-apps/pom.xml b/testsuite/integration-arquillian/test-apps/pom.xml index 913b641692..1afac477af 100644 --- a/testsuite/integration-arquillian/test-apps/pom.xml +++ b/testsuite/integration-arquillian/test-apps/pom.xml @@ -5,7 +5,7 @@ integration-arquillian org.keycloak.testsuite - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT 4.0.0 diff --git a/testsuite/integration-arquillian/test-apps/servlet-authz/pom.xml b/testsuite/integration-arquillian/test-apps/servlet-authz/pom.xml index 41e09b36dc..adeb787049 100755 --- a/testsuite/integration-arquillian/test-apps/servlet-authz/pom.xml +++ b/testsuite/integration-arquillian/test-apps/servlet-authz/pom.xml @@ -6,7 +6,7 @@ org.keycloak.testsuite integration-arquillian-test-apps - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT servlet-authz-app diff --git a/testsuite/integration-arquillian/test-apps/servlet-policy-enforcer/pom.xml b/testsuite/integration-arquillian/test-apps/servlet-policy-enforcer/pom.xml index 6cd4610d4b..2dfa42d6aa 100755 --- a/testsuite/integration-arquillian/test-apps/servlet-policy-enforcer/pom.xml +++ b/testsuite/integration-arquillian/test-apps/servlet-policy-enforcer/pom.xml @@ -24,7 +24,7 @@ org.keycloak.testsuite integration-arquillian-test-apps - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT servlet-policy-enforcer diff --git a/testsuite/integration-arquillian/test-apps/servlets/pom.xml b/testsuite/integration-arquillian/test-apps/servlets/pom.xml index 8ec3f96117..ed876dbcbf 100644 --- a/testsuite/integration-arquillian/test-apps/servlets/pom.xml +++ b/testsuite/integration-arquillian/test-apps/servlets/pom.xml @@ -5,7 +5,7 @@ integration-arquillian-test-apps org.keycloak.testsuite - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT 4.0.0 diff --git a/testsuite/integration-arquillian/test-apps/servlets/src/main/java/org/keycloak/testsuite/adapter/servlet/SendUsernameServlet.java b/testsuite/integration-arquillian/test-apps/servlets/src/main/java/org/keycloak/testsuite/adapter/servlet/SendUsernameServlet.java index f5690a5822..2c0b17d1d2 100755 --- a/testsuite/integration-arquillian/test-apps/servlets/src/main/java/org/keycloak/testsuite/adapter/servlet/SendUsernameServlet.java +++ b/testsuite/integration-arquillian/test-apps/servlets/src/main/java/org/keycloak/testsuite/adapter/servlet/SendUsernameServlet.java @@ -25,6 +25,7 @@ import org.keycloak.adapters.spi.AuthenticationError; import org.keycloak.saml.processing.core.saml.v2.constants.X500SAMLProfileConstants; import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.ws.rs.GET; import javax.ws.rs.POST; @@ -47,7 +48,7 @@ import java.util.List; * @version $Revision: 1 $ */ @Path("/") -public class SendUsernameServlet { +public class SendUsernameServlet extends HttpServlet { private static boolean checkRoles = false; private static SamlAuthenticationError authError; diff --git a/testsuite/integration-arquillian/test-apps/spring-boot-adapter/mvnw b/testsuite/integration-arquillian/test-apps/spring-boot-adapter/mvnw new file mode 100755 index 0000000000..5bf251c077 --- /dev/null +++ b/testsuite/integration-arquillian/test-apps/spring-boot-adapter/mvnw @@ -0,0 +1,225 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Maven2 Start Up Batch script +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# M2_HOME - location of maven2's installed home dir +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ] ; then + + if [ -f /etc/mavenrc ] ; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ] ; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false; +darwin=false; +mingw=false +case "`uname`" in + CYGWIN*) cygwin=true ;; + MINGW*) mingw=true;; + Darwin*) darwin=true + # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home + # See https://developer.apple.com/library/mac/qa/qa1170/_index.html + if [ -z "$JAVA_HOME" ]; then + if [ -x "/usr/libexec/java_home" ]; then + export JAVA_HOME="`/usr/libexec/java_home`" + else + export JAVA_HOME="/Library/Java/Home" + fi + fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=`java-config --jre-home` + fi +fi + +if [ -z "$M2_HOME" ] ; then + ## resolve links - $0 may be a link to maven's home + PRG="$0" + + # need this for relative symlinks + while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG="`dirname "$PRG"`/$link" + fi + done + + saveddir=`pwd` + + M2_HOME=`dirname "$PRG"`/.. + + # make it fully qualified + M2_HOME=`cd "$M2_HOME" && pwd` + + cd "$saveddir" + # echo Using m2 at $M2_HOME +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --unix "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --unix "$CLASSPATH"` +fi + +# For Migwn, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$M2_HOME" ] && + M2_HOME="`(cd "$M2_HOME"; pwd)`" + [ -n "$JAVA_HOME" ] && + JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" + # TODO classpath? +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="`which javac`" + if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=`which readlink` + if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + if $darwin ; then + javaHome="`dirname \"$javaExecutable\"`" + javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + else + javaExecutable="`readlink -f \"$javaExecutable\"`" + fi + javaHome="`dirname \"$javaExecutable\"`" + javaHome=`expr "$javaHome" : '\(.*\)/bin'` + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + else + JAVACMD="`which java`" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + + if [ -z "$1" ] + then + echo "Path not specified to find_maven_basedir" + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=`cd "$wdir/.."; pwd` + fi + # end of workaround + done + echo "${basedir}" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + echo "$(tr -s '\n' ' ' < "$1")" + fi +} + +BASE_DIR=`find_maven_basedir "$(pwd)"` +if [ -z "$BASE_DIR" ]; then + exit 1; +fi + +export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} +echo $MAVEN_PROJECTBASEDIR +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --path --windows "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + [ -n "$MAVEN_PROJECTBASEDIR" ] && + MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` +fi + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +exec "$JAVACMD" \ + $MAVEN_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/testsuite/integration-arquillian/test-apps/spring-boot-adapter/mvnw.cmd b/testsuite/integration-arquillian/test-apps/spring-boot-adapter/mvnw.cmd new file mode 100644 index 0000000000..019bd74d76 --- /dev/null +++ b/testsuite/integration-arquillian/test-apps/spring-boot-adapter/mvnw.cmd @@ -0,0 +1,143 @@ +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Maven2 Start Up Batch script +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM M2_HOME - location of maven2's installed home dir +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" +if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" + +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" +if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%" == "on" pause + +if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% + +exit /B %ERROR_CODE% diff --git a/testsuite/integration-arquillian/test-apps/spring-boot-adapter/pom.xml b/testsuite/integration-arquillian/test-apps/spring-boot-adapter/pom.xml new file mode 100644 index 0000000000..b53481bf75 --- /dev/null +++ b/testsuite/integration-arquillian/test-apps/spring-boot-adapter/pom.xml @@ -0,0 +1,134 @@ + + + 4.0.0 + + org.keycloak + spring-boot-adapter + 0.0.1-SNAPSHOT + jar + + spring-boot-adapter + Spring boot adapter test application + + + org.springframework.boot + spring-boot-starter-parent + 1.5.3.RELEASE + + + + + UTF-8 + UTF-8 + 1.8 + + 3.3.0.CR1-SNAPSHOT + + + + + + org.springframework.boot + spring-boot-starter-thymeleaf + + + + org.springframework.boot + spring-boot-starter-test + test + + + + org.springframework.boot + spring-boot-starter-web + + + + org.keycloak + keycloak-spring-boot-adapter + ${keycloak.version} + + + + + + + spring-boot-adapter-tomcat + + + org.springframework.boot + spring-boot-starter-web + + + org.keycloak + keycloak-tomcat8-adapter + ${keycloak.version} + + + + + + spring-boot-adapter-jetty + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-tomcat + + + + + org.springframework.boot + spring-boot-starter-jetty + + + + org.keycloak + keycloak-jetty94-adapter + ${keycloak.version} + + + + + + spring-boot-adapter-undertow + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-tomcat + + + + + org.springframework.boot + spring-boot-starter-undertow + + + + org.keycloak + keycloak-undertow-adapter + ${keycloak.version} + + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + + diff --git a/testsuite/integration-arquillian/test-apps/spring-boot-adapter/src/main/java/org/keycloak/AdminController.java b/testsuite/integration-arquillian/test-apps/spring-boot-adapter/src/main/java/org/keycloak/AdminController.java new file mode 100644 index 0000000000..3b9ccc4108 --- /dev/null +++ b/testsuite/integration-arquillian/test-apps/spring-boot-adapter/src/main/java/org/keycloak/AdminController.java @@ -0,0 +1,59 @@ +package org.keycloak; + +import java.io.IOException; +import java.util.Map; + +import javax.servlet.http.HttpServletRequest; + +import org.keycloak.adapters.RefreshableKeycloakSecurityContext; +import org.keycloak.common.util.Time; +import org.keycloak.jose.jws.JWSInput; +import org.keycloak.jose.jws.JWSInputException; +import org.keycloak.representations.RefreshToken; +import org.keycloak.util.JsonSerialization; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.util.NumberUtils; +import org.springframework.util.StringUtils; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.context.request.WebRequest; + +@Controller +@RequestMapping(path = "/admin") +public class AdminController { + + @RequestMapping(path = "/TokenServlet", method = RequestMethod.GET) + public String showTokens(WebRequest req, Model model, @RequestParam Map attributes) throws IOException { + String timeOffset = attributes.get("timeOffset"); + if (!StringUtils.isEmpty(timeOffset)) { + int offset; + try { + offset = Integer.parseInt(timeOffset, 10); + } + catch (NumberFormatException e) { + offset = 0; + } + + Time.setOffset(offset); + } + + RefreshableKeycloakSecurityContext ctx = + (RefreshableKeycloakSecurityContext) req.getAttribute(KeycloakSecurityContext.class.getName(), WebRequest.SCOPE_REQUEST); + String accessTokenPretty = JsonSerialization.writeValueAsPrettyString(ctx.getToken()); + RefreshToken refreshToken; + try { + refreshToken = new JWSInput(ctx.getRefreshToken()).readJsonContent(RefreshToken.class); + } catch (JWSInputException e) { + throw new IOException(e); + } + String refreshTokenPretty = JsonSerialization.writeValueAsPrettyString(refreshToken); + + model.addAttribute("accessToken", accessTokenPretty); + model.addAttribute("refreshToken", refreshTokenPretty); + model.addAttribute("accessTokenString", ctx.getTokenString()); + + return "tokens"; + } +} diff --git a/testsuite/integration-arquillian/test-apps/spring-boot-adapter/src/main/java/org/keycloak/SpringBootAdapterApplication.java b/testsuite/integration-arquillian/test-apps/spring-boot-adapter/src/main/java/org/keycloak/SpringBootAdapterApplication.java new file mode 100644 index 0000000000..3833299034 --- /dev/null +++ b/testsuite/integration-arquillian/test-apps/spring-boot-adapter/src/main/java/org/keycloak/SpringBootAdapterApplication.java @@ -0,0 +1,12 @@ +package org.keycloak; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class SpringBootAdapterApplication { + + public static void main(String[] args) { + SpringApplication.run(SpringBootAdapterApplication.class, args); + } +} diff --git a/testsuite/integration-arquillian/test-apps/spring-boot-adapter/src/main/resources/application.properties b/testsuite/integration-arquillian/test-apps/spring-boot-adapter/src/main/resources/application.properties new file mode 100644 index 0000000000..84de1bbcd4 --- /dev/null +++ b/testsuite/integration-arquillian/test-apps/spring-boot-adapter/src/main/resources/application.properties @@ -0,0 +1,12 @@ +server.port=8280 + +keycloak.realm=test +keycloak.auth-server-url=http://localhost:8180/auth +keycloak.ssl-required=external +keycloak.resource=spring-boot-app +keycloak.credentials.secret=e3789ac5-bde6-4957-a7b0-612823dac101 +keycloak.realm-key=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB + +keycloak.security-constraints[0].authRoles[0]=admin +keycloak.security-constraints[0].securityCollections[0].name=Admin zone +keycloak.security-constraints[0].securityCollections[0].patterns[0]=/admin/* \ No newline at end of file diff --git a/testsuite/integration-arquillian/test-apps/spring-boot-adapter/src/main/resources/static/admin/index.html b/testsuite/integration-arquillian/test-apps/spring-boot-adapter/src/main/resources/static/admin/index.html new file mode 100644 index 0000000000..acb47afb71 --- /dev/null +++ b/testsuite/integration-arquillian/test-apps/spring-boot-adapter/src/main/resources/static/admin/index.html @@ -0,0 +1,12 @@ + + + + + springboot admin page + + + +
    You are now admin
    + + + \ No newline at end of file diff --git a/testsuite/integration-arquillian/test-apps/spring-boot-adapter/src/main/resources/static/index.html b/testsuite/integration-arquillian/test-apps/spring-boot-adapter/src/main/resources/static/index.html new file mode 100644 index 0000000000..5ca7303992 --- /dev/null +++ b/testsuite/integration-arquillian/test-apps/spring-boot-adapter/src/main/resources/static/index.html @@ -0,0 +1,12 @@ + + + + + springboot test page + + + +
    Click here to go admin
    + + + \ No newline at end of file diff --git a/testsuite/integration-arquillian/test-apps/spring-boot-adapter/src/main/resources/templates/tokens.html b/testsuite/integration-arquillian/test-apps/spring-boot-adapter/src/main/resources/templates/tokens.html new file mode 100644 index 0000000000..09dee7263d --- /dev/null +++ b/testsuite/integration-arquillian/test-apps/spring-boot-adapter/src/main/resources/templates/tokens.html @@ -0,0 +1,11 @@ + + + + Tokens from spring boot + + + + + + + \ No newline at end of file diff --git a/testsuite/integration-arquillian/test-apps/spring-boot-adapter/src/test/java/org/keycloak/SpringBootAdapterApplicationTests.java b/testsuite/integration-arquillian/test-apps/spring-boot-adapter/src/test/java/org/keycloak/SpringBootAdapterApplicationTests.java new file mode 100644 index 0000000000..8df20da764 --- /dev/null +++ b/testsuite/integration-arquillian/test-apps/spring-boot-adapter/src/test/java/org/keycloak/SpringBootAdapterApplicationTests.java @@ -0,0 +1,16 @@ +package org.keycloak; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.junit4.SpringRunner; + +@RunWith(SpringRunner.class) +@SpringBootTest +public class SpringBootAdapterApplicationTests { + + @Test + public void contextLoads() { + } + +} diff --git a/testsuite/integration-arquillian/test-apps/test-apps-dist/pom.xml b/testsuite/integration-arquillian/test-apps/test-apps-dist/pom.xml index c025ebcf8f..4268d88bd1 100644 --- a/testsuite/integration-arquillian/test-apps/test-apps-dist/pom.xml +++ b/testsuite/integration-arquillian/test-apps/test-apps-dist/pom.xml @@ -5,7 +5,7 @@ integration-arquillian-test-apps org.keycloak.testsuite - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT 4.0.0 diff --git a/testsuite/integration-arquillian/test-utils/pom.xml b/testsuite/integration-arquillian/test-utils/pom.xml deleted file mode 100644 index bf1b9622a2..0000000000 --- a/testsuite/integration-arquillian/test-utils/pom.xml +++ /dev/null @@ -1,34 +0,0 @@ - - - - integration-arquillian - org.keycloak.testsuite - 3.2.0.CR1-SNAPSHOT - - 4.0.0 - - integration-arquillian-test-utils - jar - - Test utils - - - - junit - junit - compile - - - org.jboss.logging - jboss-logging - - - commons-configuration - commons-configuration - 1.10 - - - - \ No newline at end of file diff --git a/testsuite/integration-arquillian/test-utils/src/main/java/org/keycloak/testsuite/util/junit/AggregateResultsReporter.java b/testsuite/integration-arquillian/test-utils/src/main/java/org/keycloak/testsuite/util/junit/AggregateResultsReporter.java deleted file mode 100644 index 0b50603c2b..0000000000 --- a/testsuite/integration-arquillian/test-utils/src/main/java/org/keycloak/testsuite/util/junit/AggregateResultsReporter.java +++ /dev/null @@ -1,277 +0,0 @@ -package org.keycloak.testsuite.util.junit; - -import org.apache.commons.configuration.PropertiesConfiguration; -import org.jboss.logging.Logger; -import org.junit.Ignore; -import org.junit.runner.Description; -import org.junit.runner.Result; -import org.junit.runner.notification.Failure; -import org.junit.runner.notification.RunListener; -import org.w3c.dom.Document; -import org.w3c.dom.Element; - -import javax.xml.parsers.DocumentBuilder; -import javax.xml.parsers.DocumentBuilderFactory; -import javax.xml.parsers.ParserConfigurationException; -import javax.xml.transform.OutputKeys; -import javax.xml.transform.Transformer; -import javax.xml.transform.TransformerConfigurationException; -import javax.xml.transform.TransformerException; -import javax.xml.transform.TransformerFactory; -import javax.xml.transform.dom.DOMSource; -import javax.xml.transform.stream.StreamResult; -import java.io.BufferedWriter; -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.OutputStreamWriter; -import java.io.Writer; -import java.nio.charset.Charset; -import java.util.HashMap; -import java.util.Map; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicLong; -import java.util.concurrent.atomic.AtomicReference; - -/** - * Aggregates jUnit test results into a single report - XML file. - */ -public class AggregateResultsReporter extends RunListener { - - private static final Logger LOGGER = Logger.getLogger(AggregateResultsReporter.class); - - private final Document xml; - private final File reportFile; - private final boolean working; - - private final AtomicInteger tests = new AtomicInteger(0); - private final AtomicInteger errors = new AtomicInteger(0); - private final AtomicInteger failures = new AtomicInteger(0); - private final AtomicInteger ignored = new AtomicInteger(0); - private final AtomicLong suiteStartTime = new AtomicLong(0L); - - private final AtomicReference testsuite = new AtomicReference(); - - private final Map testTimes = new HashMap(); - - public AggregateResultsReporter() { - boolean working = true; - Document xml = null; - try { - xml = createEmptyDocument(); - } catch (ParserConfigurationException ex) { - LOGGER.error("Failed to create XML DOM - reporting will not be done", ex); - working = false; - } - - File reportFile = null; - try { - reportFile = createReportFile(); - } catch (Exception ex) { - LOGGER.error("Failed to create log file - reporting will not be done", ex); - working = false; - } - - this.working = working; - this.xml = xml; - this.reportFile = reportFile; - } - - private Document createEmptyDocument() throws ParserConfigurationException { - DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); - return builder.newDocument(); - } - - private File createReportFile() throws Exception { - String logDirPath = null; - - try { - PropertiesConfiguration config = new PropertiesConfiguration(System.getProperty("testsuite.constants")); - config.setThrowExceptionOnMissing(true); - logDirPath = config.getString("log-dir"); - } catch (Exception e) { - logDirPath = System.getProperty("project.build.directory"); - if (logDirPath == null) { - throw new RuntimeException("Could not determine the path to the log directory."); - } - logDirPath += File.separator + "surefire-reports"; - } - - final File logDir = new File(logDirPath); - logDir.mkdirs(); - - final File reportFile = new File(logDir, "junit-single-report.xml").getAbsoluteFile(); - reportFile.createNewFile(); - - return reportFile; - } - - @Override - public void testRunStarted(Description description) throws Exception { - if (working) { - suiteStartTime.set(System.currentTimeMillis()); - - Element testsuite = xml.createElement("testsuite"); - - if (description.getChildren().size() == 1) { - testsuite.setAttribute("name", safeString(description.getChildren().get(0).getDisplayName())); - } - - xml.appendChild(testsuite); - this.testsuite.set(testsuite); - writeXml(); - } - } - - @Override - public void testStarted(Description description) throws Exception { - if (working) { - testTimes.put(description.getDisplayName(), System.currentTimeMillis()); - } - } - - @Override - public void testFinished(Description description) throws Exception { - if (working) { - if (testTimes.containsKey(description.getDisplayName())) { - testsuite.get().appendChild(createTestCase(description)); - writeXml(); - } - } - } - - @Override - public void testAssumptionFailure(Failure failure) { - if (working) { - ignored.incrementAndGet(); - - Element testcase = createTestCase(failure.getDescription()); - Element skipped = xml.createElement("skipped"); - skipped.setAttribute("message", safeString(failure.getMessage())); - - testcase.appendChild(skipped); - - testsuite.get().appendChild(testcase); - writeXml(); - } - } - - @Override - public void testFailure(Failure failure) throws Exception { - if (working) { - if (failure.getDescription().getMethodName() == null) { - // before class failed - for (Description child : failure.getDescription().getChildren()) { - // mark all methods failed - testFailure(new Failure(child, failure.getException())); - } - } else { - // normal failure - Element testcase = createTestCase(failure.getDescription()); - - Element element; - if (failure.getException() instanceof AssertionError) { - failures.incrementAndGet(); - element = xml.createElement("failure"); - } else { - errors.incrementAndGet(); - element = xml.createElement("error"); - } - - testcase.appendChild(element); - - element.setAttribute("type", safeString(failure.getException().getClass().getName())); - element.setAttribute("message", safeString(failure.getMessage())); - element.appendChild(xml.createCDATASection(safeString(failure.getTrace()))); - - testsuite.get().appendChild(testcase); - writeXml(); - } - } - } - - @Override - public void testIgnored(Description description) throws Exception { - if (working) { - ignored.incrementAndGet(); - - Element testcase = createTestCase(description); - - Element skipped = xml.createElement("skipped"); - skipped.setAttribute("message", safeString(description.getAnnotation(Ignore.class).value())); - - testcase.appendChild(skipped); - - testsuite.get().appendChild(testcase); - writeXml(); - } - } - - @Override - public void testRunFinished(Result result) throws Exception { - if (working) { - writeXml(); - } - } - - private void writeXml() { - Element testsuite = this.testsuite.get(); - - testsuite.setAttribute("tests", Integer.toString(tests.get())); - testsuite.setAttribute("errors", Integer.toString(errors.get())); - testsuite.setAttribute("skipped", Integer.toString(ignored.get())); - testsuite.setAttribute("failures", Integer.toString(failures.get())); - testsuite.setAttribute("time", computeTestTime(suiteStartTime.get())); - - try { - Writer writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(reportFile, false), Charset.forName("UTF-8"))); - try { - Transformer t = TransformerFactory.newInstance().newTransformer(); - t.setOutputProperty(OutputKeys.INDENT, "yes"); - t.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4"); - t.transform(new DOMSource(xml), new StreamResult(writer)); - } catch (TransformerConfigurationException ex) { - LOGGER.error("Misconfigured transformer", ex); - } catch (TransformerException ex) { - LOGGER.error("Unable to save XML file", ex); - } finally { - writer.close(); - } - } catch (IOException ex) { - LOGGER.warn("Unable to open report file", ex); - } - } - - private String computeTestTime(Long startTime) { - if (startTime == null) { - return "0"; - } else { - long amount = System.currentTimeMillis() - startTime; - return String.format("%.3f", amount / 1000F); - } - } - - private Element createTestCase(Description description) { - tests.incrementAndGet(); - - Element testcase = xml.createElement("testcase"); - - testcase.setAttribute("name", safeString(description.getMethodName())); - testcase.setAttribute("classname", safeString(description.getClassName())); - testcase.setAttribute("time", computeTestTime(testTimes.remove(description.getDisplayName()))); - - return testcase; - } - - private String safeString(String input) { - if (input == null) { - return "null"; - } - - return input - // first remove color coding (all of it) - .replaceAll("\u001b\\[\\d+m", "") - // then remove control characters that are not whitespaces - .replaceAll("[\\p{Cntrl}&&[^\\p{Space}]]", ""); - } -} diff --git a/testsuite/integration-arquillian/tests/base/pom.xml b/testsuite/integration-arquillian/tests/base/pom.xml index caa76aa32f..b2c06ad85d 100644 --- a/testsuite/integration-arquillian/tests/base/pom.xml +++ b/testsuite/integration-arquillian/tests/base/pom.xml @@ -21,7 +21,7 @@ org.keycloak.testsuite integration-arquillian-tests - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT 4.0.0 @@ -78,6 +78,11 @@ junit compile + + org.hamcrest + hamcrest-all + compile + org.subethamail subethasmtp @@ -88,6 +93,28 @@ greenmail compile + + + + + + + + + + + + + + + + + + org.testcontainers + testcontainers + 1.2.1 + test + diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/ProfileAssume.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/ProfileAssume.java index ad71d385f0..84b8282a1d 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/ProfileAssume.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/ProfileAssume.java @@ -25,6 +25,10 @@ import org.keycloak.common.Profile; */ public class ProfileAssume { + public static void assumeFeatureEnabled(Profile.Feature feature) { + Assume.assumeTrue("Ignoring test as " + feature.name() + " is not enabled", Profile.isFeatureEnabled(feature)); + } + public static void assumePreview() { Assume.assumeTrue("Ignoring test as community/preview profile is not enabled", !Profile.getName().equals("product")); } diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/Retry.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/Retry.java index 3208f02288..5b15a3f502 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/Retry.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/Retry.java @@ -17,19 +17,61 @@ package org.keycloak.testsuite; +import java.util.function.Supplier; + /** * @author Stian Thorgersen */ public class Retry { - public static void execute(Runnable runnable, int retryCount, long intervalMillis) { + /** + * Runs the given {@code runnable} at most {@code retryCount} times until it passes, + * leaving {@code intervalMillis} milliseconds between the invocations. + * The runnable is reexecuted if it throws a {@link RuntimeException} or {@link AssertionError}. + * @param runnable + * @param attemptsCount Total number of attempts to execute the {@code runnable} + * @param intervalMillis + * @return Index of the first successful invocation, starting from 0. + */ + public static int execute(Runnable runnable, int attemptsCount, long intervalMillis) { + int executionIndex = 0; while (true) { try { runnable.run(); - return; - } catch (RuntimeException e) { - retryCount--; - if (retryCount > 0) { + return executionIndex; + } catch (RuntimeException | AssertionError e) { + attemptsCount--; + executionIndex++; + if (attemptsCount > 0) { + try { + Thread.sleep(intervalMillis); + } catch (InterruptedException ie) { + ie.addSuppressed(e); + throw new RuntimeException(ie); + } + } else { + throw e; + } + } + } + } + + /** + * Runs the given {@code runnable} at most {@code retryCount} times until it passes, + * leaving {@code intervalMillis} milliseconds between the invocations. + * The runnable is reexecuted if it throws a {@link RuntimeException} or {@link AssertionError}. + * @param supplier + * @param attemptsCount Total number of attempts to execute the {@code runnable} + * @param intervalMillis + * @return Value generated by the {@code supplier}. + */ + public static T call(Supplier supplier, int attemptsCount, long intervalMillis) { + while (true) { + try { + return supplier.get(); + } catch (RuntimeException | AssertionError e) { + attemptsCount--; + if (attemptsCount > 0) { try { Thread.sleep(intervalMillis); } catch (InterruptedException ie) { diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/AngularCorsProductTestApp.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/AngularCorsProductTestApp.java index f49023ebe5..cb84089f0f 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/AngularCorsProductTestApp.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/AngularCorsProductTestApp.java @@ -66,6 +66,8 @@ public class AngularCorsProductTestApp extends AbstractPageWithInjectedUrl { @FindBy(id = "output") private WebElement outputArea; + @FindBy(id = "headers") + private WebElement headers; public void reloadData() { reloadDataButton.click(); @@ -99,5 +101,9 @@ public class AngularCorsProductTestApp extends AbstractPageWithInjectedUrl { return outputArea; } + public WebElement getHeaders() { + return headers; + } + } \ No newline at end of file diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/SalesPostServlet.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/SalesPostServlet.java index cd9ea11854..01b8b089f9 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/SalesPostServlet.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/SalesPostServlet.java @@ -27,6 +27,7 @@ import java.net.URL; */ public class SalesPostServlet extends SAMLServlet { public static final String DEPLOYMENT_NAME = "sales-post"; + public static final String CLIENT_NAME = "http://localhost:8081/sales-post/"; @ArquillianResource @OperateOnDeployment(DEPLOYMENT_NAME) diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/AppServerTestEnricher.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/AppServerTestEnricher.java index 92646f4f5e..bc83338dbd 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/AppServerTestEnricher.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/AppServerTestEnricher.java @@ -133,6 +133,14 @@ public class AppServerTestEnricher { return getAppServerQualifier(testClass).contains("tomcat"); } + public static boolean isWASAppServer(Class testClass) { + return getAppServerQualifier(testClass).contains("was"); + } + + public static boolean isWLSAppServer(Class testClass) { + return getAppServerQualifier(testClass).contains("wls"); + } + public static boolean isOSGiAppServer(Class testClass) { String q = getAppServerQualifier(testClass); return q.contains("karaf") || q.contains("fuse"); diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/AuthServerTestEnricher.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/AuthServerTestEnricher.java index 97347d9081..085c2dec5c 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/AuthServerTestEnricher.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/AuthServerTestEnricher.java @@ -81,8 +81,6 @@ public class AuthServerTestEnricher { private static final String AUTH_SERVER_CROSS_DC_PROPERTY = "auth.server.crossdc"; public static final boolean AUTH_SERVER_CROSS_DC = Boolean.parseBoolean(System.getProperty(AUTH_SERVER_CROSS_DC_PROPERTY, "false")); - private static final boolean AUTH_SERVER_UNDERTOW_CLUSTER = Boolean.parseBoolean(System.getProperty("auth.server.undertow.cluster", "false")); - private static final Boolean START_MIGRATION_CONTAINER = "auto".equals(System.getProperty("migration.mode")) || "manual".equals(System.getProperty("migration.mode")); @@ -195,11 +193,6 @@ public class AuthServerTestEnricher { suiteContext.setAuthServerInfo(container); } - // Setup with 2 undertow backend nodes and no loadbalancer. -// if (AUTH_SERVER_UNDERTOW_CLUSTER && suiteContext.getAuthServerInfo() == null && !suiteContext.getAuthServerBackendsInfo().isEmpty()) { -// suiteContext.setAuthServerInfo(suiteContext.getAuthServerBackendsInfo().get(0)); -// } - if (START_MIGRATION_CONTAINER) { // init migratedAuthServerInfo for (ContainerInfo container : suiteContext.getContainers()) { @@ -268,6 +261,8 @@ public class AuthServerTestEnricher { } public void initializeOAuthClient(@Observes(precedence = 3) BeforeClass event) { + // TODO workaround. Check if can be removed + OAuthClient.updateURLs(suiteContext.getAuthServerInfo().getContextRoot().toString()); OAuthClient oAuthClient = new OAuthClient(); oAuthClientProducer.set(oAuthClient); } diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/CacheStatisticsControllerEnricher.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/CacheStatisticsControllerEnricher.java index 4091ca4db4..33af2f2de7 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/CacheStatisticsControllerEnricher.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/CacheStatisticsControllerEnricher.java @@ -33,6 +33,7 @@ import java.util.Set; import org.keycloak.testsuite.arquillian.annotation.JmxInfinispanChannelStatistics; import org.keycloak.testsuite.arquillian.jmx.JmxConnectorRegistry; import org.keycloak.testsuite.arquillian.undertow.KeycloakOnUndertow; +import org.keycloak.testsuite.crossdc.DC; import java.io.NotSerializableException; import java.lang.management.ManagementFactory; import java.util.Objects; @@ -84,7 +85,7 @@ public class CacheStatisticsControllerEnricher implements TestEnricher { ObjectName mbeanName = new ObjectName(String.format( "%s:type=%s,name=\"%s(%s)\",manager=\"%s\",component=%s", - annotation.domain().isEmpty() ? getDefaultDomain(annotation.dcIndex(), annotation.dcNodeIndex()) : InfinispanConnectionProvider.JMX_DOMAIN, + annotation.domain().isEmpty() ? getDefaultDomain(annotation.dc().getDcIndex(), annotation.dcNodeIndex()) : InfinispanConnectionProvider.JMX_DOMAIN, annotation.type(), annotation.cacheName(), annotation.cacheMode(), @@ -98,8 +99,8 @@ public class CacheStatisticsControllerEnricher implements TestEnricher { try { Retry.execute(() -> value.reset(), 2, 150); } catch (RuntimeException ex) { - if (annotation.dcIndex() != -1 && annotation.dcNodeIndex() != -1 - && suiteContext.get().getAuthServerBackendsInfo(annotation.dcIndex()).get(annotation.dcNodeIndex()).isStarted()) { + if (annotation.dc() != DC.UNDEFINED && annotation.dcNodeIndex() != -1 + && suiteContext.get().getAuthServerBackendsInfo(annotation.dc().getDcIndex()).get(annotation.dcNodeIndex()).isStarted()) { LOG.warn("Could not reset statistics for " + mbeanName); } } @@ -113,7 +114,7 @@ public class CacheStatisticsControllerEnricher implements TestEnricher { ObjectName mbeanName = new ObjectName(String.format( "%s:type=%s,cluster=\"%s\"", - annotation.domain().isEmpty() ? getDefaultDomain(annotation.dcIndex(), annotation.dcNodeIndex()) : InfinispanConnectionProvider.JMX_DOMAIN, + annotation.domain().isEmpty() ? getDefaultDomain(annotation.dc().getDcIndex(), annotation.dcNodeIndex()) : InfinispanConnectionProvider.JMX_DOMAIN, annotation.type(), annotation.cluster() )); @@ -124,8 +125,8 @@ public class CacheStatisticsControllerEnricher implements TestEnricher { try { Retry.execute(() -> value.reset(), 2, 150); } catch (RuntimeException ex) { - if (annotation.dcIndex() != -1 && annotation.dcNodeIndex() != -1 - && suiteContext.get().getAuthServerBackendsInfo(annotation.dcIndex()).get(annotation.dcNodeIndex()).isStarted()) { + if (annotation.dc() != DC.UNDEFINED && annotation.dcNodeIndex() != -1 + && suiteContext.get().getAuthServerBackendsInfo(annotation.dc().getDcIndex()).get(annotation.dcNodeIndex()).isStarted()) { LOG.warn("Could not reset statistics for " + mbeanName); } } @@ -170,8 +171,8 @@ public class CacheStatisticsControllerEnricher implements TestEnricher { final String host; final int port; - if (annotation.dcIndex() != -1 && annotation.dcNodeIndex() != -1) { - ContainerInfo node = suiteContext.get().getAuthServerBackendsInfo(annotation.dcIndex()).get(annotation.dcNodeIndex()); + if (annotation.dc() != DC.UNDEFINED && annotation.dcNodeIndex() != -1) { + ContainerInfo node = suiteContext.get().getAuthServerBackendsInfo(annotation.dc().getDcIndex()).get(annotation.dcNodeIndex()); Container container = node.getArquillianContainer(); if (container.getDeployableContainer() instanceof KeycloakOnUndertow) { return ManagementFactory.getPlatformMBeanServer(); @@ -204,8 +205,8 @@ public class CacheStatisticsControllerEnricher implements TestEnricher { final String host; final int port; - if (annotation.dcIndex() != -1 && annotation.dcNodeIndex() != -1) { - ContainerInfo node = suiteContext.get().getAuthServerBackendsInfo(annotation.dcIndex()).get(annotation.dcNodeIndex()); + if (annotation.dc() != DC.UNDEFINED && annotation.dcNodeIndex() != -1) { + ContainerInfo node = suiteContext.get().getAuthServerBackendsInfo(annotation.dc().getDcIndex()).get(annotation.dcNodeIndex()); Container container = node.getArquillianContainer(); if (container.getDeployableContainer() instanceof KeycloakOnUndertow) { return ManagementFactory.getPlatformMBeanServer(); @@ -228,7 +229,10 @@ public class CacheStatisticsControllerEnricher implements TestEnricher { : annotation.managementPort(); } - JMXServiceURL url = new JMXServiceURL("service:jmx:remote+http://" + host + ":" + port); + String jmxUrl = "service:jmx:remote+http://" + host + ":" + port; + LOG.infof("JMX Service URL: %s", jmxUrl); + + JMXServiceURL url = new JMXServiceURL(jmxUrl); JMXConnector jmxc = jmxConnectorRegistry.get().getConnection(url); return jmxc.getMBeanServerConnection(); 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 2f1f8419d8..79b0365878 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 @@ -26,8 +26,15 @@ import org.jboss.arquillian.test.spi.annotation.ClassScoped; import org.jboss.logging.Logger; import org.jboss.logging.Logger.Level; import org.jboss.shrinkwrap.api.Archive; +import org.jboss.shrinkwrap.api.ArchivePath; +import org.jboss.shrinkwrap.api.Filters; +import org.jboss.shrinkwrap.api.Node; +import org.jboss.shrinkwrap.api.asset.ClassAsset; import org.jboss.shrinkwrap.api.asset.StringAsset; import org.jboss.shrinkwrap.api.spec.WebArchive; +import org.jboss.shrinkwrap.resolver.api.maven.Maven; +import org.jboss.shrinkwrap.resolver.api.maven.MavenFormatStage; +import org.jboss.shrinkwrap.resolver.api.maven.MavenResolverSystem; import org.keycloak.adapters.servlet.KeycloakOIDCFilter; import org.keycloak.representations.adapters.config.AdapterConfig; import org.keycloak.testsuite.arquillian.annotation.UseServletFilter; @@ -35,18 +42,29 @@ import org.keycloak.testsuite.util.IOUtil; import org.keycloak.util.JsonSerialization; import org.w3c.dom.Document; import org.w3c.dom.Element; +import org.w3c.dom.NodeList; -import javax.xml.transform.TransformerException; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.List; +import java.util.Map; import static org.keycloak.testsuite.arquillian.AppServerTestEnricher.hasAppServerContainerAnnotation; import static org.keycloak.testsuite.arquillian.AppServerTestEnricher.isRelative; import static org.keycloak.testsuite.arquillian.AppServerTestEnricher.isTomcatAppServer; +import static org.keycloak.testsuite.arquillian.AppServerTestEnricher.isWLSAppServer; +import static org.keycloak.testsuite.arquillian.AppServerTestEnricher.isWASAppServer; import static org.keycloak.testsuite.arquillian.AuthServerTestEnricher.getAuthServerContextRoot; -import static org.keycloak.testsuite.util.IOUtil.*; +import static org.keycloak.testsuite.util.IOUtil.appendChildInDocument; +import static org.keycloak.testsuite.util.IOUtil.documentToString; +import static org.keycloak.testsuite.util.IOUtil.getElementTextContent; +import static org.keycloak.testsuite.util.IOUtil.loadJson; +import static org.keycloak.testsuite.util.IOUtil.loadXML; +import static org.keycloak.testsuite.util.IOUtil.modifyDocElementAttribute; +import static org.keycloak.testsuite.util.IOUtil.modifyDocElementValue; +import static org.keycloak.testsuite.util.IOUtil.removeElementsFromDoc; +import static org.keycloak.testsuite.util.IOUtil.removeNodeByAttributeValue; /** @@ -86,6 +104,21 @@ public class DeploymentArchiveProcessor implements ApplicationArchiveProcessor { // } else { // log.info(testClass.getJavaClass().getSimpleName() + " is not an AdapterTest"); // } + if (isWLSAppServer(testClass.getJavaClass())) { +// { + MavenResolverSystem resolver = Maven.resolver(); + MavenFormatStage dependencies = resolver + .loadPomFromFile("pom.xml") + .importTestDependencies() + .resolve("org.apache.httpcomponents:httpclient") + .withTransitivity(); + + ((WebArchive) archive) + .addAsLibraries(dependencies.asFile()) + .addClass(org.keycloak.testsuite.arquillian.annotation.AppServerContainer.class) + .addClass(org.keycloak.testsuite.arquillian.annotation.UseServletFilter.class); + } + } public static boolean isAdapterTest(TestClass testClass) { @@ -260,11 +293,43 @@ public class DeploymentArchiveProcessor implements ApplicationArchiveProcessor { removeElementsFromDoc(webXmlDoc, "web-app", "login-config"); removeElementsFromDoc(webXmlDoc, "web-app", "security-role"); + if (isWASAppServer(testClass.getJavaClass())) { + removeElementsFromDoc(webXmlDoc, "web-app", "servlet-mapping"); + removeElementsFromDoc(webXmlDoc, "web-app", "servlet"); + } + + if (isWLSAppServer(testClass.getJavaClass())) { + // add tag in case it is missing + NodeList nodes = webXmlDoc.getElementsByTagName("servlet"); + if (nodes.getLength() < 1) { + Element servlet = webXmlDoc.createElement("servlet"); + Element servletName = webXmlDoc.createElement("servlet-name"); + Element servletClass = webXmlDoc.createElement("servlet-class"); + + servletName.setTextContent("javax.ws.rs.core.Application"); + servletClass.setTextContent(getServletClassName(archive)); + + servlet.appendChild(servletName); + servlet.appendChild(servletClass); + + appendChildInDocument(webXmlDoc, "web-app", servlet); + } + } } - archive.add(new StringAsset((documentToString(webXmlDoc))), WEBXML_PATH); } - + + private String getServletClassName(Archive archive) { + + Map content = archive.getContent(Filters.include(".*Servlet.class")); + for (ArchivePath path : content.keySet()) { + ClassAsset asset = (ClassAsset) content.get(path).getAsset(); + return asset.getSource().getName(); + } + + return null; + } + } diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/annotation/JmxInfinispanCacheStatistics.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/annotation/JmxInfinispanCacheStatistics.java index 2dd7bbc230..ef15acf9ea 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/annotation/JmxInfinispanCacheStatistics.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/annotation/JmxInfinispanCacheStatistics.java @@ -19,6 +19,7 @@ package org.keycloak.testsuite.arquillian.annotation; import org.keycloak.testsuite.arquillian.AuthServerTestEnricher; import org.keycloak.testsuite.arquillian.InfinispanStatistics; import org.keycloak.testsuite.arquillian.InfinispanStatistics.Constants; +import org.keycloak.testsuite.crossdc.DC; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -48,7 +49,7 @@ public @interface JmxInfinispanCacheStatistics { // Host address - either given by arrangement of DC ... /** Index of the data center, starting from 0 */ - int dcIndex() default -1; + DC dc() default DC.UNDEFINED; /** Index of the node within data center, starting from 0. Nodes are ordered by arquillian qualifier as per {@link AuthServerTestEnricher} */ int dcNodeIndex() default -1; diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/annotation/JmxInfinispanChannelStatistics.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/annotation/JmxInfinispanChannelStatistics.java index 41e9f20f51..cddb8158f1 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/annotation/JmxInfinispanChannelStatistics.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/annotation/JmxInfinispanChannelStatistics.java @@ -17,6 +17,7 @@ package org.keycloak.testsuite.arquillian.annotation; import org.keycloak.testsuite.arquillian.InfinispanStatistics.Constants; +import org.keycloak.testsuite.crossdc.DC; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -40,7 +41,7 @@ public @interface JmxInfinispanChannelStatistics { // Host address - either given by arrangement of DC ... /** Index of the data center, starting from 0 */ - int dcIndex() default -1; + DC dc() default DC.UNDEFINED; /** Index of the node within data center, starting from 0. Nodes are ordered by arquillian qualifier as per {@link AuthServerTestEnricher} */ int dcNodeIndex() default -1; diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/provider/URLProvider.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/provider/URLProvider.java index e8852bc151..f9d557b3f4 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/provider/URLProvider.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/provider/URLProvider.java @@ -39,6 +39,7 @@ public class URLProvider extends URLResourceProvider { protected final Logger log = Logger.getLogger(this.getClass()); + public static final String BOUND_TO_ALL = "0.0.0.0"; public static final String LOCALHOST_ADDRESS = "127.0.0.1"; public static final String LOCALHOST_HOSTNAME = "localhost"; @@ -59,6 +60,7 @@ public class URLProvider extends URLResourceProvider { if (url != null) { try { url = fixLocalhost(url); + url = fixBoundToAll(url); url = removeTrailingSlash(url); if (appServerSslRequired) { url = fixSsl(url); @@ -111,6 +113,14 @@ public class URLProvider extends URLResourceProvider { return url; } + public URL fixBoundToAll(URL url) throws MalformedURLException { + URL fixedUrl = url; + if (url.getHost().contains(BOUND_TO_ALL)) { + fixedUrl = new URL(fixedUrl.toExternalForm().replace(BOUND_TO_ALL, LOCALHOST_HOSTNAME)); + } + return fixedUrl; + } + public URL fixLocalhost(URL url) throws MalformedURLException { URL fixedUrl = url; if (url.getHost().contains(LOCALHOST_ADDRESS)) { diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/KeycloakTestingClient.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/KeycloakTestingClient.java index 684c383470..533dfbb08f 100755 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/KeycloakTestingClient.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/KeycloakTestingClient.java @@ -101,7 +101,7 @@ public class KeycloakTestingClient { String encoded = SerializationUtil.encode(function); String result = testing(realm != null ? realm : "master").runOnServer(encoded); - if (result != null && !result.isEmpty() && !result.trim().startsWith("{")) { + if (result != null && !result.isEmpty() && result.trim().startsWith("EXCEPTION:")) { Throwable t = SerializationUtil.decodeException(result); if (t instanceof AssertionError) { throw (AssertionError) t; @@ -117,7 +117,7 @@ public class KeycloakTestingClient { String encoded = SerializationUtil.encode(function); String result = testing(realm != null ? realm : "master").runOnServer(encoded); - if (result != null && !result.isEmpty() && !result.trim().startsWith("{")) { + if (result != null && !result.isEmpty() && result.trim().startsWith("EXCEPTION:")) { Throwable t = SerializationUtil.decodeException(result); if (t instanceof AssertionError) { throw (AssertionError) t; diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestingCacheResource.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestingCacheResource.java index 4561c99308..e1aee2a374 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestingCacheResource.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestingCacheResource.java @@ -17,6 +17,7 @@ package org.keycloak.testsuite.client.resources; +import java.util.Map; import java.util.Set; import javax.ws.rs.Consumes; @@ -26,6 +27,9 @@ import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType; +import org.keycloak.testsuite.rest.representation.JGroupsStats; +import org.keycloak.testsuite.rest.representation.RemoteCacheStats; + /** * @author Marek Posolda */ @@ -53,4 +57,20 @@ public interface TestingCacheResource { @Path("/clear") @Consumes(MediaType.TEXT_PLAIN) void clear(); + + @GET + @Path("/jgroups-stats") + @Produces(MediaType.APPLICATION_JSON) + JGroupsStats getJgroupsStats(); + + @GET + @Path("/remote-cache-stats") + @Produces(MediaType.APPLICATION_JSON) + RemoteCacheStats getRemoteCacheStats(); + + @GET + @Path("/remote-cache-last-session-refresh/{user-session-id}") + @Produces(MediaType.APPLICATION_JSON) + int getRemoteCacheLastSessionRefresh(@PathParam("user-session-id") String userSessionId); + } 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 32b05e7b8e..2787c0e82f 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 @@ -192,6 +192,11 @@ public interface TestingResource { @Produces(MediaType.APPLICATION_JSON) void removeExpired(@QueryParam("realm") final String realm); + @GET + @Path("/get-client-sessions-count") + @Produces(MediaType.APPLICATION_JSON) + Integer getClientSessionsCountInUserSession(@QueryParam("realm") final String realmName, @QueryParam("session") final String sessionId); + @Path("/cache/{cache}") TestingCacheResource cache(@PathParam("cache") String cacheName); diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/console/page/fragment/AdminConsoleAlert.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/console/page/fragment/AdminConsoleAlert.java index 9e582d8a04..bc089a94ce 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/console/page/fragment/AdminConsoleAlert.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/console/page/fragment/AdminConsoleAlert.java @@ -16,7 +16,9 @@ */ package org.keycloak.testsuite.console.page.fragment; +import org.jboss.arquillian.graphene.fragment.Root; import org.keycloak.testsuite.page.AbstractAlert; +import org.keycloak.testsuite.util.WaitUtils; import org.openqa.selenium.WebElement; import org.openqa.selenium.support.FindBy; @@ -44,6 +46,9 @@ public class AdminConsoleAlert extends AbstractAlert { public void close() { closeButton.click(); + WaitUtils.pause(500); // Sometimes, when a test is too fast, + // one of the consecutive alerts is not displayed; + // to prevent this we need to slow down a bit } } diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/crossdc/DC.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/crossdc/DC.java new file mode 100644 index 0000000000..1ed8cad64c --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/crossdc/DC.java @@ -0,0 +1,31 @@ +/* + * Copyright 2017 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.crossdc; + +/** + * Identifier of datacentre in the testsuite + * @author hmlnarik + */ +public enum DC { + FIRST, + SECOND, + UNDEFINED; + + public int getDcIndex() { + return ordinal(); + } +} diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/AccountApplicationsPage.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/AccountApplicationsPage.java index 11aac0bd7d..8c647cc0b7 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/AccountApplicationsPage.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/AccountApplicationsPage.java @@ -61,6 +61,14 @@ public class AccountApplicationsPage extends AbstractAccountPage { case 1: currentEntry = new AppEntry(); String client = col.getText(); + WebElement link = null; + try { + link = col.findElement(By.tagName("a")); + String href = link.getAttribute("href"); + currentEntry.setHref(href); + } catch (Exception e) { + //ignore + } table.put(client, currentEntry); break; case 2: @@ -111,6 +119,7 @@ public class AccountApplicationsPage extends AbstractAccountPage { private final List rolesGranted = new ArrayList(); private final List protocolMappersGranted = new ArrayList(); private final List additionalGrants = new ArrayList<>(); + private String href = null; private void addAvailableRole(String role) { rolesAvailable.add(role); @@ -127,6 +136,14 @@ public class AccountApplicationsPage extends AbstractAccountPage { private void addAdditionalGrant(String grant) { additionalGrants.add(grant); } + + public void setHref(String href) { + this.href = href; + } + + public String getHref() { + return this.href; + } public List getRolesGranted() { return rolesGranted; diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/ProceedPage.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/ProceedPage.java new file mode 100644 index 0000000000..97d7c289e0 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/ProceedPage.java @@ -0,0 +1,51 @@ +/* + * Copyright 2017 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.pages; + +import org.openqa.selenium.WebElement; +import org.openqa.selenium.support.FindBy; + +/** + * + * @author hmlnarik + */ +public class ProceedPage extends AbstractPage { + + @FindBy(className = "instruction") + private WebElement infoMessage; + + @FindBy(linkText = "» Click here to proceed") + private WebElement proceedLink; + + public String getInfo() { + return infoMessage.getText(); + } + + public boolean isCurrent() { + return driver.getPageSource().contains("kc-info-message") && proceedLink.isDisplayed(); + } + + @Override + public void open() { + throw new UnsupportedOperationException(); + } + + public void clickProceedLink() { + proceedLink.click(); + } + +} diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/ClientAttributeUpdater.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/updaters/ClientAttributeUpdater.java similarity index 55% rename from testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/ClientAttributeUpdater.java rename to testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/updaters/ClientAttributeUpdater.java index d3effb9d7e..7b82f9b9a7 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/ClientAttributeUpdater.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/updaters/ClientAttributeUpdater.java @@ -1,10 +1,9 @@ -package org.keycloak.testsuite.util; +package org.keycloak.testsuite.updaters; import org.keycloak.admin.client.resource.ClientResource; import org.keycloak.representations.idm.ClientRepresentation; import java.io.Closeable; import java.util.HashMap; -import java.util.Map; /** * @@ -12,14 +11,14 @@ import java.util.Map; */ public class ClientAttributeUpdater { - private final Map originalAttributes = new HashMap<>(); - private final ClientResource clientResource; private final ClientRepresentation rep; + private final ClientRepresentation origRep; public ClientAttributeUpdater(ClientResource clientResource) { this.clientResource = clientResource; + this.origRep = clientResource.toRepresentation(); this.rep = clientResource.toRepresentation(); if (this.rep.getAttributes() == null) { this.rep.setAttributes(new HashMap<>()); @@ -27,29 +26,28 @@ public class ClientAttributeUpdater { } public ClientAttributeUpdater setAttribute(String name, String value) { - if (! originalAttributes.containsKey(name)) { - this.originalAttributes.put(name, this.rep.getAttributes().put(name, value)); - } else { - this.rep.getAttributes().put(name, value); - } + this.rep.getAttributes().put(name, value); return this; } public ClientAttributeUpdater removeAttribute(String name) { - if (! originalAttributes.containsKey(name)) { - this.originalAttributes.put(name, this.rep.getAttributes().put(name, null)); - } else { - this.rep.getAttributes().put(name, null); - } + this.rep.getAttributes().put(name, null); + return this; + } + + public ClientAttributeUpdater setConsentRequired(Boolean consentRequired) { + rep.setConsentRequired(consentRequired); + return this; + } + + public ClientAttributeUpdater setFrontchannelLogout(Boolean frontchannelLogout) { + rep.setFrontchannelLogout(frontchannelLogout); return this; } public Closeable update() { clientResource.update(rep); - return () -> { - rep.getAttributes().putAll(originalAttributes); - clientResource.update(rep); - }; + return () -> clientResource.update(origRep); } } diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/updaters/IdentityProviderAttributeUpdater.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/updaters/IdentityProviderAttributeUpdater.java new file mode 100644 index 0000000000..b8eb487b1a --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/updaters/IdentityProviderAttributeUpdater.java @@ -0,0 +1,55 @@ +package org.keycloak.testsuite.updaters; + +import org.keycloak.admin.client.resource.IdentityProviderResource; +import org.keycloak.representations.idm.IdentityProviderRepresentation; +import java.io.Closeable; +import java.util.HashMap; +import java.util.Map; + +/** + * + * @author hmlnarik + */ +public class IdentityProviderAttributeUpdater { + + private final Map originalAttributes = new HashMap<>(); + + private final IdentityProviderResource identityProviderResource; + + private final IdentityProviderRepresentation rep; + + public IdentityProviderAttributeUpdater(IdentityProviderResource identityProviderResource) { + this.identityProviderResource = identityProviderResource; + this.rep = identityProviderResource.toRepresentation(); + if (this.rep.getConfig() == null) { + this.rep.setConfig(new HashMap<>()); + } + } + + public IdentityProviderAttributeUpdater setAttribute(String name, String value) { + if (! originalAttributes.containsKey(name)) { + this.originalAttributes.put(name, this.rep.getConfig().put(name, value)); + } else { + this.rep.getConfig().put(name, value); + } + return this; + } + + public IdentityProviderAttributeUpdater removeAttribute(String name) { + if (! originalAttributes.containsKey(name)) { + this.originalAttributes.put(name, this.rep.getConfig().put(name, null)); + } else { + this.rep.getConfig().put(name, null); + } + return this; + } + + public Closeable update() { + identityProviderResource.update(rep); + + return () -> { + rep.getConfig().putAll(originalAttributes); + identityProviderResource.update(rep); + }; + } +} diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/RealmAttributeUpdater.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/updaters/RealmAttributeUpdater.java similarity index 97% rename from testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/RealmAttributeUpdater.java rename to testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/updaters/RealmAttributeUpdater.java index 909bfcab18..72c2c4b184 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/RealmAttributeUpdater.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/updaters/RealmAttributeUpdater.java @@ -1,4 +1,4 @@ -package org.keycloak.testsuite.util; +package org.keycloak.testsuite.updaters; import org.keycloak.admin.client.resource.RealmResource; import org.keycloak.representations.idm.RealmRepresentation; diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/SetSystemProperty.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/updaters/SetSystemProperty.java similarity index 97% rename from testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/SetSystemProperty.java rename to testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/updaters/SetSystemProperty.java index 757a385103..9a798b24b5 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/SetSystemProperty.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/updaters/SetSystemProperty.java @@ -15,7 +15,7 @@ * limitations under the License. */ -package org.keycloak.testsuite.util; +package org.keycloak.testsuite.updaters; import java.io.Closeable; diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/GreenMailRule.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/GreenMailRule.java index bc0b7873a5..7ebaa1da57 100755 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/GreenMailRule.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/GreenMailRule.java @@ -43,6 +43,10 @@ public class GreenMailRule extends ExternalResource { greenMail.start(); } + public void credentials(String username, String password) { + greenMail.setUser(username, password); + } + @Override protected void after() { if (greenMail != null) { 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 734a4fcf43..1707ef7d8a 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 @@ -29,7 +29,6 @@ import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import javax.xml.transform.Transformer; -import javax.xml.transform.TransformerConfigurationException; import javax.xml.transform.TransformerException; import javax.xml.transform.TransformerFactory; import javax.xml.transform.dom.DOMSource; diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/KeyUtils.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/KeyUtils.java similarity index 100% rename from testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/KeyUtils.java rename to testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/KeyUtils.java diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/Matchers.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/Matchers.java similarity index 100% rename from testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/Matchers.java rename to testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/Matchers.java 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 207a317e96..e577758ced 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 @@ -50,9 +50,11 @@ import org.keycloak.representations.IDToken; import org.keycloak.representations.RefreshToken; import org.keycloak.representations.idm.KeysMetadataRepresentation; import org.keycloak.testsuite.arquillian.AuthServerTestEnricher; +import org.keycloak.testsuite.arquillian.SuiteContext; import org.keycloak.util.BasicAuthHelper; import org.keycloak.util.JsonSerialization; import org.keycloak.util.TokenUtil; +import com.google.common.base.Charsets; import org.openqa.selenium.By; import org.openqa.selenium.WebDriver; @@ -73,11 +75,23 @@ import java.util.*; * @author Stan Silvert ssilvert@redhat.com (C) 2016 Red Hat Inc. */ public class OAuthClient { - public static final String SERVER_ROOT = AuthServerTestEnricher.getAuthServerContextRoot(); - public static String AUTH_SERVER_ROOT = SERVER_ROOT + "/auth"; - public static final String APP_ROOT = AUTH_SERVER_ROOT + "/realms/master/app"; + public static String SERVER_ROOT; + public static String AUTH_SERVER_ROOT; + public static String APP_ROOT; private static final boolean sslRequired = Boolean.parseBoolean(System.getProperty("auth.server.ssl.required")); + static { + updateURLs(AuthServerTestEnricher.getAuthServerContextRoot()); + } + + // Workaround, but many tests directly use system properties like OAuthClient.AUTH_SERVER_ROOT instead of taking the URL from suite context + public static void updateURLs(String serverRoot) { + SERVER_ROOT = serverRoot; + AUTH_SERVER_ROOT = SERVER_ROOT + "/auth"; + APP_ROOT = AUTH_SERVER_ROOT + "/realms/master/app"; + } + + private Keycloak adminClient; private WebDriver driver; @@ -190,6 +204,7 @@ public class OAuthClient { } public void fillLoginForm(String username, String password) { + WaitUtils.waitForPageToLoad(driver); String src = driver.getPageSource(); try { driver.findElement(By.id("username")).sendKeys(username); @@ -237,8 +252,7 @@ public class OAuthClient { } public AccessTokenResponse doAccessTokenRequest(String code, String password) { - CloseableHttpClient client = newCloseableHttpClient(); - try { + try (CloseableHttpClient client = newCloseableHttpClient()) { HttpPost post = new HttpPost(getAccessTokenUrl()); List parameters = new LinkedList(); @@ -270,12 +284,7 @@ public class OAuthClient { parameters.add(new BasicNameValuePair(OAuth2Constants.CODE_VERIFIER, codeVerifier)); } - UrlEncodedFormEntity formEntity = null; - try { - formEntity = new UrlEncodedFormEntity(parameters, "UTF-8"); - } catch (UnsupportedEncodingException e) { - throw new RuntimeException(e); - } + UrlEncodedFormEntity formEntity = new UrlEncodedFormEntity(parameters, Charsets.UTF_8); post.setEntity(formEntity); try { @@ -283,8 +292,8 @@ public class OAuthClient { } catch (Exception e) { throw new RuntimeException("Failed to retrieve access token", e); } - } finally { - closeClient(client); + } catch (IOException ioe) { + throw new RuntimeException(ioe); } } @@ -297,8 +306,7 @@ public class OAuthClient { } public String introspectTokenWithClientCredential(String clientId, String clientSecret, String tokenType, String tokenToIntrospect) { - CloseableHttpClient client = new DefaultHttpClient(); - try { + try (CloseableHttpClient client = new DefaultHttpClient()) { HttpPost post = new HttpPost(getTokenIntrospectionUrl()); String authorization = BasicAuthHelper.createHeader(clientId, clientSecret); @@ -319,19 +327,16 @@ public class OAuthClient { post.setEntity(formEntity); - try { + try (CloseableHttpResponse response = client.execute(post)) { ByteArrayOutputStream out = new ByteArrayOutputStream(); - CloseableHttpResponse response = client.execute(post); response.getEntity().writeTo(out); - response.close(); - return new String(out.toByteArray()); } catch (Exception e) { throw new RuntimeException("Failed to retrieve access token", e); } - } finally { - closeClient(client); + } catch (IOException ioe) { + throw new RuntimeException(ioe); } } @@ -389,6 +394,51 @@ public class OAuthClient { } } + public AccessTokenResponse doTokenExchange(String realm, String token, String targetAudience, + String clientId, String clientSecret) throws Exception { + CloseableHttpClient client = newCloseableHttpClient(); + try { + HttpPost post = new HttpPost(getResourceOwnerPasswordCredentialGrantUrl(realm)); + + List parameters = new LinkedList(); + parameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.TOKEN_EXCHANGE_GRANT_TYPE)); + parameters.add(new BasicNameValuePair(OAuth2Constants.SUBJECT_TOKEN, token)); + parameters.add(new BasicNameValuePair(OAuth2Constants.SUBJECT_TOKEN_TYPE, OAuth2Constants.ACCESS_TOKEN_TYPE)); + parameters.add(new BasicNameValuePair(OAuth2Constants.AUDIENCE, targetAudience)); + + if (clientSecret != null) { + String authorization = BasicAuthHelper.createHeader(clientId, clientSecret); + post.setHeader("Authorization", authorization); + } else { + parameters.add(new BasicNameValuePair("client_id", clientId)); + + } + + if (clientSessionState != null) { + parameters.add(new BasicNameValuePair(AdapterConstants.CLIENT_SESSION_STATE, clientSessionState)); + } + if (clientSessionHost != null) { + parameters.add(new BasicNameValuePair(AdapterConstants.CLIENT_SESSION_HOST, clientSessionHost)); + } + if (scope != null) { + parameters.add(new BasicNameValuePair(OAuth2Constants.SCOPE, scope)); + } + + UrlEncodedFormEntity formEntity; + try { + formEntity = new UrlEncodedFormEntity(parameters, "UTF-8"); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e); + } + post.setEntity(formEntity); + + return new AccessTokenResponse(client.execute(post)); + } finally { + closeClient(client); + } + } + + public JSONWebKeySet doCertsRequest(String realm) throws Exception { CloseableHttpClient client = new DefaultHttpClient(); try { diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/SamlClient.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/SamlClient.java new file mode 100644 index 0000000000..06609b1f63 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/SamlClient.java @@ -0,0 +1,349 @@ +/* + * Copyright 2017 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.util; + +import org.apache.http.NameValuePair; +import org.apache.http.client.entity.UrlEncodedFormEntity; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.methods.HttpUriRequest; +import org.apache.http.client.protocol.HttpClientContext; +import org.apache.http.client.utils.URLEncodedUtils; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.impl.client.LaxRedirectStrategy; +import org.apache.http.message.BasicNameValuePair; +import org.apache.http.util.EntityUtils; +import org.jsoup.Jsoup; +import org.jsoup.nodes.Element; +import org.jsoup.select.Elements; +import org.keycloak.common.util.KeyUtils; +import org.keycloak.dom.saml.v2.protocol.AuthnRequestType; +import org.keycloak.saml.BaseSAML2BindingBuilder; +import org.keycloak.saml.SAMLRequestParser; +import org.keycloak.saml.SignatureAlgorithm; +import org.keycloak.saml.common.constants.GeneralConstants; +import org.keycloak.saml.common.constants.JBossSAMLURIConstants; +import org.keycloak.saml.common.exceptions.ConfigurationException; +import org.keycloak.saml.common.exceptions.ProcessingException; +import org.keycloak.saml.processing.api.saml.v2.request.SAML2Request; +import org.keycloak.saml.processing.core.saml.v2.common.SAMLDocumentHolder; +import org.w3c.dom.Document; + +import javax.ws.rs.core.Response; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.URI; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.util.LinkedList; +import java.util.List; +import java.util.UUID; + +import org.jboss.logging.Logger; +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.assertThat; +import static org.keycloak.testsuite.util.Matchers.statusCodeIsHC; + +/** + * @author hmlnarik + */ +public class SamlClient { + + @FunctionalInterface + public interface Step { + HttpUriRequest perform(CloseableHttpClient client, URI currentURI, CloseableHttpResponse currentResponse, HttpClientContext context) throws Exception; + } + + @FunctionalInterface + public interface ResultExtractor { + T extract(CloseableHttpResponse response) throws Exception; + } + + public static final class DoNotFollowRedirectStep implements Step { + + @Override + public HttpUriRequest perform(CloseableHttpClient client, URI uri, CloseableHttpResponse response, HttpClientContext context) throws Exception { + return null; + } + } + + public static class RedirectStrategyWithSwitchableFollowRedirect extends LaxRedirectStrategy { + + public boolean redirectable = true; + + @Override + protected boolean isRedirectable(String method) { + return redirectable && super.isRedirectable(method); + } + + public void setRedirectable(boolean redirectable) { + this.redirectable = redirectable; + } + } + + /** + * SAML bindings and related HttpClient methods. + */ + public enum Binding { + POST { + @Override + public SAMLDocumentHolder extractResponse(CloseableHttpResponse response) throws IOException { + assertThat(response, statusCodeIsHC(Response.Status.OK)); + String responsePage = EntityUtils.toString(response.getEntity(), "UTF-8"); + response.close(); + return extractSamlResponseFromForm(responsePage); + } + + @Override + public HttpPost createSamlUnsignedRequest(URI samlEndpoint, String relayState, Document samlRequest) { + return createSamlPostMessage(samlEndpoint, relayState, samlRequest, GeneralConstants.SAML_REQUEST_KEY, null, null); + } + + @Override + public HttpPost createSamlUnsignedResponse(URI samlEndpoint, String relayState, Document samlRequest) { + return createSamlPostMessage(samlEndpoint, relayState, samlRequest, GeneralConstants.SAML_RESPONSE_KEY, null, null); + } + + @Override + public HttpPost createSamlSignedRequest(URI samlEndpoint, String relayState, Document samlRequest, String realmPrivateKey, String realmPublicKey) { + return createSamlPostMessage(samlEndpoint, relayState, samlRequest, GeneralConstants.SAML_REQUEST_KEY, realmPrivateKey, realmPublicKey); + } + + private HttpPost createSamlPostMessage(URI samlEndpoint, String relayState, Document samlRequest, String messageType, String privateKeyStr, String publicKeyStr) { + HttpPost post = new HttpPost(samlEndpoint); + + List parameters = new LinkedList<>(); + + + try { + BaseSAML2BindingBuilder binding = new BaseSAML2BindingBuilder(); + + if (privateKeyStr != null && publicKeyStr != null) { + PrivateKey privateKey = org.keycloak.testsuite.util.KeyUtils.privateKeyFromString(privateKeyStr); + PublicKey publicKey = org.keycloak.testsuite.util.KeyUtils.publicKeyFromString(publicKeyStr); + binding + .signatureAlgorithm(SignatureAlgorithm.RSA_SHA256) + .signWith(KeyUtils.createKeyId(privateKey), privateKey, publicKey) + .signDocument(); + } + + parameters.add( + new BasicNameValuePair(messageType, + binding + .postBinding(samlRequest) + .encoded()) + ); + } catch (IOException | ConfigurationException | ProcessingException ex) { + throw new RuntimeException(ex); + } + + if (relayState != null) { + parameters.add(new BasicNameValuePair(GeneralConstants.RELAY_STATE, relayState)); + } + + UrlEncodedFormEntity formEntity; + + try { + formEntity = new UrlEncodedFormEntity(parameters, "UTF-8"); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e); + } + + post.setEntity(formEntity); + + return post; + } + + @Override + public URI getBindingUri() { + return URI.create(JBossSAMLURIConstants.SAML_HTTP_POST_BINDING.get()); + } + }, + + REDIRECT { + @Override + public SAMLDocumentHolder extractResponse(CloseableHttpResponse response) throws IOException { + assertThat(response, statusCodeIsHC(Response.Status.FOUND)); + String location = response.getFirstHeader("Location").getValue(); + response.close(); + return extractSamlResponseFromRedirect(location); + } + + @Override + public HttpGet createSamlUnsignedRequest(URI samlEndpoint, String relayState, Document samlRequest) { + try { + URI requestURI = new BaseSAML2BindingBuilder() + .relayState(relayState) + .redirectBinding(samlRequest) + .requestURI(samlEndpoint.toString()); + return new HttpGet(requestURI); + } catch (ProcessingException | ConfigurationException | IOException ex) { + throw new RuntimeException(ex); + } + } + + @Override + public URI getBindingUri() { + return URI.create(JBossSAMLURIConstants.SAML_HTTP_REDIRECT_BINDING.get()); + } + + @Override + public HttpUriRequest createSamlUnsignedResponse(URI samlEndpoint, String relayState, Document samlRequest) { + return null; + } + + @Override + public HttpUriRequest createSamlSignedRequest(URI samlEndpoint, String relayState, Document samlRequest, String realmPrivateKey, String realmPublicKey) { + return null; + } + }; + + public abstract SAMLDocumentHolder extractResponse(CloseableHttpResponse response) throws IOException; + + public abstract HttpUriRequest createSamlUnsignedRequest(URI samlEndpoint, String relayState, Document samlRequest); + + public abstract HttpUriRequest createSamlSignedRequest(URI samlEndpoint, String relayState, Document samlRequest, String realmPrivateKey, String realmPublicKey); + + public abstract URI getBindingUri(); + + public abstract HttpUriRequest createSamlUnsignedResponse(URI samlEndpoint, String relayState, Document samlRequest); + } + + private static final Logger LOG = Logger.getLogger(SamlClient.class); + + private final HttpClientContext context = HttpClientContext.create(); + + private final RedirectStrategyWithSwitchableFollowRedirect strategy = new RedirectStrategyWithSwitchableFollowRedirect(); + + /** + * Extracts and parses value of SAMLResponse input field of a form present in the given page. + * + * @param responsePage HTML code of the page + * @return + */ + public static SAMLDocumentHolder extractSamlResponseFromForm(String responsePage) { + org.jsoup.nodes.Document theResponsePage = Jsoup.parse(responsePage); + Elements samlResponses = theResponsePage.select("input[name=SAMLResponse]"); + Elements samlRequests = theResponsePage.select("input[name=SAMLRequest]"); + int size = samlResponses.size() + samlRequests.size(); + assertThat("Checking uniqueness of SAMLResponse/SAMLRequest input field in the page", size, is(1)); + + Element respElement = samlResponses.isEmpty() ? samlRequests.first() : samlResponses.first(); + + return SAMLRequestParser.parseResponsePostBinding(respElement.val()); + } + + /** + * Extracts and parses value of SAMLResponse query parameter from the given URI. + * + * @param responseUri + * @return + */ + public static SAMLDocumentHolder extractSamlResponseFromRedirect(String responseUri) { + List params = URLEncodedUtils.parse(URI.create(responseUri), "UTF-8"); + + String samlDoc = null; + for (NameValuePair param : params) { + if ("SAMLResponse".equals(param.getName()) || "SAMLRequest".equals(param.getName())) { + assertThat("Only one SAMLRequest/SAMLResponse check", samlDoc, nullValue()); + samlDoc = param.getValue(); + } + } + + return SAMLRequestParser.parseResponseRedirectBinding(samlDoc); + } + + /** + * Creates a SAML login request document with the given parameters. See SAML <AuthnRequest> description for more details. + * + * @param issuer + * @param assertionConsumerURL + * @param destination + * @return + */ + public static AuthnRequestType createLoginRequestDocument(String issuer, String assertionConsumerURL, URI destination) { + try { + SAML2Request samlReq = new SAML2Request(); + AuthnRequestType loginReq = samlReq.createAuthnRequestType(UUID.randomUUID().toString(), assertionConsumerURL, destination.toString(), issuer); + + return loginReq; + } catch (ConfigurationException ex) { + throw new RuntimeException(ex); + } + } + + public T executeAndTransform(ResultExtractor resultTransformer, List steps) { + CloseableHttpResponse currentResponse = null; + URI currentUri = URI.create("about:blank"); + strategy.setRedirectable(true); + + try (CloseableHttpClient client = createHttpClientBuilderInstance().setRedirectStrategy(strategy).build()) { + for (int i = 0; i < steps.size(); i ++) { + Step s = steps.get(i); + LOG.infof("Running step %d: %s", i, s.getClass()); + + CloseableHttpResponse origResponse = currentResponse; + + HttpUriRequest request = s.perform(client, currentUri, origResponse, context); + if (request == null) { + LOG.info("Last step returned no request, continuing with next step."); + continue; + } + + // Setting of follow redirects has to be set before executing the final request of the current step + if (i < steps.size() - 1 && steps.get(i + 1) instanceof DoNotFollowRedirectStep) { + LOG.debugf("Disabling following redirects"); + strategy.setRedirectable(false); + i++; + } else { + strategy.setRedirectable(true); + } + + LOG.infof("Executing HTTP request to %s", request.getURI()); + currentResponse = client.execute(request, context); + + currentUri = request.getURI(); + List locations = context.getRedirectLocations(); + if (locations != null && ! locations.isEmpty()) { + currentUri = locations.get(locations.size() - 1); + } + + LOG.infof("Landed to %s", currentUri); + + if (currentResponse != origResponse && origResponse != null) { + origResponse.close(); + } + } + + LOG.info("Going to extract response"); + + return resultTransformer.extract(currentResponse); + } catch (Exception ex) { + throw new RuntimeException(ex); + } + } + + public HttpClientContext getContext() { + return context; + } + + protected HttpClientBuilder createHttpClientBuilderInstance() { + return HttpClientBuilder.create(); + } +} diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/SamlClientBuilder.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/SamlClientBuilder.java new file mode 100644 index 0000000000..89d309249c --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/SamlClientBuilder.java @@ -0,0 +1,139 @@ +/* + * Copyright 2017 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.util; + +import org.keycloak.saml.processing.core.saml.v2.common.SAMLDocumentHolder; +import org.keycloak.testsuite.util.SamlClient.Binding; +import org.keycloak.testsuite.util.SamlClient.DoNotFollowRedirectStep; +import org.keycloak.testsuite.util.SamlClient.ResultExtractor; +import org.keycloak.testsuite.util.SamlClient.Step; +import java.net.URI; +import java.util.LinkedList; +import java.util.List; +import java.util.function.Consumer; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.keycloak.testsuite.util.saml.CreateAuthnRequestStepBuilder; +import org.keycloak.testsuite.util.saml.CreateLogoutRequestStepBuilder; +import org.keycloak.testsuite.util.saml.IdPInitiatedLoginBuilder; +import org.keycloak.testsuite.util.saml.LoginBuilder; +import org.keycloak.testsuite.util.saml.ModifySamlResponseStepBuilder; +import org.keycloak.testsuite.util.saml.RequiredConsentBuilder; +import org.w3c.dom.Document; + +/** + * + * @author hmlnarik + */ +public class SamlClientBuilder { + + private final List steps = new LinkedList<>(); + + public SamlClient execute(Consumer resultConsumer) { + final SamlClient samlClient = new SamlClient(); + samlClient.executeAndTransform(r -> { + resultConsumer.accept(r); + return null; + }, steps); + return samlClient; + } + + public T executeAndTransform(ResultExtractor resultTransformer) { + return new SamlClient().executeAndTransform(resultTransformer, steps); + } + + public List getSteps() { + return steps; + } + + public T addStep(T step) { + steps.add(step); + return step; + } + + public SamlClientBuilder doNotFollowRedirects() { + this.steps.add(new DoNotFollowRedirectStep()); + return this; + } + + public SamlClientBuilder clearCookies() { + this.steps.add((client, currentURI, currentResponse, context) -> { + context.getCookieStore().clear(); + return null; + }); + return this; + } + + /** Creates fresh and issues an AuthnRequest to the SAML endpoint */ + public CreateAuthnRequestStepBuilder authnRequest(URI authServerSamlUrl, String issuer, String assertionConsumerURL, Binding requestBinding) { + return addStep(new CreateAuthnRequestStepBuilder(authServerSamlUrl, issuer, assertionConsumerURL, requestBinding, this)); + } + + /** Issues the given AuthnRequest to the SAML endpoint */ + public CreateAuthnRequestStepBuilder authnRequest(URI authServerSamlUrl, Document authnRequestDocument, Binding requestBinding) { + return addStep(new CreateAuthnRequestStepBuilder(authServerSamlUrl, authnRequestDocument, requestBinding, this)); + } + + /** Issues the given AuthnRequest to the SAML endpoint */ + public CreateLogoutRequestStepBuilder logoutRequest(URI authServerSamlUrl, String issuer, Binding requestBinding) { + return addStep(new CreateLogoutRequestStepBuilder(authServerSamlUrl, issuer, requestBinding, this)); + } + + /** Handles login page */ + public LoginBuilder login() { + return addStep(new LoginBuilder(this)); + } + + /** Starts IdP-initiated flow for the given client */ + public IdPInitiatedLoginBuilder idpInitiatedLogin(URI authServerSamlUrl, String clientId) { + return addStep(new IdPInitiatedLoginBuilder(authServerSamlUrl, clientId, this)); + } + + /** Handles "Requires consent" page */ + public RequiredConsentBuilder consentRequired() { + return addStep(new RequiredConsentBuilder(this)); + } + + /** Returns SAML request or response as replied from server. Note that the redirects are disabled for this to work. */ + public SAMLDocumentHolder getSamlResponse(Binding responseBinding) { + return + doNotFollowRedirects() + .executeAndTransform(responseBinding::extractResponse); + } + + /** Returns SAML request or response as replied from server. Note that the redirects are disabled for this to work. */ + public ModifySamlResponseStepBuilder processSamlResponse(Binding responseBinding) { + return + doNotFollowRedirects() + .addStep(new ModifySamlResponseStepBuilder(responseBinding, this)); + } + + public SamlClientBuilder navigateTo(String httpGetUri) { + steps.add((client, currentURI, currentResponse, context) -> { + return new HttpGet(httpGetUri); + }); + return this; + } + + public SamlClientBuilder navigateTo(URI httpGetUri) { + steps.add((client, currentURI, currentResponse, context) -> { + return new HttpGet(httpGetUri); + }); + return this; + } + +} diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/matchers/HttpResponseBodyMatcher.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/matchers/HttpResponseBodyMatcher.java similarity index 100% rename from testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/matchers/HttpResponseBodyMatcher.java rename to testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/matchers/HttpResponseBodyMatcher.java diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/matchers/HttpResponseStatusCodeMatcher.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/matchers/HttpResponseStatusCodeMatcher.java similarity index 100% rename from testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/matchers/HttpResponseStatusCodeMatcher.java rename to testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/matchers/HttpResponseStatusCodeMatcher.java diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/matchers/ResponseBodyMatcher.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/matchers/ResponseBodyMatcher.java similarity index 100% rename from testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/matchers/ResponseBodyMatcher.java rename to testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/matchers/ResponseBodyMatcher.java diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/matchers/ResponseHeaderMatcher.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/matchers/ResponseHeaderMatcher.java similarity index 100% rename from testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/matchers/ResponseHeaderMatcher.java rename to testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/matchers/ResponseHeaderMatcher.java diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/matchers/ResponseStatusCodeMatcher.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/matchers/ResponseStatusCodeMatcher.java similarity index 100% rename from testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/matchers/ResponseStatusCodeMatcher.java rename to testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/matchers/ResponseStatusCodeMatcher.java diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/matchers/SamlLogoutRequestTypeMatcher.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/matchers/SamlLogoutRequestTypeMatcher.java similarity index 100% rename from testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/matchers/SamlLogoutRequestTypeMatcher.java rename to testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/matchers/SamlLogoutRequestTypeMatcher.java diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/matchers/SamlResponseTypeMatcher.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/matchers/SamlResponseTypeMatcher.java similarity index 100% rename from testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/matchers/SamlResponseTypeMatcher.java rename to testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/matchers/SamlResponseTypeMatcher.java diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/matchers/SamlStatusResponseTypeMatcher.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/matchers/SamlStatusResponseTypeMatcher.java similarity index 95% rename from testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/matchers/SamlStatusResponseTypeMatcher.java rename to testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/matchers/SamlStatusResponseTypeMatcher.java index ccd5377865..d76a6dd56c 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/matchers/SamlStatusResponseTypeMatcher.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/matchers/SamlStatusResponseTypeMatcher.java @@ -6,7 +6,6 @@ package org.keycloak.testsuite.util.matchers; import org.keycloak.dom.saml.v2.SAML2Object; -import org.keycloak.dom.saml.v2.protocol.LogoutRequestType; import org.keycloak.dom.saml.v2.protocol.StatusResponseType; import java.net.URI; import org.hamcrest.*; diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/saml/CreateAuthnRequestStepBuilder.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/saml/CreateAuthnRequestStepBuilder.java new file mode 100644 index 0000000000..aa85821179 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/saml/CreateAuthnRequestStepBuilder.java @@ -0,0 +1,108 @@ +/* + * Copyright 2017 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.util.saml; + +import org.keycloak.testsuite.util.SamlClientBuilder; +import org.keycloak.dom.saml.v2.protocol.AuthnRequestType; +import org.keycloak.saml.common.exceptions.ConfigurationException; +import org.keycloak.saml.common.exceptions.ParsingException; +import org.keycloak.saml.common.exceptions.ProcessingException; +import org.keycloak.saml.common.util.DocumentUtil; +import org.keycloak.saml.processing.api.saml.v2.request.SAML2Request; +import org.keycloak.testsuite.util.SamlClient.Binding; +import java.net.URI; +import java.util.UUID; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpUriRequest; +import org.apache.http.client.protocol.HttpClientContext; +import org.apache.http.impl.client.CloseableHttpClient; +import org.w3c.dom.Document; + + +public class CreateAuthnRequestStepBuilder extends SamlDocumentStepBuilder { + + private final String issuer; + private final URI authServerSamlUrl; + private final Binding requestBinding; + private final String assertionConsumerURL; + + private final Document forceLoginRequestDocument; + + private String relayState; + + public CreateAuthnRequestStepBuilder(URI authServerSamlUrl, String issuer, String assertionConsumerURL, Binding requestBinding, SamlClientBuilder clientBuilder) { + super(clientBuilder); + this.issuer = issuer; + this.authServerSamlUrl = authServerSamlUrl; + this.requestBinding = requestBinding; + this.assertionConsumerURL = assertionConsumerURL; + + this.forceLoginRequestDocument = null; + } + + public CreateAuthnRequestStepBuilder(URI authServerSamlUrl, Document loginRequestDocument, Binding requestBinding, SamlClientBuilder clientBuilder) { + super(clientBuilder); + this.forceLoginRequestDocument = loginRequestDocument; + + this.authServerSamlUrl = authServerSamlUrl; + this.requestBinding = requestBinding; + + this.issuer = null; + this.assertionConsumerURL = null; + } + + public String assertionConsumerURL() { + return assertionConsumerURL; + } + + public String relayState() { + return relayState; + } + + public void relayState(String relayState) { + this.relayState = relayState; + } + + @Override + public HttpUriRequest perform(CloseableHttpClient client, URI currentURI, CloseableHttpResponse currentResponse, HttpClientContext context) throws Exception { + Document doc = createLoginRequestDocument(); + + String documentAsString = DocumentUtil.getDocumentAsString(doc); + String transformed = getTransformer().transform(documentAsString); + + if (transformed == null) { + return null; + } + + return requestBinding.createSamlUnsignedRequest(authServerSamlUrl, relayState, DocumentUtil.getDocument(transformed)); + } + + protected Document createLoginRequestDocument() { + if (this.forceLoginRequestDocument != null) { + return this.forceLoginRequestDocument; + } + + try { + SAML2Request samlReq = new SAML2Request(); + AuthnRequestType loginReq = samlReq.createAuthnRequestType(UUID.randomUUID().toString(), assertionConsumerURL, this.authServerSamlUrl.toString(), issuer); + + return SAML2Request.convert(loginReq); + } catch (ConfigurationException | ParsingException | ProcessingException ex) { + throw new RuntimeException(ex); + } + } +} diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/saml/CreateLogoutRequestStepBuilder.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/saml/CreateLogoutRequestStepBuilder.java new file mode 100644 index 0000000000..ee594d0a22 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/saml/CreateLogoutRequestStepBuilder.java @@ -0,0 +1,116 @@ +/* + * Copyright 2017 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.util.saml; + +import org.keycloak.testsuite.util.SamlClientBuilder; +import org.keycloak.dom.saml.v2.assertion.NameIDType; +import org.keycloak.dom.saml.v2.protocol.LogoutRequestType; +import org.keycloak.saml.SAML2LogoutRequestBuilder; +import org.keycloak.saml.common.util.DocumentUtil; +import org.keycloak.testsuite.util.SamlClient.Binding; +import java.net.URI; +import java.util.function.Supplier; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpUriRequest; +import org.apache.http.client.protocol.HttpClientContext; +import org.apache.http.impl.client.CloseableHttpClient; + +/** + * + * @author hmlnarik + */ +public class CreateLogoutRequestStepBuilder extends SamlDocumentStepBuilder { + + private final URI authServerSamlUrl; + private final String issuer; + private final Binding requestBinding; + + private Supplier sessionIndex = () -> null; + private Supplier nameId = () -> null; + private Supplier relayState = () -> null; + + public CreateLogoutRequestStepBuilder(URI authServerSamlUrl, String issuer, Binding requestBinding, SamlClientBuilder clientBuilder) { + super(clientBuilder); + this.authServerSamlUrl = authServerSamlUrl; + this.issuer = issuer; + this.requestBinding = requestBinding; + } + + public String sessionIndex() { + return sessionIndex.get(); + } + + public CreateLogoutRequestStepBuilder sessionIndex(String sessionIndex) { + this.sessionIndex = () -> sessionIndex; + return this; + } + + public CreateLogoutRequestStepBuilder sessionIndex(Supplier sessionIndex) { + this.sessionIndex = sessionIndex; + return this; + } + + public String relayState() { + return relayState.get(); + } + + public CreateLogoutRequestStepBuilder relayState(String relayState) { + this.relayState = () -> relayState; + return this; + } + + public CreateLogoutRequestStepBuilder relayState(Supplier relayState) { + this.relayState = relayState; + return this; + } + + public NameIDType nameId() { + return nameId.get(); + } + + public CreateLogoutRequestStepBuilder nameId(NameIDType nameId) { + this.nameId = () -> nameId; + return this; + } + + public CreateLogoutRequestStepBuilder nameId(Supplier nameId) { + this.nameId = nameId; + return this; + } + + @Override + public HttpUriRequest perform(CloseableHttpClient client, URI currentURI, CloseableHttpResponse currentResponse, HttpClientContext context) throws Exception { + SAML2LogoutRequestBuilder builder = new SAML2LogoutRequestBuilder() + .destination(authServerSamlUrl.toString()) + .issuer(issuer) + .sessionIndex(sessionIndex()); + + if (nameId() != null) { + builder = builder.userPrincipal(nameId().getValue(), nameId().getFormat().toString()); + } + + String documentAsString = DocumentUtil.getDocumentAsString(builder.buildDocument()); + String transformed = getTransformer().transform(documentAsString); + + if (transformed == null) { + return null; + } + + return requestBinding.createSamlUnsignedRequest(authServerSamlUrl, relayState(), DocumentUtil.getDocument(transformed)); + } + +} diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/saml/IdPInitiatedLoginBuilder.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/saml/IdPInitiatedLoginBuilder.java new file mode 100644 index 0000000000..d119e64772 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/saml/IdPInitiatedLoginBuilder.java @@ -0,0 +1,52 @@ +/* + * Copyright 2017 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.util.saml; + +import org.keycloak.testsuite.util.SamlClient.Step; +import org.keycloak.testsuite.util.SamlClientBuilder; +import java.net.URI; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpUriRequest; +import org.apache.http.client.protocol.HttpClientContext; +import org.apache.http.impl.client.CloseableHttpClient; + +/** + * + * @author hmlnarik + */ +public class IdPInitiatedLoginBuilder implements Step { + + private final SamlClientBuilder clientBuilder; + private final URI authServerSamlUrl; + private final String clientId; + + public IdPInitiatedLoginBuilder(URI authServerSamlUrl, String clientId, SamlClientBuilder clientBuilder) { + this.clientBuilder = clientBuilder; + this.authServerSamlUrl = authServerSamlUrl; + this.clientId = clientId; + } + + @Override + public HttpUriRequest perform(CloseableHttpClient client, URI currentURI, CloseableHttpResponse currentResponse, HttpClientContext context) throws Exception { + return new HttpGet(authServerSamlUrl.toString() + "/clients/" + this.clientId); + } + + public SamlClientBuilder build() { + return this.clientBuilder; + } +} diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/saml/LoginBuilder.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/saml/LoginBuilder.java new file mode 100644 index 0000000000..4e5713b403 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/saml/LoginBuilder.java @@ -0,0 +1,144 @@ +/* + * Copyright 2017 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.util.saml; + +import org.keycloak.testsuite.util.SamlClientBuilder; +import org.keycloak.representations.idm.UserRepresentation; +import org.keycloak.testsuite.util.SamlClient.Step; +import java.io.UnsupportedEncodingException; +import java.net.URI; +import java.util.LinkedList; +import java.util.List; +import java.util.Objects; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriBuilder; +import org.apache.http.NameValuePair; +import org.apache.http.client.entity.UrlEncodedFormEntity; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.methods.HttpUriRequest; +import org.apache.http.client.protocol.HttpClientContext; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.message.BasicNameValuePair; +import org.apache.http.util.EntityUtils; +import org.jsoup.Jsoup; +import org.jsoup.nodes.Element; +import static org.hamcrest.Matchers.containsString; +import static org.junit.Assert.assertThat; +import static org.keycloak.testsuite.admin.Users.getPasswordOf; +import static org.keycloak.testsuite.util.Matchers.statusCodeIsHC; + +/** + * + * @author hmlnarik + */ +public class LoginBuilder implements Step { + + private final SamlClientBuilder clientBuilder; + private UserRepresentation user; + private boolean sso = false; + + public LoginBuilder(SamlClientBuilder clientBuilder) { + this.clientBuilder = clientBuilder; + } + + @Override + public HttpUriRequest perform(CloseableHttpClient client, URI currentURI, CloseableHttpResponse currentResponse, HttpClientContext context) throws Exception { + if (sso) { + return null; // skip this step + } else { + assertThat(currentResponse, statusCodeIsHC(Response.Status.OK)); + String loginPageText = EntityUtils.toString(currentResponse.getEntity(), "UTF-8"); + assertThat(loginPageText, containsString("login")); + + return handleLoginPage(loginPageText); + } + } + + public SamlClientBuilder build() { + return this.clientBuilder; + } + + public LoginBuilder user(UserRepresentation user) { + this.user = user; + return this; + } + + public LoginBuilder sso(boolean sso) { + this.sso = sso; + return this; + } + + /** + * Prepares a GET/POST request for logging the given user into the given login page. The login page is expected + * to have at least input fields with id "username" and "password". + * + * @param user + * @param loginPage + * @return + */ + private HttpUriRequest handleLoginPage(String loginPage) { + return handleLoginPage(user, loginPage); + } + + public static HttpUriRequest handleLoginPage(UserRepresentation user, String loginPage) { + String username = user.getUsername(); + String password = getPasswordOf(user); + org.jsoup.nodes.Document theLoginPage = Jsoup.parse(loginPage); + + List parameters = new LinkedList<>(); + for (Element form : theLoginPage.getElementsByTag("form")) { + String method = form.attr("method"); + String action = form.attr("action"); + boolean isPost = method != null && "post".equalsIgnoreCase(method); + + for (Element input : form.getElementsByTag("input")) { + if (Objects.equals(input.id(), "username")) { + parameters.add(new BasicNameValuePair(input.attr("name"), username)); + } else if (Objects.equals(input.id(), "password")) { + parameters.add(new BasicNameValuePair(input.attr("name"), password)); + } else { + parameters.add(new BasicNameValuePair(input.attr("name"), input.val())); + } + } + + if (isPost) { + HttpPost res = new HttpPost(action); + + UrlEncodedFormEntity formEntity; + try { + formEntity = new UrlEncodedFormEntity(parameters, "UTF-8"); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e); + } + res.setEntity(formEntity); + + return res; + } else { + UriBuilder b = UriBuilder.fromPath(action); + for (NameValuePair parameter : parameters) { + b.queryParam(parameter.getName(), parameter.getValue()); + } + return new HttpGet(b.build()); + } + } + + throw new IllegalArgumentException("Invalid login form: " + loginPage); + } + +} diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/saml/ModifySamlResponseStepBuilder.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/saml/ModifySamlResponseStepBuilder.java new file mode 100644 index 0000000000..e29091bf86 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/saml/ModifySamlResponseStepBuilder.java @@ -0,0 +1,227 @@ +/* + * Copyright 2017 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.util.saml; + +import org.keycloak.testsuite.util.SamlClientBuilder; +import org.keycloak.dom.saml.v2.SAML2Object; +import org.keycloak.saml.common.constants.GeneralConstants; +import org.keycloak.saml.processing.web.util.PostBindingUtil; +import org.keycloak.saml.processing.web.util.RedirectBindingUtil; +import org.keycloak.testsuite.util.SamlClient.Binding; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import javax.ws.rs.core.Response.Status; +import org.apache.commons.io.IOUtils; +import org.apache.http.NameValuePair; +import org.apache.http.client.entity.UrlEncodedFormEntity; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.methods.HttpUriRequest; +import org.apache.http.client.protocol.HttpClientContext; +import org.apache.http.client.utils.URIBuilder; +import org.apache.http.client.utils.URLEncodedUtils; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.message.BasicNameValuePair; +import org.apache.http.util.EntityUtils; +import org.jsoup.Jsoup; +import org.jsoup.nodes.Element; +import org.jsoup.select.Elements; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; +import static org.junit.Assert.assertThat; +import static org.keycloak.testsuite.util.Matchers.statusCodeIsHC; + + +public class ModifySamlResponseStepBuilder extends SamlDocumentStepBuilder { + + private final Binding binding; + + private URI targetUri; + private String targetAttribute; + private Binding targetBinding; + + public ModifySamlResponseStepBuilder(Binding binding, SamlClientBuilder clientBuilder) { + super(clientBuilder); + this.binding = binding; + this.targetBinding = binding; + } + + // TODO: support for signing + @Override + public HttpUriRequest perform(CloseableHttpClient client, URI currentURI, CloseableHttpResponse currentResponse, HttpClientContext context) throws Exception { + switch (binding) { + case REDIRECT: + return handleRedirectBinding(currentResponse); + + case POST: + return handlePostBinding(currentResponse); + } + + throw new RuntimeException("Unknown binding for " + ModifySamlResponseStepBuilder.class.getName()); + } + + public Binding targetBinding() { + return targetBinding; + } + + public ModifySamlResponseStepBuilder targetBinding(Binding targetBinding) { + this.targetBinding = targetBinding; + return this; + } + + public String targetAttribute() { + return targetAttribute; + } + + public ModifySamlResponseStepBuilder targetAttribute(String attribute) { + targetAttribute = attribute; + return this; + } + + public ModifySamlResponseStepBuilder targetAttributeSamlRequest() { + return targetAttribute(GeneralConstants.SAML_REQUEST_KEY); + } + + public ModifySamlResponseStepBuilder targetAttributeSamlResponse() { + return targetAttribute(GeneralConstants.SAML_RESPONSE_KEY); + } + + public URI targetUri() { + return targetUri; + } + + public ModifySamlResponseStepBuilder targetUri(URI forceUri) { + this.targetUri = forceUri; + return this; + } + + protected HttpUriRequest handleRedirectBinding(CloseableHttpResponse currentResponse) throws Exception, IOException, URISyntaxException { + NameValuePair samlParam = null; + + assertThat(currentResponse, statusCodeIsHC(Status.FOUND)); + String location = currentResponse.getFirstHeader("Location").getValue(); + URI locationUri = URI.create(location); + + List params = URLEncodedUtils.parse(locationUri, "UTF-8"); + for (Iterator it = params.iterator(); it.hasNext();) { + NameValuePair param = it.next(); + if ("SAMLResponse".equals(param.getName()) || "SAMLRequest".equals(param.getName())) { + assertThat("Only one SAMLRequest/SAMLResponse check", samlParam, nullValue()); + samlParam = param; + it.remove(); + } + } + + assertThat(samlParam, notNullValue()); + + String base64EncodedSamlDoc = samlParam.getValue(); + InputStream decoded = RedirectBindingUtil.base64DeflateDecode(base64EncodedSamlDoc); + String samlDoc = IOUtils.toString(decoded, GeneralConstants.SAML_CHARSET); + IOUtils.closeQuietly(decoded); + + String transformed = getTransformer().transform(samlDoc); + if (transformed == null) { + return null; + } + + final String attrName = this.targetAttribute != null ? this.targetAttribute : samlParam.getName(); + + return createRequest(locationUri, attrName, transformed, params); + } + + private HttpUriRequest handlePostBinding(CloseableHttpResponse currentResponse) throws Exception { + assertThat(currentResponse, statusCodeIsHC(Status.OK)); + + org.jsoup.nodes.Document theResponsePage = Jsoup.parse(EntityUtils.toString(currentResponse.getEntity())); + Elements samlResponses = theResponsePage.select("input[name=SAMLResponse]"); + Elements samlRequests = theResponsePage.select("input[name=SAMLRequest]"); + Elements forms = theResponsePage.select("form"); + Elements relayStates = theResponsePage.select("input[name=RelayState]"); + int size = samlResponses.size() + samlRequests.size(); + assertThat("Checking uniqueness of SAMLResponse/SAMLRequest input field in the page", size, is(1)); + assertThat("Checking uniqueness of forms in the page", forms, hasSize(1)); + + Element respElement = samlResponses.isEmpty() ? samlRequests.first() : samlResponses.first(); + Element form = forms.first(); + + String base64EncodedSamlDoc = respElement.val(); + InputStream decoded = PostBindingUtil.base64DecodeAsStream(base64EncodedSamlDoc); + String samlDoc = IOUtils.toString(decoded, GeneralConstants.SAML_CHARSET); + IOUtils.closeQuietly(decoded); + + String transformed = getTransformer().transform(samlDoc); + if (transformed == null) { + return null; + } + + final String attributeName = this.targetAttribute != null + ? this.targetAttribute + : respElement.attr("name"); + List parameters = new LinkedList<>(); + + if (! relayStates.isEmpty()) { + parameters.add(new BasicNameValuePair(GeneralConstants.RELAY_STATE, relayStates.first().val())); + } + URI locationUri = this.targetUri != null + ? this.targetUri + : URI.create(form.attr("action")); + + return createRequest(locationUri, attributeName, transformed, parameters); + } + + protected HttpUriRequest createRequest(URI locationUri, String attributeName, String transformed, List parameters) throws IOException, URISyntaxException { + switch (this.targetBinding) { + case POST: + return createPostRequest(locationUri, attributeName, transformed, parameters); + case REDIRECT: + return createRedirectRequest(locationUri, attributeName, transformed, parameters); + } + throw new RuntimeException("Unknown target binding for " + ModifySamlResponseStepBuilder.class.getName()); + } + + protected HttpUriRequest createRedirectRequest(URI locationUri, String attributeName, String transformed, List parameters) throws IOException, URISyntaxException { + final byte[] responseBytes = transformed.getBytes(GeneralConstants.SAML_CHARSET); + parameters.add(new BasicNameValuePair(attributeName, RedirectBindingUtil.deflateBase64Encode(responseBytes))); + + if (this.targetUri != null) { + locationUri = this.targetUri; + } + + URI target = new URIBuilder(locationUri).setParameters(parameters).build(); + + return new HttpGet(target); + } + + protected HttpUriRequest createPostRequest(URI locationUri, String attributeName, String transformed, List parameters) throws IOException { + HttpPost post = new HttpPost(locationUri); + + parameters.add(new BasicNameValuePair(attributeName, PostBindingUtil.base64Encode(transformed))); + + UrlEncodedFormEntity formEntity = new UrlEncodedFormEntity(parameters, GeneralConstants.SAML_CHARSET); + post.setEntity(formEntity); + + return post; + } +} diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/saml/RequiredConsentBuilder.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/saml/RequiredConsentBuilder.java new file mode 100644 index 0000000000..ee24b0670d --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/saml/RequiredConsentBuilder.java @@ -0,0 +1,128 @@ +/* + * Copyright 2017 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.util.saml; + +import org.keycloak.testsuite.util.SamlClient.Step; +import org.keycloak.testsuite.util.SamlClientBuilder; +import java.io.UnsupportedEncodingException; +import java.net.URI; +import java.util.LinkedList; +import java.util.List; +import java.util.Objects; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriBuilder; +import org.apache.http.NameValuePair; +import org.apache.http.client.entity.UrlEncodedFormEntity; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.methods.HttpUriRequest; +import org.apache.http.client.protocol.HttpClientContext; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.message.BasicNameValuePair; +import org.apache.http.util.EntityUtils; +import org.jsoup.Jsoup; +import org.jsoup.nodes.Element; +import static org.hamcrest.Matchers.containsString; +import static org.junit.Assert.assertThat; +import static org.keycloak.testsuite.util.Matchers.statusCodeIsHC; + +/** + * + * @author hmlnarik + */ +public class RequiredConsentBuilder implements Step { + + private final SamlClientBuilder clientBuilder; + private boolean approveConsent = true; + + public RequiredConsentBuilder(SamlClientBuilder clientBuilder) { + this.clientBuilder = clientBuilder; + } + + @Override + public HttpUriRequest perform(CloseableHttpClient client, URI currentURI, CloseableHttpResponse currentResponse, HttpClientContext context) throws Exception { + assertThat(currentResponse, statusCodeIsHC(Response.Status.OK)); + String consentPageText = EntityUtils.toString(currentResponse.getEntity(), "UTF-8"); + assertThat(consentPageText, containsString("consent")); + + return handleConsentPage(consentPageText, currentURI); + } + + public SamlClientBuilder build() { + return this.clientBuilder; + } + + public RequiredConsentBuilder approveConsent(boolean shouldApproveConsent) { + this.approveConsent = shouldApproveConsent; + return this; + } + + /** + * Prepares a GET/POST request for consent granting . The consent page is expected + * to have at least input fields with id "kc-login" and "kc-cancel". + * + * @param consentPage + * @param consent + * @return + */ + public HttpUriRequest handleConsentPage(String consentPage, URI currentURI) { + org.jsoup.nodes.Document theLoginPage = Jsoup.parse(consentPage); + + List parameters = new LinkedList<>(); + for (Element form : theLoginPage.getElementsByTag("form")) { + String method = form.attr("method"); + String action = form.attr("action"); + boolean isPost = method != null && "post".equalsIgnoreCase(method); + + for (Element input : form.getElementsByTag("input")) { + if (Objects.equals(input.id(), "kc-login")) { + if (approveConsent) + parameters.add(new BasicNameValuePair(input.attr("name"), input.attr("value"))); + } else if (Objects.equals(input.id(), "kc-cancel")) { + if (!approveConsent) + parameters.add(new BasicNameValuePair(input.attr("name"), input.attr("value"))); + } else { + parameters.add(new BasicNameValuePair(input.attr("name"), input.val())); + } + } + + if (isPost) { + HttpPost res = new HttpPost(currentURI.resolve(action)); + + UrlEncodedFormEntity formEntity; + try { + formEntity = new UrlEncodedFormEntity(parameters, "UTF-8"); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e); + } + res.setEntity(formEntity); + + return res; + } else { + UriBuilder b = UriBuilder.fromPath(action); + for (NameValuePair parameter : parameters) { + b.queryParam(parameter.getName(), parameter.getValue()); + } + return new HttpGet(b.build()); + } + } + + throw new IllegalArgumentException("Invalid consent page: " + consentPage); + } + +} diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/saml/SamlDocumentStepBuilder.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/saml/SamlDocumentStepBuilder.java new file mode 100644 index 0000000000..8b8fde083c --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/saml/SamlDocumentStepBuilder.java @@ -0,0 +1,147 @@ +/* + * Copyright 2017 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.util.saml; + +import org.keycloak.testsuite.util.SamlClientBuilder; +import org.keycloak.dom.saml.v2.SAML2Object; +import org.keycloak.dom.saml.v2.protocol.ArtifactResolveType; +import org.keycloak.dom.saml.v2.protocol.ArtifactResponseType; +import org.keycloak.dom.saml.v2.protocol.AttributeQueryType; +import org.keycloak.dom.saml.v2.protocol.AuthnRequestType; +import org.keycloak.dom.saml.v2.protocol.LogoutRequestType; +import org.keycloak.dom.saml.v2.protocol.ResponseType; +import org.keycloak.saml.common.constants.GeneralConstants; +import org.keycloak.saml.common.util.DocumentUtil; +import org.keycloak.saml.common.util.StaxUtil; +import org.keycloak.saml.processing.core.parsers.saml.SAMLParser; +import org.keycloak.saml.processing.core.saml.v2.writers.SAMLRequestWriter; +import org.keycloak.saml.processing.core.saml.v2.writers.SAMLResponseWriter; +import org.keycloak.testsuite.util.SamlClient.Step; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import javax.xml.stream.XMLStreamWriter; +import org.junit.Assert; +import org.w3c.dom.Document; + +/** + * + * @author hmlnarik + */ +public abstract class SamlDocumentStepBuilder> implements Step { + + @FunctionalInterface + public interface Saml2ObjectTransformer { + public T transform(T original) throws Exception; + } + + @FunctionalInterface + public interface Saml2DocumentTransformer { + public Document transform(Document original) throws Exception; + } + + @FunctionalInterface + public interface StringTransformer { + public String transform(String original) throws Exception; + } + + private final SamlClientBuilder clientBuilder; + + private StringTransformer transformer = t -> t; + + public SamlDocumentStepBuilder(SamlClientBuilder clientBuilder) { + this.clientBuilder = clientBuilder; + } + + @SuppressWarnings("unchecked") + public This transformObject(Saml2ObjectTransformer tr) { + final StringTransformer original = this.transformer; + this.transformer = s -> { + final String originalTransformed = original.transform(s); + + if (originalTransformed == null) { + return null; + } + + final ByteArrayInputStream baos = new ByteArrayInputStream(originalTransformed.getBytes()); + final T saml2Object = (T) new SAMLParser().parse(baos); + final T transformed = tr.transform(saml2Object); + + if (transformed == null) { + return null; + } + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + XMLStreamWriter xmlStreamWriter = StaxUtil.getXMLStreamWriter(bos); + + if (saml2Object instanceof AuthnRequestType) { + new SAMLRequestWriter(xmlStreamWriter).write((AuthnRequestType) saml2Object); + } else if (saml2Object instanceof LogoutRequestType) { + new SAMLRequestWriter(xmlStreamWriter).write((LogoutRequestType) saml2Object); + } else if (saml2Object instanceof ArtifactResolveType) { + new SAMLRequestWriter(xmlStreamWriter).write((ArtifactResolveType) saml2Object); + } else if (saml2Object instanceof AttributeQueryType) { + new SAMLRequestWriter(xmlStreamWriter).write((AttributeQueryType) saml2Object); + } else if (saml2Object instanceof ResponseType) { + new SAMLResponseWriter(xmlStreamWriter).write((ResponseType) saml2Object); + } else if (saml2Object instanceof ArtifactResponseType) { + new SAMLResponseWriter(xmlStreamWriter).write((ArtifactResponseType) saml2Object); + } else { + Assert.assertNotNull("Unknown type: ", saml2Object); + Assert.fail("Unknown type: " + saml2Object.getClass().getName()); + } + return new String(bos.toByteArray(), GeneralConstants.SAML_CHARSET); + }; + return (This) this; + } + + public This transformDocument(Saml2DocumentTransformer tr) { + final StringTransformer original = this.transformer; + this.transformer = s -> { + final String originalTransformed = original.transform(s); + + if (originalTransformed == null) { + return null; + } + + final Document transformed = tr.transform(DocumentUtil.getDocument(originalTransformed)); + return transformed == null ? null : DocumentUtil.getDocumentAsString(transformed); + }; + return (This) this; + } + + public This transformString(StringTransformer tr) { + final StringTransformer original = this.transformer; + this.transformer = s -> { + final String originalTransformed = original.transform(s); + + if (originalTransformed == null) { + return null; + } + + return tr.transform(originalTransformed); + }; + return (This) this; + } + + public SamlClientBuilder build() { + return this.clientBuilder; + } + + public StringTransformer getTransformer() { + return transformer; + } +} diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/AbstractKeycloakTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/AbstractKeycloakTest.java index 262d0b27b5..d2f7de6280 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/AbstractKeycloakTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/AbstractKeycloakTest.java @@ -144,6 +144,8 @@ public abstract class AbstractKeycloakTest { updateMasterAdminPassword(); } + beforeAbstractKeycloakTestRealmImport(); + if (testContext.getTestRealmReps() == null) { importTestRealms(); @@ -155,6 +157,9 @@ public abstract class AbstractKeycloakTest { oauth.init(adminClient, driver); } + protected void beforeAbstractKeycloakTestRealmImport() throws Exception { + } + @After public void afterAbstractKeycloakTest() { if (resetTimeOffset) { diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountTest.java index eba81f4db2..c69f489000 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountTest.java @@ -28,6 +28,9 @@ import org.keycloak.admin.client.resource.RealmResource; import org.keycloak.events.Details; import org.keycloak.events.Errors; import org.keycloak.events.EventType; +import org.keycloak.models.AccountRoles; +import org.keycloak.models.AdminRoles; +import org.keycloak.models.Constants; import org.keycloak.models.PasswordPolicy; import org.keycloak.models.utils.TimeBasedOTP; import org.keycloak.representations.idm.ClientRepresentation; @@ -78,11 +81,18 @@ public class AccountTest extends AbstractTestRealmKeycloakTest { //UserRepresentation user = findUserInRealmRep(testRealm, "test-user@localhost"); //ClientRepresentation accountApp = findClientInRealmRep(testRealm, ACCOUNT_MANAGEMENT_CLIENT_ID); UserRepresentation user2 = UserBuilder.create() - .enabled(true) - .username("test-user-no-access@localhost") - .email("test-user-no-access@localhost") - .password("password") - .build(); + .enabled(true) + .username("test-user-no-access@localhost") + .email("test-user-no-access@localhost") + .password("password") + .build(); + UserRepresentation realmAdmin = UserBuilder.create() + .enabled(true) + .username("realm-admin") + .password("password") + .role(Constants.REALM_MANAGEMENT_CLIENT_ID, AdminRoles.REALM_ADMIN) + .role(Constants.ACCOUNT_MANAGEMENT_CLIENT_ID, AccountRoles.MANAGE_ACCOUNT) + .build(); testRealm.addIdentityProvider(IdentityProviderBuilder.create() .providerId("github") @@ -105,7 +115,8 @@ public class AccountTest extends AbstractTestRealmKeycloakTest { .build()); RealmBuilder.edit(testRealm) - .user(user2); + .user(user2) + .user(realmAdmin); } private static final UriBuilder BASE = UriBuilder.fromUri("http://localhost:8180/auth"); @@ -870,6 +881,19 @@ public class AccountTest extends AbstractTestRealmKeycloakTest { } } + // KEYCLOAK-5155 + @Test + public void testConsoleListedInApplications() { + applicationsPage.open(); + loginPage.login("realm-admin", "password"); + Assert.assertTrue(applicationsPage.isCurrent()); + Map apps = applicationsPage.getApplications(); + Assert.assertThat(apps.keySet(), hasItems("Admin CLI", "Security Admin Console")); + events.clear(); + } + + + // More tests (including revoke) are in OAuthGrantTest and OfflineTokenTest @Test public void applications() { @@ -880,7 +904,7 @@ public class AccountTest extends AbstractTestRealmKeycloakTest { Assert.assertTrue(applicationsPage.isCurrent()); Map apps = applicationsPage.getApplications(); - Assert.assertThat(apps.keySet(), containsInAnyOrder("Account", "test-app", "test-app-scope", "third-party", "test-app-authz", "My Named Test App", "Test App Named - ${client_account}")); + Assert.assertThat(apps.keySet(), containsInAnyOrder("root-url-client", "Account", "test-app", "test-app-scope", "third-party", "test-app-authz", "My Named Test App", "Test App Named - ${client_account}")); AccountApplicationsPage.AppEntry accountEntry = apps.get("Account"); Assert.assertEquals(3, accountEntry.getRolesAvailable().size()); @@ -891,12 +915,14 @@ public class AccountTest extends AbstractTestRealmKeycloakTest { Assert.assertTrue(accountEntry.getRolesGranted().contains("Full Access")); Assert.assertEquals(1, accountEntry.getProtocolMappersGranted().size()); Assert.assertTrue(accountEntry.getProtocolMappersGranted().contains("Full Access")); + Assert.assertEquals("http://localhost:8180/auth/realms/test/account", accountEntry.getHref()); AccountApplicationsPage.AppEntry testAppEntry = apps.get("test-app"); Assert.assertEquals(5, testAppEntry.getRolesAvailable().size()); Assert.assertTrue(testAppEntry.getRolesAvailable().contains("Offline access")); Assert.assertTrue(testAppEntry.getRolesGranted().contains("Full Access")); Assert.assertTrue(testAppEntry.getProtocolMappersGranted().contains("Full Access")); + Assert.assertEquals("http://localhost:8180/auth/realms/master/app/auth", testAppEntry.getHref()); AccountApplicationsPage.AppEntry thirdPartyEntry = apps.get("third-party"); Assert.assertEquals(2, thirdPartyEntry.getRolesAvailable().size()); @@ -904,6 +930,22 @@ public class AccountTest extends AbstractTestRealmKeycloakTest { Assert.assertTrue(thirdPartyEntry.getRolesAvailable().contains("Have Customer User privileges in test-app")); Assert.assertEquals(0, thirdPartyEntry.getRolesGranted().size()); Assert.assertEquals(0, thirdPartyEntry.getProtocolMappersGranted().size()); + Assert.assertEquals("http://localhost:8180/auth/realms/master/app/auth", thirdPartyEntry.getHref()); + + AccountApplicationsPage.AppEntry testAppNamed = apps.get("Test App Named - ${client_account}"); + Assert.assertEquals("http://localhost:8180/varnamedapp/base", testAppNamed.getHref()); + + AccountApplicationsPage.AppEntry rootUrlClient = apps.get("root-url-client"); + Assert.assertEquals("http://localhost:8180/foo/bar/baz", rootUrlClient.getHref()); + + AccountApplicationsPage.AppEntry authzApp = apps.get("test-app-authz"); + Assert.assertEquals("http://localhost:8180/test-app-authz", authzApp.getHref()); + + AccountApplicationsPage.AppEntry namedApp = apps.get("My Named Test App"); + Assert.assertEquals("http://localhost:8180/namedapp/base", namedApp.getHref()); + + AccountApplicationsPage.AppEntry testAppScope = apps.get("test-app-scope"); + Assert.assertNull(testAppScope.getHref()); } @Test diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionEmailVerificationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionEmailVerificationTest.java index 9fd5c7ac57..f4c745268d 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionEmailVerificationTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionEmailVerificationTest.java @@ -37,6 +37,7 @@ import org.keycloak.testsuite.admin.ApiUtil; import org.keycloak.testsuite.auth.page.AuthRealm; import org.keycloak.testsuite.pages.AppPage; import org.keycloak.testsuite.pages.AppPage.RequestType; +import org.keycloak.testsuite.pages.ProceedPage; import org.keycloak.testsuite.pages.ErrorPage; import org.keycloak.testsuite.pages.InfoPage; import org.keycloak.testsuite.pages.LoginPage; @@ -81,6 +82,9 @@ public class RequiredActionEmailVerificationTest extends AbstractTestRealmKeyclo @Page protected InfoPage infoPage; + @Page + protected ProceedPage proceedPage; + @Page protected ErrorPage errorPage; @@ -330,6 +334,8 @@ public class RequiredActionEmailVerificationTest extends AbstractTestRealmKeyclo driver.navigate().to(verificationUrl2.trim()); + proceedPage.assertCurrent(); + proceedPage.clickProceedLink(); infoPage.assertCurrent(); assertEquals("Your email address has been verified.", infoPage.getInfo()); } @@ -355,6 +361,9 @@ public class RequiredActionEmailVerificationTest extends AbstractTestRealmKeyclo driver.manage().deleteAllCookies(); driver.navigate().to(verificationUrl.trim()); + proceedPage.assertCurrent(); + proceedPage.clickProceedLink(); + infoPage.assertCurrent(); events.expectRequiredAction(EventType.VERIFY_EMAIL) .user(testUserId) @@ -424,7 +433,7 @@ public class RequiredActionEmailVerificationTest extends AbstractTestRealmKeyclo driver.navigate().to(verificationUrl.trim()); loginPage.assertCurrent(); - assertEquals("You took too long to login. Login process starting from beginning.", loginPage.getError()); + assertEquals("Action expired. Please start again.", loginPage.getError()); events.expectRequiredAction(EventType.EXECUTE_ACTION_TOKEN_ERROR) .error(Errors.EXPIRED_CODE) @@ -462,7 +471,7 @@ public class RequiredActionEmailVerificationTest extends AbstractTestRealmKeyclo driver.navigate().to(verificationUrl.trim()); errorPage.assertCurrent(); - assertEquals("The link you clicked is a old stale link and is no longer valid. Maybe you have already verified your email?", errorPage.getError()); + assertEquals("Action expired.", errorPage.getError()); events.expectRequiredAction(EventType.EXECUTE_ACTION_TOKEN_ERROR) .error(Errors.EXPIRED_CODE) diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/AbstractFuseAdminAdapterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/AbstractFuseAdminAdapterTest.java index 3cc9b38701..ef5d835403 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/AbstractFuseAdminAdapterTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/AbstractFuseAdminAdapterTest.java @@ -205,7 +205,7 @@ public abstract class AbstractFuseAdminAdapterTest extends AbstractExampleAdapte pipe.write("logout\n".getBytes()); pipe.flush(); - channel.waitFor(EnumSet.of(ClientChannelEvent.CLOSED), 0); + channel.waitFor(EnumSet.of(ClientChannelEvent.CLOSED), TimeUnit.SECONDS.toMillis(15L)); session.close(true); client.stop(); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/cors/AbstractCorsExampleAdapterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/cors/AbstractCorsExampleAdapterTest.java index 43981645aa..83a34c72d9 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/cors/AbstractCorsExampleAdapterTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/cors/AbstractCorsExampleAdapterTest.java @@ -94,6 +94,7 @@ public abstract class AbstractCorsExampleAdapterTest extends AbstractExampleAdap waitUntilElement(angularCorsProductPage.getOutput()).text().contains("iphone"); waitUntilElement(angularCorsProductPage.getOutput()).text().contains("ipad"); waitUntilElement(angularCorsProductPage.getOutput()).text().contains("ipod"); + waitUntilElement(angularCorsProductPage.getHeaders()).text().contains("\"x-custom1\":\"some-value\""); angularCorsProductPage.loadRoles(); waitUntilElement(angularCorsProductPage.getOutput()).text().contains("user"); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractClientInitiatedAccountLinkTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractClientInitiatedAccountLinkTest.java index ea9937eda3..f95fe7f241 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractClientInitiatedAccountLinkTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractClientInitiatedAccountLinkTest.java @@ -168,6 +168,10 @@ public abstract class AbstractClientInitiatedAccountLinkTest extends AbstractSer user.setUsername("child"); user.setEnabled(true); childUserId = createUserAndResetPasswordWithAdminClient(realm, user, "password"); + UserRepresentation user2 = new UserRepresentation(); + user2.setUsername("child2"); + user2.setEnabled(true); + String user2Id = createUserAndResetPasswordWithAdminClient(realm, user2, "password"); // have to add a role as undertow default auth manager doesn't like "*". todo we can remove this eventually as undertow fixes this in later versions realm.roles().create(new RoleRepresentation("user", null, false)); @@ -175,11 +179,13 @@ public abstract class AbstractClientInitiatedAccountLinkTest extends AbstractSer List roles = new LinkedList<>(); roles.add(role); realm.users().get(childUserId).roles().realmLevel().add(roles); + realm.users().get(user2Id).roles().realmLevel().add(roles); ClientRepresentation brokerService = realm.clients().findByClientId(Constants.BROKER_SERVICE_CLIENT_ID).get(0); role = realm.clients().get(brokerService.getId()).roles().get(Constants.READ_TOKEN_ROLE).toRepresentation(); roles.clear(); roles.add(role); realm.users().get(childUserId).roles().clientLevel(brokerService.getId()).add(roles); + realm.users().get(user2Id).roles().clientLevel(brokerService.getId()).add(roles); } @@ -192,11 +198,6 @@ public abstract class AbstractClientInitiatedAccountLinkTest extends AbstractSer BrokerTestTools.createKcOidcBroker(adminClient, CHILD_IDP, PARENT_IDP, suiteContext); } -// @Test - public void testUi() throws Exception { - Thread.sleep(1000000000); - - } @Test public void testErrorConditions() throws Exception { @@ -388,6 +389,7 @@ public abstract class AbstractClientInitiatedAccountLinkTest extends AbstractSer String linkUrl = linkBuilder.clone() .queryParam("realm", CHILD_IDP) .queryParam("provider", PARENT_IDP).build().toString(); + System.out.println("linkUrl: " + linkUrl); navigateTo(linkUrl); Assert.assertTrue(loginPage.isCurrent(CHILD_IDP)); Assert.assertTrue(driver.getPageSource().contains(PARENT_IDP)); 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 420fad8c48..b62ba31b8a 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 @@ -17,6 +17,7 @@ package org.keycloak.testsuite.adapter.servlet; +import org.keycloak.testsuite.updaters.ClientAttributeUpdater; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpUriRequest; @@ -53,7 +54,6 @@ import org.keycloak.saml.BaseSAML2BindingBuilder; import org.keycloak.saml.SAML2ErrorResponseBuilder; import org.keycloak.saml.common.constants.JBossSAMLURIConstants; import org.keycloak.saml.common.util.XmlKeyInfoKeyNameTransformer; -import org.keycloak.saml.processing.api.saml.v2.request.SAML2Request; import org.keycloak.saml.processing.core.saml.v2.common.SAMLDocumentHolder; import org.keycloak.services.resources.RealmsResource; import org.keycloak.testsuite.adapter.AbstractServletsAdapterTest; @@ -65,6 +65,7 @@ import org.keycloak.testsuite.page.AbstractPage; import org.keycloak.testsuite.util.*; import org.keycloak.testsuite.util.SamlClient.Binding; +import org.keycloak.testsuite.util.SamlClientBuilder; import org.openqa.selenium.By; import org.w3c.dom.Document; import org.w3c.dom.Element; @@ -95,6 +96,12 @@ import java.security.PublicKey; import java.util.*; import java.util.stream.Collectors; +import javax.xml.xpath.XPath; +import javax.xml.xpath.XPathConstants; +import javax.xml.xpath.XPathExpression; +import javax.xml.xpath.XPathFactory; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; import static org.hamcrest.Matchers.*; import static org.junit.Assert.*; import static org.keycloak.representations.idm.CredentialRepresentation.PASSWORD; @@ -106,8 +113,6 @@ import static org.keycloak.testsuite.util.IOUtil.loadXML; import static org.keycloak.testsuite.util.IOUtil.modifyDocElementAttribute; import static org.keycloak.testsuite.util.Matchers.bodyHC; import static org.keycloak.testsuite.util.Matchers.statusCodeIsHC; -import static org.keycloak.testsuite.util.SamlClient.idpInitiatedLogin; -import static org.keycloak.testsuite.util.SamlClient.login; import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlStartsWith; import static org.keycloak.testsuite.util.WaitUtils.*; @@ -470,10 +475,9 @@ public abstract class AbstractSAMLServletsAdapterTest extends AbstractServletsAd @Test public void employeeAcsTest() { - SAMLDocumentHolder samlResponse = new SamlClient(employeeAcsServletPage.buildUri()).getSamlResponse(Binding.POST, (client, context, strategy) -> { - strategy.setRedirectable(false); - return client.execute(new HttpGet(employeeAcsServletPage.buildUri()), context); - }); + SAMLDocumentHolder samlResponse = new SamlClientBuilder() + .navigateTo(employeeAcsServletPage.buildUri()) + .getSamlResponse(Binding.POST); assertThat(samlResponse.getSamlObject(), instanceOf(AuthnRequestType.class)); assertThat(((AuthnRequestType) samlResponse.getSamlObject()).getAssertionConsumerServiceURL(), notNullValue()); @@ -657,6 +661,50 @@ public abstract class AbstractSAMLServletsAdapterTest extends AbstractServletsAd } } + @Test + public void salesPostEncRejectConsent() throws Exception { + ClientRepresentation salesPostEncClient = testRealmResource().clients().findByClientId(SalesPostEncServlet.CLIENT_NAME).get(0); + try (Closeable client = new ClientAttributeUpdater(testRealmResource().clients().get(salesPostEncClient.getId())) + .setConsentRequired(true) + .update()) { + new SamlClientBuilder() + .navigateTo(salesPostEncServletPage.toString()) + .processSamlResponse(Binding.POST).build() + .login().user(bburkeUser).build() + .consentRequired().approveConsent(false).build() + .processSamlResponse(Binding.POST).build() + + .execute(r -> { + assertThat(r, statusCodeIsHC(Response.Status.OK)); + assertThat(r, bodyHC(containsString("urn:oasis:names:tc:SAML:2.0:status:RequestDenied"))); // TODO: revisit - should the HTTP status be 403 too? + }); + } finally { + salesPostEncServletPage.logout(); + } + } + + @Test + public void salesPostRejectConsent() throws Exception { + ClientRepresentation salesPostClient = testRealmResource().clients().findByClientId(SalesPostServlet.CLIENT_NAME).get(0); + try (Closeable client = new ClientAttributeUpdater(testRealmResource().clients().get(salesPostClient.getId())) + .setConsentRequired(true) + .update()) { + new SamlClientBuilder() + .navigateTo(salesPostServletPage.toString()) + .processSamlResponse(Binding.POST).build() + .login().user(bburkeUser).build() + .consentRequired().approveConsent(false).build() + .processSamlResponse(Binding.POST).build() + + .execute(r -> { + assertThat(r, statusCodeIsHC(Response.Status.OK)); + assertThat(r, bodyHC(containsString("urn:oasis:names:tc:SAML:2.0:status:RequestDenied"))); // TODO: revisit - should the HTTP status be 403 too? + }); + } finally { + salesPostServletPage.logout(); + } + } + @Test public void salesPostPassiveTest() { salesPostPassiveServletPage.navigateTo(); @@ -1028,58 +1076,74 @@ public abstract class AbstractSAMLServletsAdapterTest extends AbstractServletsAd @Test //KEYCLOAK-4020 public void testBooleanAttribute() throws Exception { - AuthnRequestType req = SamlClient.createLoginRequestDocument("http://localhost:8081/employee2/", getAppServerSamlEndpoint(employee2ServletPage).toString(), getAuthServerSamlEndpoint(SAMLSERVLETDEMO)); - Document doc = SAML2Request.convert(req); + new SamlClientBuilder() + .authnRequest(getAuthServerSamlEndpoint(SAMLSERVLETDEMO), "http://localhost:8081/employee2/", getAppServerSamlEndpoint(employee2ServletPage).toString(), Binding.POST).build() + .login().user(bburkeUser).build() + .processSamlResponse(Binding.POST) + .transformDocument(responseDoc -> { + Element attribute = responseDoc.createElement("saml:Attribute"); + attribute.setAttribute("Name", "boolean-attribute"); + attribute.setAttribute("NameFormat", "urn:oasis:names:tc:SAML:2.0:attrname-format:basic"); - SAMLDocumentHolder res = login(bburkeUser, getAuthServerSamlEndpoint(SAMLSERVLETDEMO), doc, null, SamlClient.Binding.POST, SamlClient.Binding.POST); - Document responseDoc = res.getSamlDocument(); + Element attributeValue = responseDoc.createElement("saml:AttributeValue"); + attributeValue.setAttribute("xmlns:xs", "http://www.w3.org/2001/XMLSchema"); + attributeValue.setAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance"); + attributeValue.setAttribute("xsi:type", "xs:boolean"); + attributeValue.setTextContent("true"); - Element attribute = responseDoc.createElement("saml:Attribute"); - attribute.setAttribute("Name", "boolean-attribute"); - attribute.setAttribute("NameFormat", "urn:oasis:names:tc:SAML:2.0:attrname-format:basic"); + attribute.appendChild(attributeValue); + IOUtil.appendChildInDocument(responseDoc, "samlp:Response/saml:Assertion/saml:AttributeStatement", attribute); - Element attributeValue = responseDoc.createElement("saml:AttributeValue"); - attributeValue.setAttribute("xmlns:xs", "http://www.w3.org/2001/XMLSchema"); - attributeValue.setAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance"); - attributeValue.setAttribute("xsi:type", "xs:boolean"); - attributeValue.setTextContent("true"); + return responseDoc; + }) + .build() - attribute.appendChild(attributeValue); - IOUtil.appendChildInDocument(responseDoc, "samlp:Response/saml:Assertion/saml:AttributeStatement", attribute); + .navigateTo(employee2ServletPage.toString() + "/getAttributes") - CloseableHttpResponse response = null; - try (CloseableHttpClient client = HttpClientBuilder.create().build()) { - HttpClientContext context = HttpClientContext.create(); + .execute(r -> { + assertThat(r, statusCodeIsHC(Response.Status.OK)); + assertThat(r, bodyHC(containsString("boolean-attribute: true"))); + }); + } - HttpUriRequest post = SamlClient.Binding.POST.createSamlUnsignedResponse(getAppServerSamlEndpoint(employee2ServletPage), null, responseDoc); - response = client.execute(post, context); - assertThat(response, statusCodeIsHC(Response.Status.FOUND)); - response.close(); + @Test + public void testNameIDUnset() throws Exception { + new SamlClientBuilder() + .navigateTo(employee2ServletPage.toString()) + .processSamlResponse(Binding.POST).build() + .login().user(bburkeUser).build() + .processSamlResponse(Binding.POST) + .transformDocument(responseDoc -> { + XPathFactory xPathfactory = XPathFactory.newInstance(); + XPath xpath = xPathfactory.newXPath(); + XPathExpression expr = xpath.compile("//*[local-name()='NameID']"); - HttpGet get = new HttpGet(employee2ServletPage.toString() + "/getAttributes"); - response = client.execute(get); - assertThat(response, statusCodeIsHC(Response.Status.OK)); - assertThat(response, bodyHC(containsString("boolean-attribute: true"))); - } catch (Exception ex) { - throw new RuntimeException(ex); - } finally { - if (response != null) { - EntityUtils.consumeQuietly(response.getEntity()); - try { response.close(); } catch (IOException ex) { } - } - } + NodeList nodeList = (NodeList) expr.evaluate(responseDoc, XPathConstants.NODESET); + assertThat(nodeList.getLength(), is(1)); + + final Node nameIdNode = nodeList.item(0); + nameIdNode.getParentNode().removeChild(nameIdNode); + + return responseDoc; + }) + .build() + + .navigateTo(employee2ServletPage.toString()) + + .execute(r -> { + assertThat(r, statusCodeIsHC(Response.Status.OK)); + assertThat(r, bodyHC(allOf(containsString("principal="), not(containsString("500"))))); + }); } // KEYCLOAK-4329 @Test public void testEmptyKeyInfoElement() { - samlidpInitiatedLoginPage.setAuthRealm(SAMLSERVLETDEMO); - samlidpInitiatedLoginPage.setUrlName("sales-post-sig-email"); - System.out.println(samlidpInitiatedLoginPage.toString()); - URI idpInitiatedLoginPage = URI.create(samlidpInitiatedLoginPage.toString()); - log.debug("Log in using idp initiated login"); - SAMLDocumentHolder documentHolder = idpInitiatedLogin(bburkeUser, idpInitiatedLoginPage, SamlClient.Binding.POST); + SAMLDocumentHolder documentHolder = new SamlClientBuilder() + .idpInitiatedLogin(getAuthServerSamlEndpoint(SAMLSERVLETDEMO), "sales-post-sig-email").build() + .login().user(bburkeUser).build() + .getSamlResponse(Binding.POST); log.debug("Removing KeyInfo from Keycloak response"); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/undertow/servlet/UndertowClientInitiatedAccountLinkTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/undertow/servlet/UndertowClientInitiatedAccountLinkTest.java index a1eef978a1..336d6b7e55 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/undertow/servlet/UndertowClientInitiatedAccountLinkTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/undertow/servlet/UndertowClientInitiatedAccountLinkTest.java @@ -16,6 +16,7 @@ */ package org.keycloak.testsuite.adapter.undertow.servlet; +import org.junit.Test; import org.keycloak.testsuite.adapter.servlet.AbstractClientInitiatedAccountLinkTest; import org.keycloak.testsuite.arquillian.annotation.AppServerContainer; @@ -26,4 +27,15 @@ import org.keycloak.testsuite.arquillian.annotation.AppServerContainer; @AppServerContainer("auth-server-undertow") public class UndertowClientInitiatedAccountLinkTest extends AbstractClientInitiatedAccountLinkTest { + //@Test + public void testUi() throws Exception { + Thread.sleep(1000000000); + + } + + @Override + @Test + public void testAccountLink() throws Exception { + super.testAccountLink(); + } } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/FineGrainAdminUnitTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/FineGrainAdminUnitTest.java index 8386aa612c..5e9a47322b 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/FineGrainAdminUnitTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/FineGrainAdminUnitTest.java @@ -16,16 +16,15 @@ */ package org.keycloak.testsuite.admin; +import org.jboss.arquillian.container.test.api.Deployment; +import org.jboss.shrinkwrap.api.spec.WebArchive; import org.junit.Assert; import org.junit.Test; import org.keycloak.admin.client.Keycloak; -import org.keycloak.authorization.AuthorizationProvider; -import org.keycloak.authorization.AuthorizationProviderFactory; import org.keycloak.authorization.model.Resource; import org.keycloak.models.ClientTemplateModel; import org.keycloak.models.GroupModel; import org.keycloak.models.utils.KeycloakModelUtils; -import org.keycloak.models.utils.RepresentationToModel; import org.keycloak.representations.idm.ClientTemplateRepresentation; import org.keycloak.representations.idm.authorization.Logic; import org.keycloak.representations.idm.authorization.UserPolicyRepresentation; @@ -48,12 +47,15 @@ import org.keycloak.representations.idm.RoleRepresentation; import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.representations.idm.authorization.DecisionStrategy; import org.keycloak.testsuite.AbstractKeycloakTest; +import org.keycloak.testsuite.runonserver.RunOnServerDeployment; import org.keycloak.testsuite.util.AdminClientUtil; import javax.ws.rs.ClientErrorException; +import javax.ws.rs.core.Response; import java.util.LinkedList; import java.util.List; +import static org.keycloak.testsuite.admin.ImpersonationDisabledTest.IMPERSONATION_DISABLED; import static org.keycloak.testsuite.auth.page.AuthRealm.TEST; /** @@ -65,6 +67,11 @@ public class FineGrainAdminUnitTest extends AbstractKeycloakTest { public static final String CLIENT_NAME = "application"; + @Deployment + public static WebArchive deploy() { + return RunOnServerDeployment.create(FineGrainAdminUnitTest.class); + } + @Override public void addTestRealms(List testRealms) { RealmRepresentation testRealmRep = new RealmRepresentation(); @@ -75,38 +82,21 @@ public class FineGrainAdminUnitTest extends AbstractKeycloakTest { } public static void setupDemo(KeycloakSession session) { RealmModel realm = session.realms().getRealmByName(TEST); - ClientModel client = realm.addClient("sales-pipeline-application"); + realm.addRole("realm-role"); + ClientModel client = realm.addClient("sales-application"); RoleModel clientAdmin = client.addRole("admin"); client.addRole("leader-creator"); client.addRole("viewLeads"); - ClientModel client2 = realm.addClient("market-analysis-application"); - RoleModel client2Admin = client2.addRole("admin"); - client2.addRole("market-manager"); - client2.addRole("viewMarkets"); GroupModel sales = realm.createGroup("sales"); - RoleModel salesAppsAdminRole = realm.addRole("sales-apps-admin"); - salesAppsAdminRole.addCompositeRole(clientAdmin); - salesAppsAdminRole.addCompositeRole(client2Admin); - ClientModel realmManagementClient = realm.getClientByClientId("realm-management"); - RoleModel queryClient = realmManagementClient.getRole(AdminRoles.QUERY_CLIENTS); UserModel admin = session.users().addUser(realm, "salesManager"); admin.setEnabled(true); session.userCredentialManager().updateCredential(realm, admin, UserCredentialModel.password("password")); - admin = session.users().addUser(realm, "sales-group-admin"); + + admin = session.users().addUser(realm, "sales-admin"); admin.setEnabled(true); session.userCredentialManager().updateCredential(realm, admin, UserCredentialModel.password("password")); - admin = session.users().addUser(realm, "sales-it"); - admin.setEnabled(true); - session.userCredentialManager().updateCredential(realm, admin, UserCredentialModel.password("password")); - admin = session.users().addUser(realm, "sales-pipeline-admin"); - admin.setEnabled(true); - session.userCredentialManager().updateCredential(realm, admin, UserCredentialModel.password("password")); - admin = session.users().addUser(realm, "client-admin"); - admin.setEnabled(true); - admin.grantRole(queryClient); - session.userCredentialManager().updateCredential(realm, admin, UserCredentialModel.password("password")); UserModel user = session.users().addUser(realm, "salesman"); user.setEnabled(true); @@ -383,10 +373,11 @@ public class FineGrainAdminUnitTest extends AbstractKeycloakTest { } - + @Override protected boolean isImportAfterEachMethod() { return true; } + //@Test public void testDemo() throws Exception { testingClient.server().run(FineGrainAdminUnitTest::setupDemo); @@ -394,7 +385,7 @@ public class FineGrainAdminUnitTest extends AbstractKeycloakTest { } - @Test + //@Test public void testEvaluationLocal() throws Exception { testingClient.server().run(FineGrainAdminUnitTest::setupPolices); testingClient.server().run(FineGrainAdminUnitTest::setupUsers); @@ -432,7 +423,7 @@ public class FineGrainAdminUnitTest extends AbstractKeycloakTest { realmClient.realm(TEST).clients().get(client.getId()).update(client); Assert.fail("should fail with forbidden exception"); } catch (ClientErrorException e) { - Assert.assertEquals(e.getResponse().getStatus(), 403); + Assert.assertEquals(403, e.getResponse().getStatus()); } client.setFullScopeAllowed(false); @@ -443,7 +434,7 @@ public class FineGrainAdminUnitTest extends AbstractKeycloakTest { realmClient.realm(TEST).clients().get(client.getId()).update(client); Assert.fail("should fail with forbidden exception"); } catch (ClientErrorException e) { - Assert.assertEquals(e.getResponse().getStatus(), 403); + Assert.assertEquals(403, e.getResponse().getStatus()); } client.setClientTemplate(null); @@ -453,13 +444,13 @@ public class FineGrainAdminUnitTest extends AbstractKeycloakTest { realmClient.realm(TEST).clients().get(client.getId()).getScopeMappings().realmLevel().add(realmRoleSet); Assert.fail("should fail with forbidden exception"); } catch (ClientErrorException e) { - Assert.assertEquals(e.getResponse().getStatus(), 403); + Assert.assertEquals(403, e.getResponse().getStatus()); } } // test illegal impersonation - { + if (!IMPERSONATION_DISABLED) { Keycloak realmClient = AdminClientUtil.createAdminClient(suiteContext.isAdapterCompatTesting(), TEST, "nomap-admin", "password", Constants.ADMIN_CLI_CLIENT_ID, null); realmClient.realm(TEST).users().get(user1.getId()).impersonate(); @@ -470,7 +461,7 @@ public class FineGrainAdminUnitTest extends AbstractKeycloakTest { realmClient.realm(TEST).users().get(anotherAdmin.getId()).impersonate(); Assert.fail("should fail with forbidden exception"); } catch (ClientErrorException e) { - Assert.assertEquals(e.getResponse().getStatus(), 403); + Assert.assertEquals(403, e.getResponse().getStatus()); } @@ -536,7 +527,7 @@ public class FineGrainAdminUnitTest extends AbstractKeycloakTest { realmClient.realm(TEST).users().get(user1.getId()).roles().realmLevel().add(realmRoleSet); Assert.fail("should fail with forbidden exception"); } catch (ClientErrorException e) { - Assert.assertEquals(e.getResponse().getStatus(), 403); + Assert.assertEquals(403, e.getResponse().getStatus()); } } @@ -547,7 +538,7 @@ public class FineGrainAdminUnitTest extends AbstractKeycloakTest { realmClient.realm(TEST).users().get(user1.getId()).roles().realmLevel().add(realmRoleSet); Assert.fail("should fail with forbidden exception"); } catch (ClientErrorException e) { - Assert.assertEquals(e.getResponse().getStatus(), 403); + Assert.assertEquals(403, e.getResponse().getStatus()); } } @@ -564,21 +555,21 @@ public class FineGrainAdminUnitTest extends AbstractKeycloakTest { realmClient.realm(TEST).users().get(groupMember.getId()).roles().clientLevel(client.getId()).remove(clientRoleSet); roles = realmClient.realm(TEST).users().get(groupMember.getId()).roles().realmLevel().listAvailable(); - Assert.assertEquals(roles.size(), 1); + Assert.assertEquals(1, roles.size()); realmClient.realm(TEST).users().get(groupMember.getId()).roles().realmLevel().add(realmRoleSet); realmClient.realm(TEST).users().get(groupMember.getId()).roles().realmLevel().remove(realmRoleSet); try { realmClient.realm(TEST).users().get(groupMember.getId()).roles().realmLevel().add(realmRole2Set); Assert.fail("should fail with forbidden exception"); } catch (ClientErrorException e) { - Assert.assertEquals(e.getResponse().getStatus(), 403); + Assert.assertEquals(403, e.getResponse().getStatus()); } try { realmClient.realm(TEST).users().get(user1.getId()).roles().realmLevel().add(realmRoleSet); Assert.fail("should fail with forbidden exception"); } catch (ClientErrorException e) { - Assert.assertEquals(e.getResponse().getStatus(), 403); + Assert.assertEquals(403, e.getResponse().getStatus()); } @@ -603,7 +594,7 @@ public class FineGrainAdminUnitTest extends AbstractKeycloakTest { realmClient.realm(TEST).users().get(user1.getId()).roles().realmLevel().add(realmRoleSet); Assert.fail("should fail with forbidden exception"); } catch (ClientErrorException e) { - Assert.assertEquals(e.getResponse().getStatus(), 403); + Assert.assertEquals(403, e.getResponse().getStatus()); } } @@ -658,6 +649,57 @@ public class FineGrainAdminUnitTest extends AbstractKeycloakTest { } } + + // KEYCLOAK-5152 + @Test + public void testMasterRealmWithComposites() throws Exception { + RoleRepresentation composite = new RoleRepresentation(); + composite.setName("composite"); + composite.setComposite(true); + adminClient.realm(TEST).roles().create(composite); + composite = adminClient.realm(TEST).roles().get("composite").toRepresentation(); + + ClientRepresentation client = adminClient.realm(TEST).clients().findByClientId(Constants.REALM_MANAGEMENT_CLIENT_ID).get(0); + RoleRepresentation createClient = adminClient.realm(TEST).clients().get(client.getId()).roles().get(AdminRoles.CREATE_CLIENT).toRepresentation(); + RoleRepresentation queryRealms = adminClient.realm(TEST).clients().get(client.getId()).roles().get(AdminRoles.QUERY_REALMS).toRepresentation(); + List composites = new LinkedList<>(); + composites.add(createClient); + composites.add(queryRealms); + adminClient.realm(TEST).rolesById().addComposites(composite.getId(), composites); + } + + public static void setup5152(KeycloakSession session) { + RealmModel realm = session.realms().getRealmByName(TEST); + ClientModel realmAdminClient = realm.getClientByClientId(Constants.REALM_MANAGEMENT_CLIENT_ID); + RoleModel realmAdminRole = realmAdminClient.getRole(AdminRoles.REALM_ADMIN); + + UserModel realmUser = session.users().addUser(realm, "realm-admin"); + realmUser.grantRole(realmAdminRole); + realmUser.setEnabled(true); + session.userCredentialManager().updateCredential(realm, realmUser, UserCredentialModel.password("password")); + } + + // KEYCLOAK-5152 + @Test + public void testRealmWithComposites() throws Exception { + testingClient.server().run(FineGrainAdminUnitTest::setup5152); + + Keycloak realmClient = AdminClientUtil.createAdminClient(suiteContext.isAdapterCompatTesting(), + TEST, "realm-admin", "password", Constants.ADMIN_CLI_CLIENT_ID, null); + + RoleRepresentation composite = new RoleRepresentation(); + composite.setName("composite"); + composite.setComposite(true); + realmClient.realm(TEST).roles().create(composite); + composite = adminClient.realm(TEST).roles().get("composite").toRepresentation(); + + ClientRepresentation client = adminClient.realm(TEST).clients().findByClientId(Constants.REALM_MANAGEMENT_CLIENT_ID).get(0); + RoleRepresentation viewUsers = adminClient.realm(TEST).clients().get(client.getId()).roles().get(AdminRoles.CREATE_CLIENT).toRepresentation(); + + List composites = new LinkedList<>(); + composites.add(viewUsers); + realmClient.realm(TEST).rolesById().addComposites(composite.getId(), composites); + } // testRestEvaluationMasterRealm // testRestEvaluationMasterAdminTestRealm @@ -700,6 +742,91 @@ public class FineGrainAdminUnitTest extends AbstractKeycloakTest { testingClient.server().run(FineGrainAdminUnitTest::invokeDelete); } + // KEYCLOAK-5211 + @Test + public void testCreateRealmCreateClient() throws Exception { + ClientRepresentation rep = new ClientRepresentation(); + rep.setName("fullScopedClient"); + rep.setClientId("fullScopedClient"); + rep.setFullScopeAllowed(true); + rep.setSecret("618268aa-51e6-4e64-93c4-3c0bc65b8171"); + rep.setProtocol("openid-connect"); + rep.setPublicClient(false); + rep.setEnabled(true); + adminClient.realm("master").clients().create(rep); + + Keycloak realmClient = AdminClientUtil.createAdminClient(suiteContext.isAdapterCompatTesting(), + "master", "admin", "admin", "fullScopedClient", "618268aa-51e6-4e64-93c4-3c0bc65b8171"); + + RealmRepresentation newRealm=new RealmRepresentation(); + newRealm.setRealm("anotherRealm"); + newRealm.setId("anotherRealm"); + newRealm.setEnabled(true); + realmClient.realms().create(newRealm); + + ClientRepresentation newClient = new ClientRepresentation(); + + try { + newClient.setName("newClient"); + newClient.setClientId("newClient"); + newClient.setFullScopeAllowed(true); + newClient.setSecret("secret"); + newClient.setProtocol("openid-connect"); + newClient.setPublicClient(false); + newClient.setEnabled(true); + Response response = realmClient.realm("anotherRealm").clients().create(newClient); + Assert.assertEquals(403, response.getStatus()); + + realmClient.close(); + realmClient = AdminClientUtil.createAdminClient(suiteContext.isAdapterCompatTesting(), + "master", "admin", "admin", "fullScopedClient", "618268aa-51e6-4e64-93c4-3c0bc65b8171"); + response = realmClient.realm("anotherRealm").clients().create(newClient); + Assert.assertEquals(201, response.getStatus()); + } finally { + adminClient.realm("anotherRealm").remove(); + + } + + + } + + // KEYCLOAK-5211 + @Test + public void testCreateRealmCreateClientWithMaster() throws Exception { + ClientRepresentation rep = new ClientRepresentation(); + rep.setName("fullScopedClient"); + rep.setClientId("fullScopedClient"); + rep.setFullScopeAllowed(true); + rep.setSecret("618268aa-51e6-4e64-93c4-3c0bc65b8171"); + rep.setProtocol("openid-connect"); + rep.setPublicClient(false); + rep.setEnabled(true); + adminClient.realm("master").clients().create(rep); + + RealmRepresentation newRealm=new RealmRepresentation(); + newRealm.setRealm("anotherRealm"); + newRealm.setId("anotherRealm"); + newRealm.setEnabled(true); + adminClient.realms().create(newRealm); + + try { + ClientRepresentation newClient = new ClientRepresentation(); + + newClient.setName("newClient"); + newClient.setClientId("newClient"); + newClient.setFullScopeAllowed(true); + newClient.setSecret("secret"); + newClient.setProtocol("openid-connect"); + newClient.setPublicClient(false); + newClient.setEnabled(true); + Response response = adminClient.realm("anotherRealm").clients().create(newClient); + Assert.assertEquals(201, response.getStatus()); + } finally { + adminClient.realm("anotherRealm").remove(); + + } + } + } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/IllegalAdminUpgradeTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/IllegalAdminUpgradeTest.java index 7c6ced5e29..3318a6db01 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/IllegalAdminUpgradeTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/IllegalAdminUpgradeTest.java @@ -16,6 +16,8 @@ */ package org.keycloak.testsuite.admin; +import org.jboss.arquillian.container.test.api.Deployment; +import org.jboss.shrinkwrap.api.spec.WebArchive; import org.junit.Assert; import org.junit.Test; import org.keycloak.admin.client.Keycloak; @@ -40,6 +42,7 @@ import org.keycloak.representations.idm.authorization.UserPolicyRepresentation; import org.keycloak.services.resources.admin.permissions.AdminPermissionManagement; import org.keycloak.services.resources.admin.permissions.AdminPermissions; import org.keycloak.testsuite.AbstractKeycloakTest; +import org.keycloak.testsuite.runonserver.RunOnServerDeployment; import org.keycloak.testsuite.util.AdminClientUtil; import javax.ws.rs.ClientErrorException; @@ -58,6 +61,11 @@ public class IllegalAdminUpgradeTest extends AbstractKeycloakTest { public static final String CLIENT_NAME = "application"; + @Deployment + public static WebArchive deploy() { + return RunOnServerDeployment.create(FineGrainAdminUnitTest.class); + } + @Override public void addTestRealms(List testRealms) { RealmRepresentation testRealmRep = new RealmRepresentation(); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/ImpersonationDisabledTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/ImpersonationDisabledTest.java index ef1c1c3cc8..b1485c5b10 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/ImpersonationDisabledTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/ImpersonationDisabledTest.java @@ -32,11 +32,13 @@ import static org.keycloak.testsuite.auth.page.AuthRealm.TEST; * @author Vlastislav Ramik */ public class ImpersonationDisabledTest extends AbstractAdminTest { + + public static boolean IMPERSONATION_DISABLED = "impersonation".equals(System.getProperty("feature.name")) + && "disabled".equals(System.getProperty("feature.value")); @BeforeClass public static void enabled() { - Assume.assumeTrue("impersonation".equals(System.getProperty("feature.name")) - && "disabled".equals(System.getProperty("feature.value"))); + Assume.assumeTrue(IMPERSONATION_DISABLED); } @Test @@ -44,6 +46,7 @@ public class ImpersonationDisabledTest extends AbstractAdminTest { String impersonatedUserId = adminClient.realm(TEST).users().search("test-user@localhost", 0, 1).get(0).getId(); try { + log.debug("--Expected javax.ws.rs.WebApplicationException--"); adminClient.realms().realm("test").users().get(impersonatedUserId).impersonate(); } catch (ServerErrorException e) { assertEquals(Response.Status.NOT_IMPLEMENTED.getStatusCode(), e.getResponse().getStatus()); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/SMTPConnectionTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/SMTPConnectionTest.java new file mode 100644 index 0000000000..303cfd65ad --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/SMTPConnectionTest.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.testsuite.admin; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.keycloak.admin.client.resource.RealmResource; +import org.keycloak.representations.idm.RealmRepresentation; +import org.keycloak.representations.idm.UserRepresentation; +import org.keycloak.testsuite.AbstractKeycloakTest; +import org.keycloak.testsuite.util.GreenMailRule; +import org.keycloak.testsuite.util.UserBuilder; + +import javax.mail.internet.MimeMessage; +import javax.ws.rs.core.Response; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; +import static org.keycloak.util.JsonSerialization.writeValueAsPrettyString; + +/** + * @author Bruno Oliveira + */ +public class SMTPConnectionTest extends AbstractKeycloakTest { + + @Rule + public GreenMailRule greenMailRule = new GreenMailRule(); + private RealmResource realm; + + @Override + public void addTestRealms(List testRealms) { + } + + @Before + public void before() { + realm = adminClient.realm("master"); + List admin = realm.users().search("admin", 0, 1); + UserRepresentation user = UserBuilder.edit(admin.get(0)).email("admin@localhost").build(); + realm.users().get(user.getId()).update(user); + } + + private String settings(String host, String port, String from, String auth, String ssl, String starttls, + String username, String password) throws Exception { + Map config = new HashMap<>(); + config.put("host", host); + config.put("port", port); + config.put("from", from); + config.put("auth", auth); + config.put("ssl", ssl); + config.put("starttls", starttls); + config.put("user", username); + config.put("password", password); + return writeValueAsPrettyString(config); + } + + @Test + public void testWithEmptySettings() throws Exception { + Response response = realm.testSMTPConnection(settings(null, null, null, null, null, null, + null, null)); + assertStatus(response, 500); + } + + @Test + public void testWithProperSettings() throws Exception { + Response response = realm.testSMTPConnection(settings("127.0.0.1", "3025", "auto@keycloak.org", null, null, null, + null, null)); + assertStatus(response, 204); + assertMailReceived(); + } + + @Test + public void testWithAuthEnabledCredentialsEmpty() throws Exception { + Response response = realm.testSMTPConnection(settings("127.0.0.1", "3025", "auto@keycloak.org", "true", null, null, + null, null)); + assertStatus(response, 500); + } + + @Test + public void testWithAuthEnabledValidCredentials() throws Exception { + greenMailRule.credentials("admin@localhost", "admin"); + Response response = realm.testSMTPConnection(settings("127.0.0.1", "3025", "auto@keycloak.org", "true", null, null, + "admin@localhost", "admin")); + assertStatus(response, 204); + } + + private void assertStatus(Response response, int status) { + assertEquals(status, response.getStatus()); + response.close(); + } + + private void assertMailReceived() { + if (greenMailRule.getReceivedMessages().length == 1) { + try { + MimeMessage message = greenMailRule.getReceivedMessages()[0]; + assertEquals("[KEYCLOAK] - SMTP test message", message.getSubject()); + } catch (Exception e) { + e.printStackTrace(); + } + } else { + fail("E-mail was not received"); + } + } +} diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UserTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UserTest.java index 567b2846fa..58193e9ff1 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UserTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UserTest.java @@ -18,9 +18,11 @@ package org.keycloak.testsuite.admin; import org.hamcrest.Matchers; +import org.jboss.arquillian.container.test.api.Deployment; import org.jboss.arquillian.drone.api.annotation.Drone; import org.jboss.arquillian.graphene.page.Page; import org.jboss.arquillian.test.api.ArquillianResource; +import org.jboss.shrinkwrap.api.spec.WebArchive; import org.junit.Assert; import org.junit.Rule; import org.junit.Test; @@ -29,9 +31,13 @@ import org.keycloak.admin.client.resource.RealmResource; import org.keycloak.admin.client.resource.RoleMappingResource; import org.keycloak.admin.client.resource.UserResource; import org.keycloak.admin.client.resource.UsersResource; +import org.keycloak.common.util.Base64; +import org.keycloak.credential.CredentialModel; import org.keycloak.events.admin.OperationType; import org.keycloak.events.admin.ResourceType; import org.keycloak.models.Constants; +import org.keycloak.models.PasswordPolicy; +import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.CredentialRepresentation; @@ -44,10 +50,13 @@ import org.keycloak.representations.idm.RequiredActionProviderRepresentation; import org.keycloak.representations.idm.RoleRepresentation; import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.services.resources.RealmsResource; +import org.keycloak.testsuite.AbstractTestRealmKeycloakTest; import org.keycloak.testsuite.page.LoginPasswordUpdatePage; import org.keycloak.testsuite.pages.ErrorPage; import org.keycloak.testsuite.pages.InfoPage; import org.keycloak.testsuite.pages.LoginPage; +import org.keycloak.testsuite.pages.ProceedPage; +import org.keycloak.testsuite.runonserver.RunOnServerDeployment; import org.keycloak.testsuite.util.AdminEventPaths; import org.keycloak.testsuite.util.ClientBuilder; import org.keycloak.testsuite.util.GreenMailRule; @@ -65,12 +74,15 @@ import javax.ws.rs.core.Response; import javax.ws.rs.core.UriBuilder; import java.io.IOException; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.LinkedList; import java.util.List; - import java.util.concurrent.atomic.AtomicInteger; + import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; @@ -90,19 +102,29 @@ public class UserTest extends AbstractAdminTest { @Page protected LoginPasswordUpdatePage passwordUpdatePage; - @ArquillianResource protected OAuthClient oAuthClient; @Page protected InfoPage infoPage; + @Page + protected ProceedPage proceedPage; + @Page protected ErrorPage errorPage; @Page protected LoginPage loginPage; + @Deployment + public static WebArchive deploy() { + return RunOnServerDeployment.create( + AbstractAdminTest.class, + AbstractTestRealmKeycloakTest.class, + UserResource.class); + } + public String createUser() { return createUser("user1", "user1@localhost"); } @@ -168,6 +190,73 @@ public class UserTest extends AbstractAdminTest { response.close(); } + @Test + public void createUserWithHashedCredentials() { + UserRepresentation user = new UserRepresentation(); + user.setUsername("user_creds"); + user.setEmail("email@localhost"); + + CredentialRepresentation hashedPassword = new CredentialRepresentation(); + hashedPassword.setAlgorithm("my-algorithm"); + hashedPassword.setCounter(11); + hashedPassword.setCreatedDate(1001l); + hashedPassword.setDevice("deviceX"); + hashedPassword.setDigits(6); + hashedPassword.setHashIterations(22); + hashedPassword.setHashedSaltedValue("ABC"); + hashedPassword.setPeriod(99); + hashedPassword.setSalt(Base64.encodeBytes("theSalt".getBytes())); + hashedPassword.setType(CredentialRepresentation.PASSWORD); + + user.setCredentials(Arrays.asList(hashedPassword)); + + createUser(user); + + CredentialModel credentialHashed = fetchCredentials("user_creds"); + assertNotNull("Expecting credential", credentialHashed); + assertEquals("my-algorithm", credentialHashed.getAlgorithm()); + assertEquals(11, credentialHashed.getCounter()); + assertEquals(Long.valueOf(1001), credentialHashed.getCreatedDate()); + assertEquals("deviceX", credentialHashed.getDevice()); + assertEquals(6, credentialHashed.getDigits()); + assertEquals(22, credentialHashed.getHashIterations()); + assertEquals("ABC", credentialHashed.getValue()); + assertEquals(99, credentialHashed.getPeriod()); + assertEquals("theSalt", new String(credentialHashed.getSalt())); + assertEquals(CredentialRepresentation.PASSWORD, credentialHashed.getType()); + } + + @Test + public void createUserWithRawCredentials() { + UserRepresentation user = new UserRepresentation(); + user.setUsername("user_rawpw"); + user.setEmail("email.raw@localhost"); + + CredentialRepresentation rawPassword = new CredentialRepresentation(); + rawPassword.setValue("ABCD"); + rawPassword.setType(CredentialRepresentation.PASSWORD); + user.setCredentials(Arrays.asList(rawPassword)); + + createUser(user); + + CredentialModel credential = fetchCredentials("user_rawpw"); + assertNotNull("Expecting credential", credential); + assertEquals(PasswordPolicy.HASH_ALGORITHM_DEFAULT, credential.getAlgorithm()); + assertEquals(PasswordPolicy.HASH_ITERATIONS_DEFAULT, credential.getHashIterations()); + assertNotEquals("ABCD", credential.getValue()); + assertEquals(CredentialRepresentation.PASSWORD, credential.getType()); + } + + private CredentialModel fetchCredentials(String username) { + return getTestingClient().server(REALM_NAME).fetch(session -> { + RealmModel realm = session.getContext().getRealm(); + UserModel user = session.users().getUserByUsername(username, realm); + List storedCredentialsByType = session.userCredentialManager().getStoredCredentialsByType(realm, user, CredentialRepresentation.PASSWORD); + System.out.println(storedCredentialsByType.size()); + return storedCredentialsByType.get(0); + }, CredentialModel.class); + } + @Test public void createDuplicatedUser3() { createUser(); @@ -543,6 +632,9 @@ public class UserTest extends AbstractAdminTest { driver.navigate().to(link); + proceedPage.assertCurrent(); + Assert.assertThat(proceedPage.getInfo(), Matchers.containsString("Update Password")); + proceedPage.clickProceedLink(); passwordUpdatePage.assertCurrent(); passwordUpdatePage.changePassword("new-pass", "new-pass"); @@ -579,6 +671,9 @@ public class UserTest extends AbstractAdminTest { driver.navigate().to(link); + proceedPage.assertCurrent(); + Assert.assertThat(proceedPage.getInfo(), Matchers.containsString("Update Password")); + proceedPage.clickProceedLink(); passwordUpdatePage.assertCurrent(); passwordUpdatePage.changePassword("new-pass" + i, "new-pass" + i); @@ -621,6 +716,9 @@ public class UserTest extends AbstractAdminTest { driver.navigate().to(link); + proceedPage.assertCurrent(); + Assert.assertThat(proceedPage.getInfo(), Matchers.containsString("Update Password")); + proceedPage.clickProceedLink(); passwordUpdatePage.assertCurrent(); passwordUpdatePage.changePassword("new-pass" + i, "new-pass" + i); @@ -659,6 +757,9 @@ public class UserTest extends AbstractAdminTest { driver.navigate().to(link); + proceedPage.assertCurrent(); + Assert.assertThat(proceedPage.getInfo(), Matchers.containsString("Update Password")); + proceedPage.clickProceedLink(); passwordUpdatePage.assertCurrent(); driver.manage().deleteAllCookies(); @@ -666,6 +767,9 @@ public class UserTest extends AbstractAdminTest { driver.navigate().to(link); + proceedPage.assertCurrent(); + Assert.assertThat(proceedPage.getInfo(), Matchers.containsString("Update Password")); + proceedPage.clickProceedLink(); passwordUpdatePage.assertCurrent(); passwordUpdatePage.changePassword("new-pass", "new-pass"); @@ -706,7 +810,7 @@ public class UserTest extends AbstractAdminTest { driver.navigate().to(link); errorPage.assertCurrent(); - assertEquals("An error occurred, please login again through your application.", errorPage.getError()); + assertEquals("Action expired.", errorPage.getError()); } finally { setTimeOffset(0); @@ -765,6 +869,9 @@ public class UserTest extends AbstractAdminTest { driver.navigate().to(link); + proceedPage.assertCurrent(); + Assert.assertThat(proceedPage.getInfo(), Matchers.containsString("Update Password")); + proceedPage.clickProceedLink(); passwordUpdatePage.assertCurrent(); passwordUpdatePage.changePassword("new-pass", "new-pass"); @@ -825,6 +932,9 @@ public class UserTest extends AbstractAdminTest { driver.navigate().to(link); + proceedPage.assertCurrent(); + Assert.assertThat(proceedPage.getInfo(), Matchers.containsString("Update Password")); + proceedPage.clickProceedLink(); passwordUpdatePage.assertCurrent(); passwordUpdatePage.changePassword("new-pass", "new-pass"); @@ -896,11 +1006,17 @@ public class UserTest extends AbstractAdminTest { driver.navigate().to(link); + proceedPage.assertCurrent(); + Assert.assertThat(proceedPage.getInfo(), Matchers.containsString("Verify Email")); + proceedPage.clickProceedLink(); Assert.assertEquals("Your account has been updated.", infoPage.getInfo()); driver.navigate().to("about:blank"); driver.navigate().to(link); // It should be possible to use the same action token multiple times + proceedPage.assertCurrent(); + Assert.assertThat(proceedPage.getInfo(), Matchers.containsString("Verify Email")); + proceedPage.clickProceedLink(); Assert.assertEquals("Your account has been updated.", infoPage.getInfo()); } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/InitialFlowsTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/InitialFlowsTest.java index 0481518518..57fe6de7bc 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/InitialFlowsTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/InitialFlowsTest.java @@ -19,6 +19,8 @@ package org.keycloak.testsuite.admin.authentication; import org.junit.Assert; import org.junit.Test; +import org.keycloak.models.utils.DefaultAuthenticationFlows; +import org.keycloak.protocol.docker.DockerAuthenticator; import org.keycloak.representations.idm.AuthenticationExecutionExportRepresentation; import org.keycloak.representations.idm.AuthenticationExecutionInfoRepresentation; import org.keycloak.representations.idm.AuthenticationFlowRepresentation; @@ -155,6 +157,13 @@ public class InitialFlowsTest extends AbstractAuthenticationTest { addExecInfo(execs, "OTP", "direct-grant-validate-otp", false, 0, 2, OPTIONAL, null, new String[]{REQUIRED, OPTIONAL, DISABLED}); expected.add(new FlowExecutions(flow, execs)); + flow = newFlow("docker auth", "Used by Docker clients to authenticate against the IDP", "basic-flow", true, true); + addExecExport(flow, null, false, "docker-http-basic-authenticator", false, null, REQUIRED, 10); + + execs = new LinkedList<>(); + addExecInfo(execs, "Docker Authenticator", "docker-http-basic-authenticator", false, 0, 0, REQUIRED, null, new String[]{REQUIRED}); + expected.add(new FlowExecutions(flow, execs)); + flow = newFlow("first broker login", "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account", "basic-flow", true, true); addExecExport(flow, null, false, "idp-review-profile", false, "review profile config", REQUIRED, 10); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/ProvidersTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/ProvidersTest.java index e13794dc8e..f55e90f48e 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/ProvidersTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/ProvidersTest.java @@ -151,6 +151,7 @@ public class ProvidersTest extends AbstractAuthenticationTest { "Validates the password supplied as a 'password' form parameter in direct grant request"); addProviderInfo(result, "direct-grant-validate-username", "Username Validation", "Validates the username supplied as a 'username' form parameter in direct grant request"); + addProviderInfo(result, "docker-http-basic-authenticator", "Docker Authenticator", "Uses HTTP Basic authentication to validate docker users, returning a docker error token on auth failure"); addProviderInfo(result, "expected-param-authenticator", "TEST: Expected Parameter", "You will be approved if you send query string parameter 'foo' with expected value."); addProviderInfo(result, "http-basic-authenticator", "HTTP Basic Authentication", "Validates username and password from Authorization HTTP header"); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/AbstractAuthorizationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/AbstractAuthorizationTest.java index 2c3aac71e0..51a2caebc9 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/AbstractAuthorizationTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/AbstractAuthorizationTest.java @@ -20,10 +20,12 @@ package org.keycloak.testsuite.admin.client.authorization; import org.junit.After; import org.junit.Before; import org.junit.BeforeClass; +import org.keycloak.admin.client.resource.AuthorizationResource; import org.keycloak.admin.client.resource.ClientResource; import org.keycloak.admin.client.resource.ResourceScopeResource; import org.keycloak.admin.client.resource.ResourceScopesResource; import org.keycloak.representations.idm.ClientRepresentation; +import org.keycloak.representations.idm.authorization.ResourceServerRepresentation; import org.keycloak.representations.idm.authorization.ScopeRepresentation; import org.keycloak.testsuite.ProfileAssume; import org.keycloak.testsuite.admin.client.AbstractClientTest; @@ -38,7 +40,7 @@ import static org.junit.Assert.assertFalse; */ public abstract class AbstractAuthorizationTest extends AbstractClientTest { - protected static final String RESOURCE_SERVER_CLIENT_ID = "test-resource-server"; + protected static final String RESOURCE_SERVER_CLIENT_ID = "resource-server-test"; @BeforeClass public static void enabled() { @@ -73,8 +75,17 @@ public abstract class AbstractAuthorizationTest extends AbstractClientTest { resourceServer.setAuthorizationServicesEnabled(true); resourceServer.setServiceAccountsEnabled(true); + resourceServer.setPublicClient(false); + resourceServer.setSecret("secret"); getClientResource().update(resourceServer); + + AuthorizationResource authorization = getClientResource().authorization(); + ResourceServerRepresentation settings = authorization.exportSettings(); + + settings.setAllowRemoteResourceManagement(true); + + authorization.update(settings); } protected ResourceScopeResource createDefaultScope() { diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/GroupPolicyManagementTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/GroupPolicyManagementTest.java index 57d86a79ee..4decccf1a7 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/GroupPolicyManagementTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/GroupPolicyManagementTest.java @@ -58,16 +58,11 @@ public class GroupPolicyManagementTest extends AbstractPolicyManagementTest { return super.createTestRealm().group(GroupBuilder.create().name("Group A") .subGroups(Arrays.asList("Group B", "Group D").stream().map(name -> { if ("Group B".equals(name)) { - return GroupBuilder.create().name(name).subGroups(Arrays.asList("Group C", "Group E").stream().map(new Function() { - @Override - public GroupRepresentation apply(String name) { - return GroupBuilder.create().name(name).build(); - } - }).collect(Collectors.toList())).build(); + return GroupBuilder.create().name(name).subGroups(Arrays.asList("Group C", "Group E").stream().map(name1 -> GroupBuilder.create().name(name1).build()).collect(Collectors.toList())).build(); } return GroupBuilder.create().name(name).build(); }).collect(Collectors.toList())) - .build()).group(GroupBuilder.create().name("Group E").build()); + .build()).group(GroupBuilder.create().name("Group F").build()); } @Test @@ -81,7 +76,7 @@ public class GroupPolicyManagementTest extends AbstractPolicyManagementTest { representation.setLogic(Logic.NEGATIVE); representation.setGroupsClaim("groups"); representation.addGroupPath("/Group A/Group B/Group C", true); - representation.addGroupPath("Group E"); + representation.addGroupPath("Group F"); assertCreated(authorization, representation); } @@ -97,7 +92,7 @@ public class GroupPolicyManagementTest extends AbstractPolicyManagementTest { representation.setLogic(Logic.NEGATIVE); representation.setGroupsClaim("groups"); representation.addGroupPath("/Group A/Group B/Group C", true); - representation.addGroupPath("Group E"); + representation.addGroupPath("Group F"); assertCreated(authorization, representation); @@ -114,7 +109,7 @@ public class GroupPolicyManagementTest extends AbstractPolicyManagementTest { assertRepresentation(representation, permission); for (GroupPolicyRepresentation.GroupDefinition roleDefinition : representation.getGroups()) { - if (roleDefinition.getPath().equals("Group E")) { + if (roleDefinition.getPath().equals("Group F")) { roleDefinition.setExtendChildren(true); } } @@ -137,7 +132,7 @@ public class GroupPolicyManagementTest extends AbstractPolicyManagementTest { representation.setName("Delete Group Policy"); representation.setGroupsClaim("groups"); representation.addGroupPath("/Group A/Group B/Group C", true); - representation.addGroupPath("Group E"); + representation.addGroupPath("Group F"); GroupPoliciesResource policies = authorization.policies().group(); Response response = policies.create(representation); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/ResourceManagementTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/ResourceManagementTest.java index 9907472a6a..3c1a2f189f 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/ResourceManagementTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/ResourceManagementTest.java @@ -22,17 +22,22 @@ import org.junit.Before; import org.junit.Test; import org.keycloak.admin.client.resource.ResourceResource; import org.keycloak.admin.client.resource.ResourcesResource; +import org.keycloak.authorization.client.util.HttpResponseException; +import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.authorization.ResourceRepresentation; import org.keycloak.representations.idm.authorization.ScopeRepresentation; import javax.ws.rs.NotFoundException; import javax.ws.rs.core.Response; import java.util.HashSet; +import java.util.List; import java.util.Set; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; /** * @@ -47,9 +52,29 @@ public class ResourceManagementTest extends AbstractAuthorizationTest { enableAuthorizationServices(); } + @Override + public void addTestRealms(List testRealms) { + RealmRepresentation testRealmRep = new RealmRepresentation(); + testRealmRep.setId("authz-test"); + testRealmRep.setRealm("authz-test"); + testRealmRep.setEnabled(true); + testRealms.add(testRealmRep); + } + + @Override + public void setDefaultPageUriParameters() { + super.setDefaultPageUriParameters(); + testRealmPage.setAuthRealm("authz-test"); + } + + @Override + protected String getRealmId() { + return "authz-test"; + } + @Test public void testCreate() { - ResourceRepresentation newResource = createResource().toRepresentation(); + ResourceRepresentation newResource = createResource(); assertEquals("Test Resource", newResource.getName()); assertEquals("/test/*", newResource.getUri()); @@ -57,18 +82,35 @@ public class ResourceManagementTest extends AbstractAuthorizationTest { assertEquals("icon-test-resource", newResource.getIconUri()); } + @Test + public void failCreateWithSameName() { + ResourceRepresentation newResource = createResource(); + + try { + doCreateResource(newResource); + fail("Can not create resources with the same name and owner"); + } catch (Exception e) { + assertEquals(HttpResponseException.class, e.getCause().getClass()); + assertEquals(409, HttpResponseException.class.cast(e.getCause()).getStatusCode()); + } + + newResource.setName(newResource.getName() + " Another"); + + newResource = doCreateResource(newResource); + + assertNotNull(newResource.getId()); + assertEquals("Test Resource Another", newResource.getName()); + } + @Test public void testUpdate() { - ResourceResource resourceResource = createResource(); - ResourceRepresentation resource = resourceResource.toRepresentation(); + ResourceRepresentation resource = createResource(); resource.setType("changed"); resource.setIconUri("changed"); resource.setUri("changed"); - resourceResource.update(resource); - - resource = resourceResource.toRepresentation(); + resource = doUpdateResource(resource); assertEquals("changed", resource.getIconUri()); assertEquals("changed", resource.getType()); @@ -77,17 +119,16 @@ public class ResourceManagementTest extends AbstractAuthorizationTest { @Test(expected = NotFoundException.class) public void testDelete() { - ResourceResource resourceResource = createResource(); + ResourceRepresentation resource = createResource(); - resourceResource.remove(); + doRemoveResource(resource); - resourceResource.toRepresentation(); + getClientResource().authorization().resources().resource(resource.getId()).toRepresentation(); } @Test public void testAssociateScopes() { - ResourceResource resourceResource = createResourceWithDefaultScopes(); - ResourceRepresentation updated = resourceResource.toRepresentation(); + ResourceRepresentation updated = createResourceWithDefaultScopes(); assertEquals(3, updated.getScopes().size()); @@ -98,8 +139,7 @@ public class ResourceManagementTest extends AbstractAuthorizationTest { @Test public void testUpdateScopes() { - ResourceResource resourceResource = createResourceWithDefaultScopes(); - ResourceRepresentation resource = resourceResource.toRepresentation(); + ResourceRepresentation resource = createResourceWithDefaultScopes(); Set scopes = new HashSet<>(resource.getScopes()); assertEquals(3, scopes.size()); @@ -107,9 +147,7 @@ public class ResourceManagementTest extends AbstractAuthorizationTest { resource.setScopes(scopes); - resourceResource.update(resource); - - ResourceRepresentation updated = resourceResource.toRepresentation(); + ResourceRepresentation updated = doUpdateResource(resource); assertEquals(2, resource.getScopes().size()); @@ -124,16 +162,13 @@ public class ResourceManagementTest extends AbstractAuthorizationTest { updated.setScopes(scopes); - resourceResource.update(updated); - - updated = resourceResource.toRepresentation(); + updated = doUpdateResource(updated); assertEquals(0, updated.getScopes().size()); } - private ResourceResource createResourceWithDefaultScopes() { - ResourceResource resourceResource = createResource(); - ResourceRepresentation resource = resourceResource.toRepresentation(); + private ResourceRepresentation createResourceWithDefaultScopes() { + ResourceRepresentation resource = createResource(); assertEquals(0, resource.getScopes().size()); @@ -145,9 +180,7 @@ public class ResourceManagementTest extends AbstractAuthorizationTest { resource.setScopes(scopes); - resourceResource.update(resource); - - return resourceResource; + return doUpdateResource(resource); } private boolean containsScope(String scopeName, ResourceRepresentation resource) { @@ -164,7 +197,7 @@ public class ResourceManagementTest extends AbstractAuthorizationTest { return false; } - private ResourceResource createResource() { + private ResourceRepresentation createResource() { ResourceRepresentation newResource = new ResourceRepresentation(); newResource.setName("Test Resource"); @@ -172,14 +205,36 @@ public class ResourceManagementTest extends AbstractAuthorizationTest { newResource.setType("test-resource"); newResource.setIconUri("icon-test-resource"); + return doCreateResource(newResource); + } + + protected ResourceRepresentation doCreateResource(ResourceRepresentation newResource) { ResourcesResource resources = getClientResource().authorization().resources(); Response response = resources.create(newResource); - assertEquals(Response.Status.CREATED.getStatusCode(), response.getStatus()); + int status = response.getStatus(); + + if (status != Response.Status.CREATED.getStatusCode()) { + throw new RuntimeException(new HttpResponseException("Error", status, "", null)); + } ResourceRepresentation stored = response.readEntity(ResourceRepresentation.class); - return resources.resource(stored.getId()); + return resources.resource(stored.getId()).toRepresentation(); + } + + protected ResourceRepresentation doUpdateResource(ResourceRepresentation resource) { + ResourcesResource resources = getClientResource().authorization().resources(); + ResourceResource existing = resources.resource(resource.getId()); + + existing.update(resource); + + return resources.resource(resource.getId()).toRepresentation(); + } + + protected void doRemoveResource(ResourceRepresentation resource) { + ResourcesResource resources = getClientResource().authorization().resources(); + resources.resource(resource.getId()).remove(); } } \ No newline at end of file diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/ResourceManagementWithAuthzClientTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/ResourceManagementWithAuthzClientTest.java new file mode 100644 index 0000000000..5f07b2ff55 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/ResourceManagementWithAuthzClientTest.java @@ -0,0 +1,127 @@ +/* + * Copyright 2017 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.admin.client.authorization; + +import java.io.IOException; +import java.util.stream.Collectors; + +import org.jetbrains.annotations.NotNull; +import org.keycloak.authorization.client.AuthzClient; +import org.keycloak.authorization.client.Configuration; +import org.keycloak.authorization.client.representation.RegistrationResponse; +import org.keycloak.representations.idm.authorization.ResourceOwnerRepresentation; +import org.keycloak.representations.idm.authorization.ResourceRepresentation; +import org.keycloak.representations.idm.authorization.ScopeRepresentation; +import org.keycloak.util.JsonSerialization; + +/** + * + * @author Pedro Igor + */ +public class ResourceManagementWithAuthzClientTest extends ResourceManagementTest { + + private AuthzClient authzClient; + + @Override + protected ResourceRepresentation doCreateResource(ResourceRepresentation newResource) { + org.keycloak.authorization.client.representation.ResourceRepresentation resource = toResourceRepresentation(newResource); + + AuthzClient authzClient = getAuthzClient(); + RegistrationResponse response = authzClient.protection().resource().create(resource); + + return toResourceRepresentation(authzClient, response.getId()); + } + + @Override + protected ResourceRepresentation doUpdateResource(ResourceRepresentation resource) { + AuthzClient authzClient = getAuthzClient(); + + authzClient.protection().resource().update(toResourceRepresentation(resource)); + + return toResourceRepresentation(authzClient, resource.getId()); + } + + @Override + protected void doRemoveResource(ResourceRepresentation resource) { + getAuthzClient().protection().resource().delete(resource.getId()); + } + + private ResourceRepresentation toResourceRepresentation(AuthzClient authzClient, String id) { + org.keycloak.authorization.client.representation.ResourceRepresentation created = authzClient.protection().resource().findById(id).getResourceDescription(); + ResourceRepresentation resourceRepresentation = new ResourceRepresentation(); + + resourceRepresentation.setId(created.getId()); + resourceRepresentation.setName(created.getName()); + resourceRepresentation.setIconUri(created.getIconUri()); + resourceRepresentation.setUri(created.getUri()); + resourceRepresentation.setType(created.getType()); + ResourceOwnerRepresentation owner = new ResourceOwnerRepresentation(); + + owner.setId(created.getOwner()); + + resourceRepresentation.setOwner(owner); + resourceRepresentation.setScopes(created.getScopes().stream().map(scopeRepresentation -> { + ScopeRepresentation scope = new ScopeRepresentation(); + + scope.setId(scopeRepresentation.getId()); + scope.setName(scopeRepresentation.getName()); + scope.setIconUri(scopeRepresentation.getIconUri()); + + return scope; + }).collect(Collectors.toSet())); + + return resourceRepresentation; + } + + private org.keycloak.authorization.client.representation.ResourceRepresentation toResourceRepresentation(ResourceRepresentation newResource) { + org.keycloak.authorization.client.representation.ResourceRepresentation resource = new org.keycloak.authorization.client.representation.ResourceRepresentation(); + + resource.setId(newResource.getId()); + resource.setName(newResource.getName()); + resource.setIconUri(newResource.getIconUri()); + resource.setUri(newResource.getUri()); + resource.setType(newResource.getType()); + + if (newResource.getOwner() != null) { + resource.setOwner(newResource.getOwner().getId()); + } + + resource.setScopes(newResource.getScopes().stream().map(scopeRepresentation -> { + org.keycloak.authorization.client.representation.ScopeRepresentation scope = new org.keycloak.authorization.client.representation.ScopeRepresentation(); + + scope.setName(scopeRepresentation.getName()); + scope.setIconUri(scopeRepresentation.getIconUri()); + + return scope; + }).collect(Collectors.toSet())); + + return resource; + } + + private AuthzClient getAuthzClient() { + if (authzClient == null) { + try { + authzClient = AuthzClient.create(JsonSerialization.readValue(getClass().getResourceAsStream("/authorization-test/default-keycloak.json"), Configuration.class)); + } catch (IOException cause) { + throw new RuntimeException("Failed to create authz client", cause); + } + } + + return authzClient; + } +} \ No newline at end of file diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/ResourceServerManagementTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/ResourceServerManagementTest.java new file mode 100644 index 0000000000..73e1961d30 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/ResourceServerManagementTest.java @@ -0,0 +1,68 @@ +/* + * Copyright 2017 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.admin.client.authorization; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import java.util.List; + +import org.junit.Test; +import org.keycloak.admin.client.resource.AuthorizationResource; +import org.keycloak.admin.client.resource.ClientsResource; +import org.keycloak.representations.idm.ClientRepresentation; +import org.keycloak.representations.idm.authorization.PolicyEnforcementMode; +import org.keycloak.util.JsonSerialization; + +/** + * + * @author Pedro Igor + */ +public class ResourceServerManagementTest extends AbstractAuthorizationTest { + + @Test + public void testCreateAndDeleteResourceServer() throws Exception { + ClientsResource clientsResource = testRealmResource().clients(); + + clientsResource.create(JsonSerialization.readValue(getClass().getResourceAsStream("/authorization-test/client-with-authz-settings.json"), ClientRepresentation.class)).close(); + + List clients = clientsResource.findByClientId("authz-client"); + + assertFalse(clients.isEmpty()); + + String clientId = clients.get(0).getId(); + AuthorizationResource settings = clientsResource.get(clientId).authorization(); + + assertEquals(PolicyEnforcementMode.PERMISSIVE, settings.exportSettings().getPolicyEnforcementMode()); + + assertFalse(settings.resources().findByName("Resource 1").isEmpty()); + assertFalse(settings.resources().findByName("Resource 15").isEmpty()); + assertFalse(settings.resources().findByName("Resource 20").isEmpty()); + + assertNotNull(settings.permissions().resource().findByName("Resource 15 Permission")); + assertNotNull(settings.policies().role().findByName("Resource 1 Policy")); + + clientsResource.get(clientId).remove(); + + clients = clientsResource.findByClientId("authz-client"); + + assertTrue(clients.isEmpty()); + } +} \ No newline at end of file diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/concurrency/AbstractConcurrencyTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/concurrency/AbstractConcurrencyTest.java index 86526b9ac5..fe20270f63 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/concurrency/AbstractConcurrencyTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/concurrency/AbstractConcurrencyTest.java @@ -19,85 +19,90 @@ package org.keycloak.testsuite.admin.concurrency; import org.keycloak.admin.client.Keycloak; import org.keycloak.admin.client.resource.RealmResource; +import org.keycloak.representations.idm.RealmRepresentation; +import org.keycloak.testsuite.AbstractTestRealmKeycloakTest; import java.util.LinkedList; +import java.util.Collection; import java.util.List; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.atomic.AtomicReference; -import java.util.concurrent.locks.Lock; -import java.util.concurrent.locks.ReentrantLock; -import org.keycloak.testsuite.admin.AbstractAdminTest; +import java.util.concurrent.Callable; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; /** * @author Stian Thorgersen */ -public abstract class AbstractConcurrencyTest extends AbstractAdminTest { +public abstract class AbstractConcurrencyTest extends AbstractTestRealmKeycloakTest { - private static final int DEFAULT_THREADS = 5; - private static final int DEFAULT_ITERATIONS = 20; + private static final int DEFAULT_THREADS = 4; + private static final int DEFAULT_NUMBER_OF_EXECUTIONS = 20 * DEFAULT_THREADS; + + public static final String REALM_NAME = "test"; // If enabled only one request is allowed at the time. Useful for checking that test is working. private static final boolean SYNCHRONIZED = false; - protected void run(final KeycloakRunnable runnable) throws Throwable { - run(runnable, DEFAULT_THREADS, DEFAULT_ITERATIONS); + @Override + public void configureTestRealm(RealmRepresentation testRealm) { } - protected void run(final KeycloakRunnable runnable, final int numThreads, final int numIterationsPerThread) throws Throwable { - final CountDownLatch latch = new CountDownLatch(numThreads); - final AtomicReference failed = new AtomicReference(); - final List threads = new LinkedList<>(); - final Lock lock = SYNCHRONIZED ? new ReentrantLock() : null; + protected void run(final KeycloakRunnable... runnables) { + run(DEFAULT_THREADS, DEFAULT_NUMBER_OF_EXECUTIONS, runnables); + } - for (int t = 0; t < numThreads; t++) { - final int threadNum = t; - Thread thread = new Thread() { - @Override - public void run() { - Keycloak keycloak = null; - try { - if (lock != null) { - lock.lock(); - } + protected void run(final int numThreads, final int totalNumberOfExecutions, final KeycloakRunnable... runnables) { + final ExecutorService service = SYNCHRONIZED + ? Executors.newSingleThreadExecutor() + : Executors.newFixedThreadPool(numThreads); - keycloak = Keycloak.getInstance(getAuthServerRoot().toString(), "master", "admin", "admin", org.keycloak.models.Constants.ADMIN_CLI_CLIENT_ID); - RealmResource realm = keycloak.realm(REALM_NAME); - for (int i = 0; i < numIterationsPerThread && latch.getCount() > 0; i++) { - log.infov("thread {0}, iteration {1}", threadNum, i); - runnable.run(keycloak, realm, threadNum, i); - } - latch.countDown(); - } catch (Throwable t) { - failed.compareAndSet(null, t); - while (latch.getCount() > 0) { - latch.countDown(); - } - } finally { - keycloak.close(); - if (lock != null) { - lock.unlock(); - } - } + ThreadLocal keycloaks = new ThreadLocal() { + @Override + protected Keycloak initialValue() { + return Keycloak.getInstance(getAuthServerRoot().toString(), "master", "admin", "admin", org.keycloak.models.Constants.ADMIN_CLI_CLIENT_ID); + } + }; + + AtomicInteger currentThreadIndex = new AtomicInteger(); + Collection> tasks = new LinkedList<>(); + Collection failures = new ConcurrentLinkedQueue<>(); + final List> runnablesToTasks = new LinkedList<>(); + for (KeycloakRunnable runnable : runnables) { + runnablesToTasks.add(() -> { + int arrayIndex = currentThreadIndex.getAndIncrement() % numThreads; + try { + runnable.run(arrayIndex % numThreads, keycloaks.get(), keycloaks.get().realm(REALM_NAME)); + } catch (Throwable ex) { + failures.add(ex); } - }; - thread.start(); - threads.add(thread); + return null; + }); + } + for (int i = 0; i < totalNumberOfExecutions; i ++) { + runnablesToTasks.forEach(tasks::add); } - latch.await(); - - for (Thread t : threads) { - t.join(); + try { + service.invokeAll(tasks); + service.shutdown(); + service.awaitTermination(3, TimeUnit.MINUTES); + } catch (InterruptedException ex) { + throw new RuntimeException(ex); } - if (failed.get() != null) { - throw failed.get(); + if (! failures.isEmpty()) { + RuntimeException ex = new RuntimeException("There were failures in threads. Failures count: " + failures.size()); + failures.forEach(ex::addSuppressed); + failures.forEach(e -> log.error(e.getMessage(), e)); + throw ex; } } protected interface KeycloakRunnable { - void run(Keycloak keycloak, RealmResource realm, int threadNum, int iterationNum); + void run(int threadIndex, Keycloak keycloak, RealmResource realm) throws Throwable; } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/concurrency/ConcurrencyTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/concurrency/ConcurrencyTest.java index a2f440932e..2d2053bd12 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/concurrency/ConcurrencyTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/concurrency/ConcurrencyTest.java @@ -17,12 +17,12 @@ package org.keycloak.testsuite.admin.concurrency; -import org.junit.Assert; -import org.junit.Ignore; import org.junit.Test; import org.keycloak.admin.client.Keycloak; import org.keycloak.admin.client.resource.ClientResource; +import org.keycloak.admin.client.resource.ClientsResource; import org.keycloak.admin.client.resource.RealmResource; +import org.keycloak.admin.client.resource.RolesResource; import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.GroupRepresentation; import org.keycloak.representations.idm.RoleRepresentation; @@ -31,7 +31,11 @@ import javax.ws.rs.NotFoundException; import javax.ws.rs.core.Response; import org.keycloak.testsuite.admin.ApiUtil; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicInteger; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; /** @@ -39,203 +43,198 @@ import static org.junit.Assert.fail; */ public class ConcurrencyTest extends AbstractConcurrencyTest { - boolean passedCreateClient = false; - boolean passedCreateRole = false; + public void concurrentTest(KeycloakRunnable... tasks) throws Throwable { + System.out.println("***************************"); + long start = System.currentTimeMillis(); + run(tasks); + long end = System.currentTimeMillis() - start; + System.out.println("took " + end + " ms"); + } - //@Test + @Test public void testAllConcurrently() throws Throwable { - Thread client = new Thread(new Runnable() { - @Override - public void run() { - try { - createClient(); - passedCreateClient = true; - } catch (Throwable throwable) { - throw new RuntimeException(throwable); - } - } - }); - Thread role = new Thread(new Runnable() { - @Override - public void run() { - try { - createRole(); - passedCreateRole = true; - } catch (Throwable throwable) { - throw new RuntimeException(throwable); - } - } - }); - - client.start(); - role.start(); - client.join(); - role.join(); - Assert.assertTrue(passedCreateClient); - Assert.assertTrue(passedCreateRole); + AtomicInteger uniqueCounter = new AtomicInteger(100000); + concurrentTest( + new CreateClient(uniqueCounter), + new CreateRemoveClient(uniqueCounter), + new CreateGroup(uniqueCounter), + new CreateRole(uniqueCounter) + ); } @Test public void createClient() throws Throwable { - System.out.println("***************************"); - long start = System.currentTimeMillis(); - run(new KeycloakRunnable() { - @Override - public void run(Keycloak keycloak, RealmResource realm, int threadNum, int iterationNum) { - String name = "c-" + threadNum + "-" + iterationNum; - ClientRepresentation c = new ClientRepresentation(); - c.setClientId(name); - Response response = realm.clients().create(c); - String id = ApiUtil.getCreatedId(response); - response.close(); - - c = realm.clients().get(id).toRepresentation(); - assertNotNull(c); - boolean found = false; - for (ClientRepresentation r : realm.clients().findAll()) { - if (r.getClientId().equals(name)) { - found = true; - break; - } - } - if (!found) { - fail("Client " + name + " not found in client list"); - } - } - }); - long end = System.currentTimeMillis() - start; - System.out.println("createClient took " + end); - + AtomicInteger uniqueCounter = new AtomicInteger(); + concurrentTest(new CreateClient(uniqueCounter)); } @Test public void createGroup() throws Throwable { - System.out.println("***************************"); - long start = System.currentTimeMillis(); - run(new KeycloakRunnable() { - @Override - public void run(Keycloak keycloak, RealmResource realm, int threadNum, int iterationNum) { - String name = "c-" + threadNum + "-" + iterationNum; - GroupRepresentation c = new GroupRepresentation(); - c.setName(name); - Response response = realm.groups().add(c); - String id = ApiUtil.getCreatedId(response); - response.close(); - - c = realm.groups().group(id).toRepresentation(); - assertNotNull(c); - boolean found = false; - for (GroupRepresentation r : realm.groups().groups()) { - if (r.getName().equals(name)) { - found = true; - break; - } - } - if (!found) { - fail("Group " + name + " not found in group list"); - } - } - }); - long end = System.currentTimeMillis() - start; - System.out.println("createGroup took " + end); - + AtomicInteger uniqueCounter = new AtomicInteger(); + concurrentTest(new CreateGroup(uniqueCounter)); } @Test - @Ignore public void createRemoveClient() throws Throwable { // FYI< this will fail as HSQL seems to be trying to perform table locks. - System.out.println("***************************"); - long start = System.currentTimeMillis(); - run(new KeycloakRunnable() { - @Override - public void run(Keycloak keycloak, RealmResource realm, int threadNum, int iterationNum) { - String name = "c-" + threadNum + "-" + iterationNum; - ClientRepresentation c = new ClientRepresentation(); - c.setClientId(name); - Response response = realm.clients().create(c); - String id = ApiUtil.getCreatedId(response); - response.close(); - - c = realm.clients().get(id).toRepresentation(); - assertNotNull(c); - boolean found = false; - for (ClientRepresentation r : realm.clients().findAll()) { - if (r.getClientId().equals(name)) { - found = true; - break; - } - } - if (!found) { - fail("Client " + name + " not found in client list"); - } - realm.clients().get(id).remove(); - try { - c = realm.clients().get(id).toRepresentation(); - fail("Client " + name + " should not be found. Should throw a 404"); - } catch (NotFoundException e) { - - } - found = false; - for (ClientRepresentation r : realm.clients().findAll()) { - if (r.getClientId().equals(name)) { - found = true; - break; - } - } - Assert.assertFalse("Client " + name + " should not be in client list", found); - - } - }); - long end = System.currentTimeMillis() - start; - System.out.println("createClient took " + end); - - } - - - @Test - public void createRole() throws Throwable { - long start = System.currentTimeMillis(); - run(new KeycloakRunnable() { - @Override - public void run(Keycloak keycloak, RealmResource realm, int threadNum, int iterationNum) { - String name = "r-" + threadNum + "-" + iterationNum; - RoleRepresentation r = new RoleRepresentation(name, null, false); - realm.roles().create(r); - assertNotNull(realm.roles().get(name).toRepresentation()); - } - }); - long end = System.currentTimeMillis() - start; - System.out.println("createRole took " + end); - + AtomicInteger uniqueCounter = new AtomicInteger(); + concurrentTest(new CreateRemoveClient(uniqueCounter)); } @Test public void createClientRole() throws Throwable { - long start = System.currentTimeMillis(); ClientRepresentation c = new ClientRepresentation(); c.setClientId("client"); - Response response = realm.clients().create(c); + Response response = adminClient.realm(REALM_NAME).clients().create(c); final String clientId = ApiUtil.getCreatedId(response); response.close(); - System.out.println("*********************************************"); - - run(new KeycloakRunnable() { - @Override - public void run(Keycloak keycloak, RealmResource realm, int threadNum, int iterationNum) { - String name = "r-" + threadNum + "-" + iterationNum; - RoleRepresentation r = new RoleRepresentation(name, null, false); - - ClientResource client = realm.clients().get(clientId); - client.roles().create(r); - - assertNotNull(client.roles().get(name).toRepresentation()); - } - }); - long end = System.currentTimeMillis() - start; - System.out.println("createClientRole took " + end); - System.out.println("*********************************************"); - + AtomicInteger uniqueCounter = new AtomicInteger(); + concurrentTest(new CreateClientRole(uniqueCounter, clientId)); } + + @Test + public void createRole() throws Throwable { + AtomicInteger uniqueCounter = new AtomicInteger(); + run(new CreateRole(uniqueCounter)); + } + + private class CreateClient implements KeycloakRunnable { + + private final AtomicInteger clientIndex; + + public CreateClient(AtomicInteger clientIndex) { + this.clientIndex = clientIndex; + } + + @Override + public void run(int threadIndex, Keycloak keycloak, RealmResource realm) throws Throwable { + String name = "c-" + clientIndex.getAndIncrement(); + ClientRepresentation c = new ClientRepresentation(); + c.setClientId(name); + Response response = realm.clients().create(c); + String id = ApiUtil.getCreatedId(response); + response.close(); + + c = realm.clients().get(id).toRepresentation(); + assertNotNull(c); + assertTrue("Client " + name + " not found in client list", + realm.clients().findAll().stream() + .map(ClientRepresentation::getClientId) + .filter(Objects::nonNull) + .anyMatch(name::equals)); + } + } + + private class CreateRemoveClient implements KeycloakRunnable { + + private final AtomicInteger clientIndex; + + public CreateRemoveClient(AtomicInteger clientIndex) { + this.clientIndex = clientIndex; + } + + @Override + public void run(int threadIndex, Keycloak keycloak, RealmResource realm) throws Throwable { + String name = "c-" + clientIndex.getAndIncrement(); + ClientRepresentation c = new ClientRepresentation(); + c.setClientId(name); + final ClientsResource clients = realm.clients(); + + Response response = clients.create(c); + String id = ApiUtil.getCreatedId(response); + response.close(); + final ClientResource client = clients.get(id); + + c = client.toRepresentation(); + assertNotNull(c); + assertTrue("Client " + name + " not found in client list", + clients.findAll().stream() + .map(ClientRepresentation::getClientId) + .filter(Objects::nonNull) + .anyMatch(name::equals)); + + client.remove(); + try { + client.toRepresentation(); + fail("Client " + name + " should not be found. Should throw a 404"); + } catch (NotFoundException e) { + + } + + assertFalse("Client " + name + " should now not present in client list", + clients.findAll().stream() + .map(ClientRepresentation::getClientId) + .filter(Objects::nonNull) + .anyMatch(name::equals)); + } + } + + private class CreateGroup implements KeycloakRunnable { + + private final AtomicInteger uniqueIndex; + + public CreateGroup(AtomicInteger uniqueIndex) { + this.uniqueIndex = uniqueIndex; + } + + @Override + public void run(int threadIndex, Keycloak keycloak, RealmResource realm) throws Throwable { + String name = "g-" + uniqueIndex.getAndIncrement(); + GroupRepresentation c = new GroupRepresentation(); + c.setName(name); + Response response = realm.groups().add(c); + String id = ApiUtil.getCreatedId(response); + response.close(); + + c = realm.groups().group(id).toRepresentation(); + assertNotNull(c); + assertTrue("Group " + name + " not found in group list", + realm.groups().groups().stream() + .map(GroupRepresentation::getName) + .filter(Objects::nonNull) + .anyMatch(name::equals)); + } + } + + private class CreateClientRole implements KeycloakRunnable { + + private final AtomicInteger uniqueCounter; + private final String clientId; + + public CreateClientRole(AtomicInteger uniqueCounter, String clientId) { + this.uniqueCounter = uniqueCounter; + this.clientId = clientId; + } + + @Override + public void run(int threadIndex, Keycloak keycloak, RealmResource realm) throws Throwable { + String name = "cr-" + uniqueCounter.getAndIncrement(); + RoleRepresentation r = new RoleRepresentation(name, null, false); + + final RolesResource roles = realm.clients().get(clientId).roles(); + roles.create(r); + assertNotNull(roles.get(name).toRepresentation()); + } + } + + private class CreateRole implements KeycloakRunnable { + + private final AtomicInteger uniqueCounter; + + public CreateRole(AtomicInteger uniqueCounter) { + this.uniqueCounter = uniqueCounter; + } + + @Override + public void run(int threadIndex, Keycloak keycloak, RealmResource realm) throws Throwable { + String name = "r-" + uniqueCounter.getAndIncrement(); + RoleRepresentation r = new RoleRepresentation(name, null, false); + + final RolesResource roles = realm.roles(); + roles.create(r); + assertNotNull(roles.get(name).toRepresentation()); + } + } + } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/concurrency/ConcurrentLoginTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/concurrency/ConcurrentLoginTest.java index ade3995369..ff6f10f459 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/concurrency/ConcurrentLoginTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/concurrency/ConcurrentLoginTest.java @@ -17,14 +17,11 @@ package org.keycloak.testsuite.admin.concurrency; -import java.io.BufferedReader; import java.io.IOException; -import java.io.InputStreamReader; import java.io.UnsupportedEncodingException; import java.net.URI; import java.net.URISyntaxException; import java.util.ArrayList; -import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -49,9 +46,21 @@ import org.junit.Before; import org.junit.Test; import org.keycloak.OAuth2Constants; import org.keycloak.admin.client.Keycloak; +import org.keycloak.admin.client.resource.ClientsResource; import org.keycloak.admin.client.resource.RealmResource; +import org.keycloak.representations.AccessToken; import org.keycloak.representations.idm.ClientRepresentation; +import org.keycloak.testsuite.Retry; +import org.keycloak.testsuite.admin.ApiUtil; +import org.keycloak.testsuite.util.ClientBuilder; import org.keycloak.testsuite.util.OAuthClient; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; +import org.apache.http.client.CookieStore; +import org.apache.http.impl.client.BasicCookieStore; +import org.hamcrest.Matchers; @@ -60,88 +69,95 @@ import org.keycloak.testsuite.util.OAuthClient; */ public class ConcurrentLoginTest extends AbstractConcurrencyTest { - private static final int DEFAULT_THREADS = 10; - private static final int DEFAULT_ITERATIONS = 20; - private static final int CLIENTS_PER_THREAD = 10; - private static final int DEFAULT_CLIENTS_COUNT = CLIENTS_PER_THREAD * DEFAULT_THREADS; - + protected static final int DEFAULT_THREADS = 4; + protected static final int CLIENTS_PER_THREAD = 30; + protected static final int DEFAULT_CLIENTS_COUNT = CLIENTS_PER_THREAD * DEFAULT_THREADS; + @Before public void beforeTest() { + createClients(); + } + + protected void createClients() { + final ClientsResource clients = adminClient.realm(REALM_NAME).clients(); for (int i = 0; i < DEFAULT_CLIENTS_COUNT; i++) { - ClientRepresentation client = new ClientRepresentation(); - client.setClientId("client" + i); - client.setDirectAccessGrantsEnabled(true); - client.setRedirectUris(Arrays.asList("http://localhost:8180/auth/realms/master/app/*")); - client.setWebOrigins(Arrays.asList("http://localhost:8180")); - client.setSecret("password"); - - log.debug("creating " + client.getClientId()); - Response create = adminClient.realm("test").clients().create(client); - Assert.assertEquals(Response.Status.CREATED, create.getStatusInfo()); + ClientRepresentation client = ClientBuilder.create() + .clientId("client" + i) + .directAccessGrants() + .redirectUris("http://localhost:8180/auth/realms/master/app/*") + .addWebOrigin("http://localhost:8180") + .secret("password") + .build(); + + Response create = clients.create(client); + String clientId = ApiUtil.getCreatedId(create); create.close(); + getCleanup(REALM_NAME).addClientUuid(clientId); + log.debugf("created %s [uuid=%s]", client.getClientId(), clientId); } log.debug("clients created"); } - - @Override - protected void run(final KeycloakRunnable runnable) throws Throwable { - run(runnable, DEFAULT_THREADS, DEFAULT_ITERATIONS); - } - + @Test - public void concurrentLogin() throws Throwable { - System.out.println("*********************************************"); + public void concurrentLoginSingleUser() throws Throwable { + log.info("*********************************************"); long start = System.currentTimeMillis(); + AtomicReference userSessionId = new AtomicReference<>(); + LoginTask loginTask = null; + try (CloseableHttpClient httpClient = HttpClientBuilder.create().setRedirectStrategy(new LaxRedirectStrategy()).build()) { - - HttpUriRequest request = handleLogin(getPageContent(oauth.getLoginFormUrl(), httpClient, null), "test-user@localhost", "password"); - - log.debug("Executing login request"); - - Assert.assertTrue(parseAndCloseResponse(httpClient.execute(request)).contains("AUTH_RESPONSE")); - - run(new KeycloakRunnable() { - @Override - public void run(Keycloak keycloak, RealmResource realm, int threadNum, int iterationNum) { - OAuthClient oauth = new OAuthClient(); - oauth.init(adminClient, driver); - - int startIndex = CLIENTS_PER_THREAD * threadNum; - for (int i = startIndex; i < startIndex + CLIENTS_PER_THREAD; i++) { - oauth.clientId("client" + i); - log.trace("Accessing login page for " + oauth.getClientId() + " threat " + threadNum + " iteration " + iterationNum); - try { - final HttpClientContext context = HttpClientContext.create(); - - String pageContent = getPageContent(oauth.getLoginFormUrl(), httpClient, context); - String currentUrl = context.getRedirectLocations().get(0).toString(); - - Assert.assertTrue(pageContent.contains("AUTH_RESPONSE")); - - String code = getQueryFromUrl(currentUrl).get(OAuth2Constants.CODE); - OAuthClient.AccessTokenResponse accessRes = oauth.doAccessTokenRequest(code, "password"); - Assert.assertEquals("AccessTokenResponse: error: '" + accessRes.getError() + "' desc: '" + accessRes.getErrorDescription() + "'", - 200, accessRes.getStatusCode()); - - OAuthClient.AccessTokenResponse refreshRes = oauth.doRefreshTokenRequest(accessRes.getRefreshToken(), "password"); - Assert.assertEquals("AccessTokenResponse: error: '" + refreshRes.getError() + "' desc: '" + refreshRes.getErrorDescription() + "'", - 200, refreshRes.getStatusCode()); - } catch (Exception ex) { - throw new RuntimeException(ex); - } - } - } - }); - } - - long end = System.currentTimeMillis() - start; - System.out.println("concurrentLogin took " + (end/1000) + "s"); - System.out.println("*********************************************"); + loginTask = new LoginTask(httpClient, userSessionId, 100, 1, Arrays.asList( + createHttpClientContextForUser(httpClient, "test-user@localhost", "password") + )); + run(DEFAULT_THREADS, DEFAULT_CLIENTS_COUNT, loginTask); + int clientSessionsCount = testingClient.testing().getClientSessionsCountInUserSession("test", userSessionId.get()); + Assert.assertEquals(1 + DEFAULT_CLIENTS_COUNT, clientSessionsCount); + } finally { + long end = System.currentTimeMillis() - start; + log.infof("Statistics: %s", loginTask == null ? "??" : loginTask.getHistogram()); + log.info("concurrentLoginSingleUser took " + (end/1000) + "s"); + log.info("*********************************************"); + } } - - private String getPageContent(String url, CloseableHttpClient httpClient, HttpClientContext context) throws Exception { + protected HttpClientContext createHttpClientContextForUser(final CloseableHttpClient httpClient, String userName, String password) throws IOException { + final HttpClientContext context = HttpClientContext.create(); + CookieStore cookieStore = new BasicCookieStore(); + context.setCookieStore(cookieStore); + HttpUriRequest request = handleLogin(getPageContent(oauth.getLoginFormUrl(), httpClient, context), userName, password); + log.debug("Executing login request"); + Assert.assertTrue(parseAndCloseResponse(httpClient.execute(request, context)).contains("AUTH_RESPONSE")); + return context; + } + + @Test + public void concurrentLoginMultipleUsers() throws Throwable { + log.info("*********************************************"); + long start = System.currentTimeMillis(); + + AtomicReference userSessionId = new AtomicReference<>(); + LoginTask loginTask = null; + + try (CloseableHttpClient httpClient = HttpClientBuilder.create().setRedirectStrategy(new LaxRedirectStrategy()).build()) { + loginTask = new LoginTask(httpClient, userSessionId, 100, 1, Arrays.asList( + createHttpClientContextForUser(httpClient, "test-user@localhost", "password"), + createHttpClientContextForUser(httpClient, "john-doh@localhost", "password"), + createHttpClientContextForUser(httpClient, "roleRichUser", "password") + )); + + run(DEFAULT_THREADS, DEFAULT_CLIENTS_COUNT, loginTask); + int clientSessionsCount = testingClient.testing().getClientSessionsCountInUserSession("test", userSessionId.get()); + Assert.assertEquals(1 + DEFAULT_CLIENTS_COUNT / 3 + (DEFAULT_CLIENTS_COUNT % 3 <= 0 ? 0 : 1), clientSessionsCount); + } finally { + long end = System.currentTimeMillis() - start; + log.infof("Statistics: %s", loginTask == null ? "??" : loginTask.getHistogram()); + log.info("concurrentLoginMultipleUsers took " + (end/1000) + "s"); + log.info("*********************************************"); + } + } + + protected String getPageContent(String url, CloseableHttpClient httpClient, HttpClientContext context) throws IOException { HttpGet request = new HttpGet(url); request.setHeader("User-Agent", "Mozilla/5.0"); @@ -149,31 +165,18 @@ public class ConcurrentLoginTest extends AbstractConcurrencyTest { "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"); request.setHeader("Accept-Language", "en-US,en;q=0.5"); - if (context != null) { - return parseAndCloseResponse(httpClient.execute(request, context)); - } else { - return parseAndCloseResponse(httpClient.execute(request)); - } - + return parseAndCloseResponse(httpClient.execute(request, context)); } - private String parseAndCloseResponse(CloseableHttpResponse response) throws UnsupportedOperationException, IOException { + protected String parseAndCloseResponse(CloseableHttpResponse response) { try { int responseCode = response.getStatusLine().getStatusCode(); + String resp = EntityUtils.toString(response.getEntity()); + if (responseCode != 200) { - log.debug("Response Code : " + responseCode); + log.debugf("Response Code: %d, Body: %s", responseCode, resp); } - BufferedReader rd = new BufferedReader( - new InputStreamReader(response.getEntity().getContent())); - StringBuilder result = new StringBuilder(); - String line; - while ((line = rd.readLine()) != null) { - result.append(line); - } - if (responseCode != 200) { - log.debug(result.toString()); - } - return result.toString(); + return resp; } catch (IOException | UnsupportedOperationException ex) { throw new RuntimeException(ex); } finally { @@ -185,16 +188,15 @@ public class ConcurrentLoginTest extends AbstractConcurrencyTest { } } } - - private HttpUriRequest handleLogin(String html, String username, String password) throws UnsupportedEncodingException { - System.out.println("Extracting form's data..."); + protected HttpUriRequest handleLogin(String html, String username, String password) throws UnsupportedEncodingException { + log.debug("Extracting form's data..."); // Keycloak form id Element loginform = Jsoup.parse(html).getElementById("kc-form-login"); String method = loginform.attr("method"); String action = loginform.attr("action"); - + List paramList = new ArrayList<>(); for (Element inputElement : loginform.getElementsByTag("input")) { @@ -206,9 +208,9 @@ public class ConcurrentLoginTest extends AbstractConcurrencyTest { paramList.add(new BasicNameValuePair(key, password)); } } - + boolean isPost = method != null && "post".equalsIgnoreCase(method); - + if (isPost) { HttpPost req = new HttpPost(action); @@ -225,8 +227,8 @@ public class ConcurrentLoginTest extends AbstractConcurrencyTest { throw new UnsupportedOperationException("not supported yet!"); } } - - private Map getQueryFromUrl(String url) throws URISyntaxException { + + private static Map getQueryFromUrl(String url) throws URISyntaxException { Map m = new HashMap<>(); List pairs = URLEncodedUtils.parse(new URI(url), "UTF-8"); for (NameValuePair p : pairs) { @@ -235,5 +237,98 @@ public class ConcurrentLoginTest extends AbstractConcurrencyTest { return m; } + public class LoginTask implements KeycloakRunnable { + + private final AtomicInteger clientIndex = new AtomicInteger(); + private final ThreadLocal oauthClient = new ThreadLocal() { + @Override + protected OAuthClient initialValue() { + OAuthClient oauth1 = new OAuthClient(); + oauth1.init(adminClient, driver); + return oauth1; + } + }; + + private final CloseableHttpClient httpClient; + private final AtomicReference userSessionId; + + private final int retryDelayMs; + private final int retryCount; + private final AtomicInteger[] retryHistogram; + private final AtomicInteger totalInvocations = new AtomicInteger(); + private final List clientContexts; + + public LoginTask(CloseableHttpClient httpClient, AtomicReference userSessionId, int retryDelayMs, int retryCount, List clientContexts) { + this.httpClient = httpClient; + this.userSessionId = userSessionId; + this.retryDelayMs = retryDelayMs; + this.retryCount = retryCount; + this.retryHistogram = new AtomicInteger[retryCount]; + for (int i = 0; i < retryHistogram.length; i ++) { + retryHistogram[i] = new AtomicInteger(); + } + this.clientContexts = clientContexts; + } + + @Override + public void run(int threadIndex, Keycloak keycloak, RealmResource realm) throws Throwable { + int i = clientIndex.getAndIncrement(); + OAuthClient oauth1 = oauthClient.get(); + oauth1.clientId("client" + i); + log.infof("%d [%s]: Accessing login page for %s", threadIndex, Thread.currentThread().getName(), oauth1.getClientId()); + + final HttpClientContext templateContext = clientContexts.get(i % clientContexts.size()); + final HttpClientContext context = HttpClientContext.create(); + context.setCookieStore(templateContext.getCookieStore()); + String pageContent = getPageContent(oauth1.getLoginFormUrl(), httpClient, context); + Assert.assertThat(pageContent, Matchers.containsString("AUTH_RESPONSE")); + Assert.assertThat(context.getRedirectLocations(), Matchers.notNullValue()); + Assert.assertThat(context.getRedirectLocations(), Matchers.not(Matchers.empty())); + String currentUrl = context.getRedirectLocations().get(0).toString(); + String code = getQueryFromUrl(currentUrl).get(OAuth2Constants.CODE); + + AtomicReference accessResRef = new AtomicReference<>(); + totalInvocations.incrementAndGet(); + + // obtain access + refresh token via code-to-token flow + OAuthClient.AccessTokenResponse accessRes = oauth1.doAccessTokenRequest(code, "password"); + Assert.assertEquals("AccessTokenResponse: client: " + oauth1.getClientId() + ", error: '" + accessRes.getError() + "' desc: '" + accessRes.getErrorDescription() + "'", + 200, accessRes.getStatusCode()); + accessResRef.set(accessRes); + + // Refresh access + refresh token using refresh token + int invocationIndex = Retry.execute(() -> { + OAuthClient.AccessTokenResponse refreshRes = oauth1.doRefreshTokenRequest(accessResRef.get().getRefreshToken(), "password"); + Assert.assertEquals("AccessTokenResponse: client: " + oauth1.getClientId() + ", error: '" + refreshRes.getError() + "' desc: '" + refreshRes.getErrorDescription() + "'", + 200, refreshRes.getStatusCode()); + }, retryCount, retryDelayMs); + + retryHistogram[invocationIndex].incrementAndGet(); + + if (userSessionId.get() == null) { + AccessToken token = oauth1.verifyToken(accessResRef.get().getAccessToken()); + userSessionId.set(token.getSessionState()); + } + } + + public int getRetryDelayMs() { + return retryDelayMs; + } + + public int getRetryCount() { + return retryCount; + } + + public Map getHistogram() { + Map res = new LinkedHashMap<>(retryCount); + for (int i = 0; i < retryHistogram.length; i ++) { + AtomicInteger item = retryHistogram[i]; + + res.put(i * retryDelayMs, item.get()); + } + return res; + } + } + } \ No newline at end of file diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/AbstractBrokerTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/AbstractBrokerTest.java index 6500ad0ce1..6332dd0c78 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/AbstractBrokerTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/AbstractBrokerTest.java @@ -1,6 +1,5 @@ package org.keycloak.testsuite.broker; -import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -14,7 +13,6 @@ import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.RoleRepresentation; import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.testsuite.Assert; -import org.keycloak.testsuite.admin.ApiUtil; import org.keycloak.testsuite.pages.ConsentPage; import org.keycloak.testsuite.util.*; @@ -40,6 +38,8 @@ import static org.keycloak.testsuite.broker.BrokerTestTools.*; public abstract class AbstractBrokerTest extends AbstractBaseBrokerTest { + protected IdentityProviderResource identityProviderResource; + @Before public void beforeBrokerTest() { log.debug("creating user for realm " + bc.providerRealmName()); @@ -61,7 +61,8 @@ public abstract class AbstractBrokerTest extends AbstractBaseBrokerTest { log.debug("adding identity provider to realm " + bc.consumerRealmName()); RealmResource realm = adminClient.realm(bc.consumerRealmName()); - realm.identityProviders().create(bc.setUpIdentityProvider(suiteContext)); + realm.identityProviders().create(bc.setUpIdentityProvider(suiteContext)).close(); + identityProviderResource = realm.identityProviders().get(bc.getIDPAlias()); // addClients List clients = bc.createProviderClients(suiteContext); @@ -70,7 +71,7 @@ public abstract class AbstractBrokerTest extends AbstractBaseBrokerTest { for (ClientRepresentation client : clients) { log.debug("adding client " + client.getName() + " to realm " + bc.providerRealmName()); - providerRealm.clients().create(client); + providerRealm.clients().create(client).close(); } } @@ -80,7 +81,7 @@ public abstract class AbstractBrokerTest extends AbstractBaseBrokerTest { for (ClientRepresentation client : clients) { log.debug("adding client " + client.getName() + " to realm " + bc.consumerRealmName()); - consumerRealm.clients().create(client); + consumerRealm.clients().create(client).close(); } } @@ -90,6 +91,12 @@ public abstract class AbstractBrokerTest extends AbstractBaseBrokerTest { @Test public void testLogInAsUserInIDP() { + loginUser(); + + testSingleLogout(); + } + + protected void loginUser() { driver.navigate().to(getAccountUrl(bc.consumerRealmName())); log.debug("Clicking social " + bc.getIDPAlias()); @@ -98,16 +105,16 @@ public abstract class AbstractBrokerTest extends AbstractBaseBrokerTest { waitForPage(driver, "log in to"); Assert.assertTrue("Driver should be on the provider realm page right now", - driver.getCurrentUrl().contains("/auth/realms/" + bc.providerRealmName() + "/")); + driver.getCurrentUrl().contains("/auth/realms/" + bc.providerRealmName() + "/")); log.debug("Logging in"); accountLoginPage.login(bc.getUserLogin(), bc.getUserPassword()); waitForPage(driver, "update account information"); - Assert.assertTrue(updateAccountInformationPage.isCurrent()); + updateAccountInformationPage.assertCurrent(); Assert.assertTrue("We must be on correct realm right now", - driver.getCurrentUrl().contains("/auth/realms/" + bc.consumerRealmName() + "/")); + driver.getCurrentUrl().contains("/auth/realms/" + bc.consumerRealmName() + "/")); log.debug("Updating info on updateAccount page"); updateAccountInformationPage.updateAccountInformation(bc.getUserLogin(), bc.getUserEmail(), "Firstname", "Lastname"); @@ -128,9 +135,7 @@ public abstract class AbstractBrokerTest extends AbstractBaseBrokerTest { } Assert.assertTrue("There must be user " + bc.getUserLogin() + " in realm " + bc.consumerRealmName(), - isUserFound); - - testSingleLogout(); + isUserFound); } @Test diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlBrokerConfiguration.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlBrokerConfiguration.java index e5f5b8df81..da0bc2bbcc 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlBrokerConfiguration.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlBrokerConfiguration.java @@ -6,6 +6,7 @@ package org.keycloak.testsuite.broker; import org.keycloak.protocol.ProtocolMapperUtils; +import org.keycloak.protocol.saml.SamlConfigAttributes; import org.keycloak.protocol.saml.SamlProtocol; import org.keycloak.protocol.saml.mappers.AttributeStatementHelper; import org.keycloak.protocol.saml.mappers.UserAttributeStatementMapper; @@ -22,6 +23,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import static org.keycloak.broker.saml.SAMLIdentityProviderConfig.*; import static org.keycloak.testsuite.broker.BrokerTestConstants.*; import static org.keycloak.testsuite.broker.BrokerTestTools.*; @@ -63,17 +65,17 @@ public class KcSamlBrokerConfiguration implements BrokerConfiguration { Map attributes = new HashMap<>(); - attributes.put("saml.authnstatement", "true"); - attributes.put("saml_single_logout_service_url_post", + attributes.put(SamlConfigAttributes.SAML_AUTHNSTATEMENT, "true"); + attributes.put(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_POST_ATTRIBUTE, getAuthRoot(suiteContext) + "/auth/realms/" + REALM_CONS_NAME + "/broker/" + IDP_SAML_ALIAS + "/endpoint"); - attributes.put("saml_assertion_consumer_url_post", + attributes.put(SamlProtocol.SAML_ASSERTION_CONSUMER_URL_POST_ATTRIBUTE, getAuthRoot(suiteContext) + "/auth/realms/" + REALM_CONS_NAME + "/broker/" + IDP_SAML_ALIAS + "/endpoint"); - attributes.put("saml_force_name_id_format", "true"); - attributes.put("saml_name_id_format", "username"); - attributes.put("saml.assertion.signature", "false"); - attributes.put("saml.server.signature", "false"); - attributes.put("saml.client.signature", "false"); - attributes.put("saml.encrypt", "false"); + attributes.put(SamlConfigAttributes.SAML_FORCE_NAME_ID_FORMAT_ATTRIBUTE, "true"); + attributes.put(SamlConfigAttributes.SAML_NAME_ID_FORMAT_ATTRIBUTE, "username"); + attributes.put(SamlConfigAttributes.SAML_ASSERTION_SIGNATURE, "false"); + attributes.put(SamlConfigAttributes.SAML_SERVER_SIGNATURE, "false"); + attributes.put(SamlConfigAttributes.SAML_CLIENT_SIGNATURE_ATTRIBUTE, "false"); + attributes.put(SamlConfigAttributes.SAML_ENCRYPT, "false"); client.setAttributes(attributes); @@ -133,15 +135,15 @@ public class KcSamlBrokerConfiguration implements BrokerConfiguration { Map config = idp.getConfig(); - config.put("singleSignOnServiceUrl", getAuthRoot(suiteContext) + "/auth/realms/" + REALM_PROV_NAME + "/protocol/saml"); - config.put("singleLogoutServiceUrl", getAuthRoot(suiteContext) + "/auth/realms/" + REALM_PROV_NAME + "/protocol/saml"); - config.put("nameIDPolicyFormat", "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"); - config.put("forceAuthn", "true"); - config.put("postBindingResponse", "true"); - config.put("postBindingAuthnRequest", "true"); - config.put("validateSignature", "false"); - config.put("wantAuthnRequestsSigned", "false"); - config.put("backchannelSupported", "true"); + config.put(SINGLE_SIGN_ON_SERVICE_URL, getAuthRoot(suiteContext) + "/auth/realms/" + REALM_PROV_NAME + "/protocol/saml"); + config.put(SINGLE_LOGOUT_SERVICE_URL, getAuthRoot(suiteContext) + "/auth/realms/" + REALM_PROV_NAME + "/protocol/saml"); + config.put(NAME_ID_POLICY_FORMAT, "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"); + config.put(FORCE_AUTHN, "true"); + config.put(POST_BINDING_RESPONSE, "true"); + config.put(POST_BINDING_AUTHN_REQUEST, "true"); + config.put(VALIDATE_SIGNATURE, "false"); + config.put(WANT_AUTHN_REQUESTS_SIGNED, "false"); + config.put(BACKCHANNEL_SUPPORTED, "true"); return idp; } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlSignedBrokerTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlSignedBrokerTest.java index b4825cdb38..ef23a9a8e9 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlSignedBrokerTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlSignedBrokerTest.java @@ -1,19 +1,29 @@ package org.keycloak.testsuite.broker; +import org.keycloak.admin.client.resource.ClientResource; +import org.keycloak.broker.saml.SAMLIdentityProviderConfig; +import org.keycloak.protocol.saml.SamlConfigAttributes; import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.IdentityProviderRepresentation; import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.testsuite.arquillian.SuiteContext; +import org.keycloak.testsuite.updaters.ClientAttributeUpdater; +import org.keycloak.testsuite.updaters.IdentityProviderAttributeUpdater; +import java.io.Closeable; import java.util.HashMap; import java.util.List; import java.util.Map; +import org.hamcrest.Matchers; +import org.junit.Assert; +import org.junit.Test; import static org.keycloak.testsuite.broker.BrokerTestConstants.*; +import static org.keycloak.testsuite.broker.BrokerTestTools.encodeUrl; public class KcSamlSignedBrokerTest extends KcSamlBrokerTest { - public static class KcSamlSignedBrokerConfiguration extends KcSamlBrokerConfiguration { + public class KcSamlSignedBrokerConfiguration extends KcSamlBrokerConfiguration { @Override public RealmRepresentation createProviderRealm() { @@ -39,6 +49,9 @@ public class KcSamlSignedBrokerTest extends KcSamlBrokerTest { public List createProviderClients(SuiteContext suiteContext) { List clientRepresentationList = super.createProviderClients(suiteContext); + String consumerCert = adminClient.realm(consumerRealmName()).keys().getKeyMetadata().getKeys().get(0).getCertificate(); + Assert.assertThat(consumerCert, Matchers.notNullValue()); + for (ClientRepresentation client : clientRepresentationList) { client.setClientAuthenticatorType("client-secret"); client.setSurrogateAuthRequired(false); @@ -49,12 +62,11 @@ public class KcSamlSignedBrokerTest extends KcSamlBrokerTest { client.setAttributes(attributes); } - attributes.put("saml.assertion.signature", "true"); - attributes.put("saml.server.signature", "true"); - attributes.put("saml.client.signature", "true"); - attributes.put("saml.signature.algorithm", "RSA_SHA256"); - attributes.put("saml.signing.private.key", IDP_SAML_SIGN_KEY); - attributes.put("saml.signing.certificate", IDP_SAML_SIGN_CERT); + attributes.put(SamlConfigAttributes.SAML_ASSERTION_SIGNATURE, "true"); + attributes.put(SamlConfigAttributes.SAML_SERVER_SIGNATURE, "true"); + attributes.put(SamlConfigAttributes.SAML_CLIENT_SIGNATURE_ATTRIBUTE, "true"); + attributes.put(SamlConfigAttributes.SAML_SIGNATURE_ALGORITHM, "RSA_SHA256"); + attributes.put(SamlConfigAttributes.SAML_SIGNING_CERTIFICATE_ATTRIBUTE, consumerCert); } return clientRepresentationList; @@ -64,11 +76,15 @@ public class KcSamlSignedBrokerTest extends KcSamlBrokerTest { public IdentityProviderRepresentation setUpIdentityProvider(SuiteContext suiteContext) { IdentityProviderRepresentation result = super.setUpIdentityProvider(suiteContext); + String providerCert = adminClient.realm(providerRealmName()).keys().getKeyMetadata().getKeys().get(0).getCertificate(); + Assert.assertThat(providerCert, Matchers.notNullValue()); + Map config = result.getConfig(); - config.put("validateSignature", "true"); - config.put("wantAuthnRequestsSigned", "true"); - config.put("signingCertificate", IDP_SAML_SIGN_CERT); + config.put(SAMLIdentityProviderConfig.VALIDATE_SIGNATURE, "true"); + config.put(SAMLIdentityProviderConfig.WANT_ASSERTIONS_SIGNED, "true"); + config.put(SAMLIdentityProviderConfig.WANT_AUTHN_REQUESTS_SIGNED, "true"); + config.put(SAMLIdentityProviderConfig.SIGNING_CERTIFICATE_KEY, providerCert); return result; } @@ -76,7 +92,50 @@ public class KcSamlSignedBrokerTest extends KcSamlBrokerTest { @Override protected BrokerConfiguration getBrokerConfiguration() { - return KcSamlSignedBrokerConfiguration.INSTANCE; + return new KcSamlSignedBrokerConfiguration(); } + @Test + public void testSignedEncryptedAssertions() throws Exception { + ClientRepresentation client = adminClient.realm(bc.providerRealmName()) + .clients() + .findByClientId(bc.getIDPClientIdInProviderRealm(suiteContext)) + .get(0); + + final ClientResource clientResource = realmsResouce().realm(bc.providerRealmName()).clients().get(client.getId()); + Assert.assertThat(clientResource, Matchers.notNullValue()); + + String providerCert = adminClient.realm(bc.providerRealmName()).keys().getKeyMetadata().getKeys().get(0).getCertificate(); + Assert.assertThat(providerCert, Matchers.notNullValue()); + + String consumerCert = adminClient.realm(bc.consumerRealmName()).keys().getKeyMetadata().getKeys().get(0).getCertificate(); + Assert.assertThat(consumerCert, Matchers.notNullValue()); + + try (Closeable idpUpdater = new IdentityProviderAttributeUpdater(identityProviderResource) + .setAttribute(SAMLIdentityProviderConfig.VALIDATE_SIGNATURE, "true") + .setAttribute(SAMLIdentityProviderConfig.WANT_ASSERTIONS_SIGNED, "true") + .setAttribute(SAMLIdentityProviderConfig.WANT_ASSERTIONS_ENCRYPTED, "true") + .setAttribute(SAMLIdentityProviderConfig.WANT_AUTHN_REQUESTS_SIGNED, "false") + .setAttribute(SAMLIdentityProviderConfig.SIGNING_CERTIFICATE_KEY, providerCert) + .update(); + Closeable clientUpdater = new ClientAttributeUpdater(clientResource) + .setAttribute(SamlConfigAttributes.SAML_ENCRYPT, "true") + .setAttribute(SamlConfigAttributes.SAML_ENCRYPTION_CERTIFICATE_ATTRIBUTE, consumerCert) + .setAttribute(SamlConfigAttributes.SAML_SERVER_SIGNATURE, "false") // only sign assertions + .setAttribute(SamlConfigAttributes.SAML_ASSERTION_SIGNATURE, "true") + .setAttribute(SamlConfigAttributes.SAML_CLIENT_SIGNATURE_ATTRIBUTE, "false") + .update()) + { + // Login should pass because assertion is signed. + loginUser(); + + // Logout should fail because logout response is not signed. + driver.navigate().to(BrokerTestTools.getAuthRoot(suiteContext) + + "/auth/realms/" + bc.providerRealmName() + + "/protocol/" + "openid-connect" + + "/logout?redirect_uri=" + encodeUrl(getAccountUrl(bc.providerRealmName()))); + + errorPage.assertCurrent(); + } + } } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/registration/AbstractRegCliTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/registration/AbstractRegCliTest.java index 40d755a090..e171ccafe0 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/registration/AbstractRegCliTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/registration/AbstractRegCliTest.java @@ -455,7 +455,7 @@ public abstract class AbstractRegCliTest extends AbstractCliTest { ClientRepresentation client3 = JsonSerialization.readValue(exe.stdout(), ClientRepresentation.class); Assert.assertEquals("clientId", "test-client", client3.getClientId()); - Assert.assertNotEquals("registrationAccessToken in returned json is different than one returned by create", + Assert.assertEquals("registrationAccessToken in returned json is different than one returned by create", client.getRegistrationAccessToken(), client3.getRegistrationAccessToken()); lastModified2 = configFile.exists() ? configFile.lastModified() : 0; diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/ClientRegistrationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/ClientRegistrationTest.java index d5903220d1..86edae734b 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/ClientRegistrationTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/ClientRegistrationTest.java @@ -186,6 +186,23 @@ public class ClientRegistrationTest extends AbstractClientRegistrationTest { updateClient(); } + @Test + public void updateClientSecret() throws ClientRegistrationException { + authManageClients(); + + registerClient(); + + ClientRepresentation client = reg.get(CLIENT_ID); + assertNotNull(client.getSecret()); + client.setSecret("mysecret"); + + reg.update(client); + + ClientRepresentation updatedClient = reg.get(CLIENT_ID); + + assertEquals("mysecret", updatedClient.getSecret()); + } + @Test public void updateClientAsAdminWithCreateOnly() throws ClientRegistrationException { authCreateClients(); 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 4b4c9ba427..57f71b265d 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 @@ -139,7 +139,7 @@ public class OIDCClientRegistrationTest extends AbstractClientRegistrationTest { OIDCClientRepresentation rep = reg.oidc().get(response.getClientId()); assertNotNull(rep); - assertNotEquals(response.getRegistrationAccessToken(), rep.getRegistrationAccessToken()); + assertEquals(response.getRegistrationAccessToken(), rep.getRegistrationAccessToken()); assertTrue(CollectionUtil.collectionEquals(Arrays.asList("code", "none"), response.getResponseTypes())); assertTrue(CollectionUtil.collectionEquals(Arrays.asList(OAuth2Constants.AUTHORIZATION_CODE, OAuth2Constants.REFRESH_TOKEN), response.getGrantTypes())); assertNotNull(response.getClientSecret()); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/OIDCPairwiseClientRegistrationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/OIDCPairwiseClientRegistrationTest.java index 8666e04aec..0601879004 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/OIDCPairwiseClientRegistrationTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/OIDCPairwiseClientRegistrationTest.java @@ -28,6 +28,8 @@ import org.keycloak.client.registration.ClientRegistrationException; import org.keycloak.client.registration.HttpErrorException; import org.keycloak.protocol.oidc.mappers.SHA256PairwiseSubMapper; import org.keycloak.representations.AccessToken; +import org.keycloak.representations.IDToken; +import org.keycloak.representations.RefreshToken; import org.keycloak.representations.UserInfo; import org.keycloak.representations.idm.ClientInitialAccessCreatePresentation; import org.keycloak.representations.idm.ClientInitialAccessPresentation; @@ -41,6 +43,7 @@ import org.keycloak.testsuite.client.resources.TestOIDCEndpointsApplicationResou import org.keycloak.testsuite.util.ClientManager; import org.keycloak.testsuite.util.OAuthClient; import org.keycloak.testsuite.util.UserInfoClientUtil; +import org.keycloak.testsuite.util.UserManager; import javax.ws.rs.client.Client; import javax.ws.rs.core.Response; @@ -49,6 +52,8 @@ import java.util.Base64; import java.util.Collections; import java.util.List; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; public class OIDCPairwiseClientRegistrationTest extends AbstractClientRegistrationTest { @@ -77,6 +82,14 @@ public class OIDCPairwiseClientRegistrationTest extends AbstractClientRegistrati return response; } + public OIDCClientRepresentation createPairwise() throws ClientRegistrationException { + // Create pairwise client + OIDCClientRepresentation clientRep = createRep(); + clientRep.setSubjectType("pairwise"); + OIDCClientRepresentation pairwiseClient = reg.oidc().create(clientRep); + return pairwiseClient; + } + private void assertCreateFail(OIDCClientRepresentation client, int expectedStatusCode, String expectedErrorContains) { try { @@ -351,6 +364,109 @@ public class OIDCPairwiseClientRegistrationTest extends AbstractClientRegistrati } } + @Test + public void refreshPairwiseToken() throws Exception { + // Create pairwise client + OIDCClientRepresentation pairwiseClient = createPairwise(); + + // Login to pairwise client + OAuthClient.AccessTokenResponse accessTokenResponse = login(pairwiseClient, "test-user@localhost", "password"); + + // Verify tokens + oauth.verifyRefreshToken(accessTokenResponse.getAccessToken()); + IDToken idToken = oauth.verifyIDToken(accessTokenResponse.getIdToken()); + oauth.verifyRefreshToken(accessTokenResponse.getRefreshToken()); + + // Refresh token + OAuthClient.AccessTokenResponse refreshTokenResponse = oauth.doRefreshTokenRequest(accessTokenResponse.getRefreshToken(), pairwiseClient.getClientSecret()); + + // Verify refreshed tokens + oauth.verifyToken(refreshTokenResponse.getAccessToken()); + RefreshToken refreshedRefreshToken = oauth.verifyRefreshToken(refreshTokenResponse.getRefreshToken()); + IDToken refreshedIdToken = oauth.verifyIDToken(refreshTokenResponse.getIdToken()); + + // If an ID Token is returned as a result of a token refresh request, the following requirements apply: + // its iss Claim Value MUST be the same as in the ID Token issued when the original authentication occurred + Assert.assertEquals(idToken.getIssuer(), refreshedRefreshToken.getIssuer()); + + // its sub Claim Value MUST be the same as in the ID Token issued when the original authentication occurred + Assert.assertEquals(idToken.getSubject(), refreshedRefreshToken.getSubject()); + + // its iat Claim MUST represent the time that the new ID Token is issued + Assert.assertEquals(refreshedIdToken.getIssuedAt(), refreshedRefreshToken.getIssuedAt()); + + // its aud Claim Value MUST be the same as in the ID Token issued when the original authentication occurred + Assert.assertArrayEquals(idToken.getAudience(), refreshedRefreshToken.getAudience()); + + // if the ID Token contains an auth_time Claim, its value MUST represent the time of the original authentication + // - not the time that the new ID token is issued + Assert.assertEquals(idToken.getAuthTime(), refreshedIdToken.getAuthTime()); + + // its azp Claim Value MUST be the same as in the ID Token issued when the original authentication occurred; if + // no azp Claim was present in the original ID Token, one MUST NOT be present in the new ID Token + Assert.assertEquals(idToken.getIssuedFor(), refreshedIdToken.getIssuedFor()); + } + + @Test + public void refreshPairwiseTokenDeletedUser() throws Exception { + String userId = createUser(REALM_NAME, "delete-me@localhost", "password"); + + // Create pairwise client + OIDCClientRepresentation pairwiseClient = createPairwise(); + + // Login to pairwise client + oauth.clientId(pairwiseClient.getClientId()); + oauth.clientId(pairwiseClient.getClientId()); + OAuthClient.AuthorizationEndpointResponse loginResponse = oauth.doLogin("delete-me@localhost", "password"); + OAuthClient.AccessTokenResponse accessTokenResponse = oauth.doAccessTokenRequest(loginResponse.getCode(), pairwiseClient.getClientSecret()); + + assertEquals(200, accessTokenResponse.getStatusCode()); + + // Delete user + adminClient.realm(REALM_NAME).users().delete(userId); + + OAuthClient.AccessTokenResponse refreshTokenResponse = oauth.doRefreshTokenRequest(accessTokenResponse.getRefreshToken(), pairwiseClient.getClientSecret()); + assertEquals(400, refreshTokenResponse.getStatusCode()); + assertEquals("invalid_grant", refreshTokenResponse.getError()); + assertNull(refreshTokenResponse.getAccessToken()); + assertNull(refreshTokenResponse.getIdToken()); + assertNull(refreshTokenResponse.getRefreshToken()); + } + + @Test + public void refreshPairwiseTokenDisabledUser() throws Exception { + createUser(REALM_NAME, "disable-me@localhost", "password"); + + // Create pairwise client + OIDCClientRepresentation pairwiseClient = createPairwise(); + + // Login to pairwise client + oauth.clientId(pairwiseClient.getClientId()); + oauth.clientId(pairwiseClient.getClientId()); + OAuthClient.AuthorizationEndpointResponse loginResponse = oauth.doLogin("disable-me@localhost", "password"); + OAuthClient.AccessTokenResponse accessTokenResponse = oauth.doAccessTokenRequest(loginResponse.getCode(), pairwiseClient.getClientSecret()); + assertEquals(200, accessTokenResponse.getStatusCode()); + + try { + UserManager.realm(adminClient.realm(REALM_NAME)).username("disable-me@localhost").enabled(false); + + OAuthClient.AccessTokenResponse refreshTokenResponse = oauth.doRefreshTokenRequest(accessTokenResponse.getRefreshToken(), pairwiseClient.getClientSecret()); + assertEquals(400, refreshTokenResponse.getStatusCode()); + assertEquals("invalid_grant", refreshTokenResponse.getError()); + assertNull(refreshTokenResponse.getAccessToken()); + assertNull(refreshTokenResponse.getIdToken()); + assertNull(refreshTokenResponse.getRefreshToken()); + } finally { + UserManager.realm(adminClient.realm(REALM_NAME)).username("disable-me@localhost").enabled(true); + } + } + + private OAuthClient.AccessTokenResponse login(OIDCClientRepresentation client, String username, String password) { + oauth.clientId(client.getClientId()); + OAuthClient.AuthorizationEndpointResponse loginResponse = oauth.doLogin(username, password); + return oauth.doAccessTokenRequest(loginResponse.getCode(), client.getClientSecret()); + } + private String getPayload(String token) { String payloadBase64 = token.split("\\.")[1]; return new String(Base64.getDecoder().decode(payloadBase64)); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/RegistrationAccessTokenTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/RegistrationAccessTokenTest.java index 3eb0d7e5ef..d7ea7f1b3d 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/RegistrationAccessTokenTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/RegistrationAccessTokenTest.java @@ -82,13 +82,16 @@ public class RegistrationAccessTokenTest extends AbstractClientRegistrationTest @Test public void getClientWithRegistrationToken() throws ClientRegistrationException { + setTimeOffset(10); + ClientRepresentation rep = reg.get(client.getClientId()); assertNotNull(rep); - assertNotEquals(client.getRegistrationAccessToken(), rep.getRegistrationAccessToken()); - // check registration access token is updated - assertRead(client.getClientId(), client.getRegistrationAccessToken(), false); - assertRead(client.getClientId(), rep.getRegistrationAccessToken(), true); + assertEquals(client.getRegistrationAccessToken(), rep.getRegistrationAccessToken()); + assertNotNull(rep.getRegistrationAccessToken()); + + // KEYCLOAK-4984 check registration access token is not updated + assertRead(client.getClientId(), client.getRegistrationAccessToken(), true); } @Test diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cluster/ConcurrentLoginClusterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cluster/ConcurrentLoginClusterTest.java new file mode 100644 index 0000000000..0986c1791d --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cluster/ConcurrentLoginClusterTest.java @@ -0,0 +1,84 @@ +/* + * 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.cluster; + +import java.util.LinkedList; +import java.util.List; + +import org.jboss.arquillian.container.test.api.ContainerController; +import org.jboss.arquillian.test.api.ArquillianResource; +import org.junit.Before; +import org.keycloak.connections.infinispan.InfinispanConnectionProvider; +import org.keycloak.representations.idm.RealmRepresentation; +import org.keycloak.testsuite.Assert; +import org.keycloak.testsuite.admin.concurrency.ConcurrentLoginTest; +import org.keycloak.testsuite.arquillian.ContainerInfo; +import org.keycloak.testsuite.rest.representation.JGroupsStats; + +/** + * @author Marek Posolda + */ +public class ConcurrentLoginClusterTest extends ConcurrentLoginTest { + + + @ArquillianResource + protected ContainerController controller; + + + // Need to postpone that + @Override + public void addTestRealms(List testRealms) { + } + + + @Before + @Override + public void beforeTest() { + // Start backend nodes + log.info("Starting 2 backend nodes now"); + for (ContainerInfo node : suiteContext.getAuthServerBackendsInfo()) { + if (!controller.isStarted(node.getQualifier())) { + log.info("Starting backend node: " + node); + controller.start(node.getQualifier()); + Assert.assertTrue(controller.isStarted(node.getQualifier())); + } + } + + // Import realms + log.info("Importing realms"); + List testRealms = new LinkedList<>(); + super.addTestRealms(testRealms); + for (RealmRepresentation testRealm : testRealms) { + importRealm(testRealm); + } + log.info("Realms imported"); + + // Finally create clients + createClients(); + } + + + @Override + public void concurrentLoginSingleUser() throws Throwable { + super.concurrentLoginSingleUser(); + JGroupsStats stats = testingClient.testing().cache(InfinispanConnectionProvider.SESSION_CACHE_NAME).getJgroupsStats(); + log.info("JGroups statistics: " + stats.statsAsString()); + } + + +} diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/AbstractAdminCrossDCTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/AbstractAdminCrossDCTest.java index 2ad3cc3517..27fed71496 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/AbstractAdminCrossDCTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/AbstractAdminCrossDCTest.java @@ -96,7 +96,7 @@ public abstract class AbstractAdminCrossDCTest extends AbstractCrossDCTest { Matcher matcherInstance = matcherOnOldStat.apply(oldStat); assertThat(newStat, matcherInstance); - }, 5, 200); + }, 20, 200); } protected void assertStatistics(InfinispanStatistics stats, Runnable testedCode, BiConsumer, Map> assertionOnStats) { diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/AbstractCrossDCTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/AbstractCrossDCTest.java index b4d4236a6d..fd6300e4b0 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/AbstractCrossDCTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/AbstractCrossDCTest.java @@ -23,6 +23,8 @@ import org.keycloak.testsuite.arquillian.ContainerInfo; import org.keycloak.testsuite.arquillian.LoadBalancerController; import org.keycloak.testsuite.arquillian.annotation.LoadBalancer; import org.keycloak.testsuite.auth.page.AuthRealm; + +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -31,13 +33,14 @@ import org.jboss.arquillian.container.test.api.ContainerController; import org.jboss.arquillian.test.api.ArquillianResource; import org.junit.After; import org.junit.Before; +import org.keycloak.testsuite.client.KeycloakTestingClient; import static org.hamcrest.Matchers.lessThan; import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; /** - * + * Abstract cross-data-centre test that defines primitives for handling cross-DC setup. * @author hmlnarik */ public abstract class AbstractCrossDCTest extends AbstractTestRealmKeycloakTest { @@ -54,14 +57,16 @@ public abstract class AbstractCrossDCTest extends AbstractTestRealmKeycloakTest protected Map backendAdminClients = new HashMap<>(); + protected Map backendTestingClients = new HashMap<>(); + @After @Before public void enableOnlyFirstNodeInFirstDc() { this.loadBalancerCtrl.disableAllBackendNodes(); - loadBalancerCtrl.enableBackendNodeByName(getAutomaticallyStartedBackendNodes(0) - .findFirst() - .orElseThrow(() -> new IllegalStateException("No node is started automatically")) - .getQualifier() + loadBalancerCtrl.enableBackendNodeByName(getAutomaticallyStartedBackendNodes(DC.FIRST) + .findFirst() + .orElseThrow(() -> new IllegalStateException("No node is started automatically")) + .getQualifier() ); } @@ -72,8 +77,23 @@ public abstract class AbstractCrossDCTest extends AbstractTestRealmKeycloakTest .flatMap(List::stream) .filter(ContainerInfo::isStarted) .filter(ContainerInfo::isManual) - .map(ContainerInfo::getQualifier) - .forEach(containerController::stop); + .forEach(containerInfo -> { + containerController.stop(containerInfo.getQualifier()); + removeRESTClientsForNode(containerInfo); + }); + } + + @Before + public void initRESTClientsForStartedNodes() { + log.debug("Init REST clients for automatically started nodes"); + this.suiteContext.getDcAuthServerBackendsInfo().stream() + .flatMap(List::stream) + .filter(ContainerInfo::isStarted) + .filter(containerInfo -> !containerInfo.isManual()) + .forEach(containerInfo -> { + createRESTClientsForNode(containerInfo); + }); + } @Override @@ -98,7 +118,7 @@ public abstract class AbstractCrossDCTest extends AbstractTestRealmKeycloakTest public void initLoadBalancer() { log.debug("Initializing load balancer - only enabling started nodes in the first DC"); this.loadBalancerCtrl.disableAllBackendNodes(); - // Enable only the started nodes in each datacenter + // Enable only the started nodes in first datacenter this.suiteContext.getDcAuthServerBackendsInfo().get(0).stream() .filter(ContainerInfo::isStarted) .map(ContainerInfo::getQualifier) @@ -110,8 +130,22 @@ public abstract class AbstractCrossDCTest extends AbstractTestRealmKeycloakTest return Keycloak.getInstance(node.getContextRoot() + "/auth", AuthRealm.MASTER, AuthRealm.ADMIN, AuthRealm.ADMIN, Constants.ADMIN_CLI_CLIENT_ID); } + protected KeycloakTestingClient createTestingClientFor(ContainerInfo node) { + log.info("Initializing testing client for " + node.getContextRoot() + "/auth"); + return KeycloakTestingClient.getInstance(node.getContextRoot() + "/auth"); + } + + + protected Keycloak getAdminClientForStartedNodeInDc(int dcIndex) { + ContainerInfo firstStartedNode = this.suiteContext.getDcAuthServerBackendsInfo().get(dcIndex).stream() + .filter(ContainerInfo::isStarted) + .findFirst().get(); + + return getAdminClientFor(firstStartedNode); + } + /** - * Creates admin client directed to the given node. + * Get admin client directed to the given node. * @param node * @return */ @@ -123,11 +157,58 @@ public abstract class AbstractCrossDCTest extends AbstractTestRealmKeycloakTest return client; } + + protected KeycloakTestingClient getTestingClientForStartedNodeInDc(int dcIndex) { + ContainerInfo firstStartedNode = this.suiteContext.getDcAuthServerBackendsInfo().get(dcIndex).stream() + .filter(ContainerInfo::isStarted) + .findFirst().get(); + + return getTestingClientFor(firstStartedNode); + } + + + /** + * Get testing client directed to the given node. + * @param node + * @return + */ + protected KeycloakTestingClient getTestingClientFor(ContainerInfo node) { + KeycloakTestingClient client = backendTestingClients.get(node); + if (client == null && node.equals(suiteContext.getAuthServerInfo())) { + client = this.testingClient; + } + return client; + } + + protected void createRESTClientsForNode(ContainerInfo node) { + if (!backendAdminClients.containsKey(node)) { + backendAdminClients.put(node, createAdminClientFor(node)); + } + + if (!backendTestingClients.containsKey(node)) { + backendTestingClients.put(node, createTestingClientFor(node)); + } + } + + protected void removeRESTClientsForNode(ContainerInfo node) { + if (backendAdminClients.containsKey(node)) { + backendAdminClients.get(node).close(); + backendAdminClients.remove(node); + } + + if (backendTestingClients.containsKey(node)) { + backendTestingClients.get(node).close(); + backendTestingClients.remove(node); + } + } + + /** * Disables routing requests to the given data center in the load balancer. * @param dcIndex */ - public void disableDcOnLoadBalancer(int dcIndex) { + public void disableDcOnLoadBalancer(DC dc) { + int dcIndex = dc.ordinal(); log.infof("Disabling load balancer for dc=%d", dcIndex); this.suiteContext.getDcAuthServerBackendsInfo().get(dcIndex).forEach(c -> loadBalancerCtrl.disableBackendNodeByName(c.getQualifier())); } @@ -136,7 +217,8 @@ public abstract class AbstractCrossDCTest extends AbstractTestRealmKeycloakTest * Enables routing requests to all started nodes to the given data center in the load balancer. * @param dcIndex */ - public void enableDcOnLoadBalancer(int dcIndex) { + public void enableDcOnLoadBalancer(DC dc) { + int dcIndex = dc.ordinal(); log.infof("Enabling load balancer for dc=%d", dcIndex); final List dcNodes = this.suiteContext.getDcAuthServerBackendsInfo().get(dcIndex); if (! dcNodes.stream().anyMatch(ContainerInfo::isStarted)) { @@ -153,7 +235,8 @@ public abstract class AbstractCrossDCTest extends AbstractTestRealmKeycloakTest * @param dcIndex * @param nodeIndex */ - public void disableLoadBalancerNode(int dcIndex, int nodeIndex) { + public void disableLoadBalancerNode(DC dc, int nodeIndex) { + int dcIndex = dc.ordinal(); log.infof("Disabling load balancer for dc=%d, node=%d", dcIndex, nodeIndex); loadBalancerCtrl.disableBackendNodeByName(this.suiteContext.getDcAuthServerBackendsInfo().get(dcIndex).get(nodeIndex).getQualifier()); } @@ -163,7 +246,8 @@ public abstract class AbstractCrossDCTest extends AbstractTestRealmKeycloakTest * @param dcIndex * @param nodeIndex */ - public void enableLoadBalancerNode(int dcIndex, int nodeIndex) { + public void enableLoadBalancerNode(DC dc, int nodeIndex) { + int dcIndex = dc.ordinal(); log.infof("Enabling load balancer for dc=%d, node=%d", dcIndex, nodeIndex); final ContainerInfo backendNode = this.suiteContext.getDcAuthServerBackendsInfo().get(dcIndex).get(nodeIndex); if (backendNode == null) { @@ -181,13 +265,17 @@ public abstract class AbstractCrossDCTest extends AbstractTestRealmKeycloakTest * @param nodeIndex * @return Started instance descriptor. */ - public ContainerInfo startBackendNode(int dcIndex, int nodeIndex) { + public ContainerInfo startBackendNode(DC dc, int nodeIndex) { + int dcIndex = dc.ordinal(); assertThat((Integer) dcIndex, lessThan(this.suiteContext.getDcAuthServerBackendsInfo().size())); final List dcNodes = this.suiteContext.getDcAuthServerBackendsInfo().get(dcIndex); assertThat((Integer) nodeIndex, lessThan(dcNodes.size())); ContainerInfo dcNode = dcNodes.get(nodeIndex); assertTrue("Node " + dcNode.getQualifier() + " has to be controlled manually", dcNode.isManual()); containerController.start(dcNode.getQualifier()); + + createRESTClientsForNode(dcNode); + return dcNode; } @@ -197,11 +285,15 @@ public abstract class AbstractCrossDCTest extends AbstractTestRealmKeycloakTest * @param nodeIndex * @return Stopped instance descriptor. */ - public ContainerInfo stopBackendNode(int dcIndex, int nodeIndex) { + public ContainerInfo stopBackendNode(DC dc, int nodeIndex) { + int dcIndex = dc.ordinal(); assertThat((Integer) dcIndex, lessThan(this.suiteContext.getDcAuthServerBackendsInfo().size())); final List dcNodes = this.suiteContext.getDcAuthServerBackendsInfo().get(dcIndex); assertThat((Integer) nodeIndex, lessThan(dcNodes.size())); ContainerInfo dcNode = dcNodes.get(nodeIndex); + + removeRESTClientsForNode(dcNode); + assertTrue("Node " + dcNode.getQualifier() + " has to be controlled manually", dcNode.isManual()); containerController.stop(dcNode.getQualifier()); return dcNode; @@ -212,7 +304,8 @@ public abstract class AbstractCrossDCTest extends AbstractTestRealmKeycloakTest * @param dcIndex * @return */ - public Stream getManuallyStartedBackendNodes(int dcIndex) { + public Stream getManuallyStartedBackendNodes(DC dc) { + int dcIndex = dc.ordinal(); final List dcNodes = this.suiteContext.getDcAuthServerBackendsInfo().get(dcIndex); return dcNodes.stream().filter(ContainerInfo::isManual); } @@ -222,8 +315,39 @@ public abstract class AbstractCrossDCTest extends AbstractTestRealmKeycloakTest * @param dcIndex * @return */ - public Stream getAutomaticallyStartedBackendNodes(int dcIndex) { + public Stream getAutomaticallyStartedBackendNodes(DC dc) { + int dcIndex = dc.ordinal(); final List dcNodes = this.suiteContext.getDcAuthServerBackendsInfo().get(dcIndex); return dcNodes.stream().filter(c -> ! c.isManual()); } + + + /** + * Sets time offset on all the started containers. + * + * @param offset + */ + @Override + public void setTimeOffset(int offset) { + super.setTimeOffset(offset); + setTimeOffsetOnAllStartedContainers(offset); + } + + private void setTimeOffsetOnAllStartedContainers(int offset) { + backendTestingClients.entrySet().stream() + .filter(testingClientEntry -> testingClientEntry.getKey().isStarted()) + .forEach(testingClientEntry -> { + KeycloakTestingClient testingClient = testingClientEntry.getValue(); + testingClient.testing().setTimeOffset(Collections.singletonMap("offset", String.valueOf(offset))); + }); + } + + /** + * Resets time offset on all the started containers. + */ + @Override + public void resetTimeOffset() { + super.resetTimeOffset(); + setTimeOffsetOnAllStartedContainers(0); + } } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/ActionTokenCrossDCTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/ActionTokenCrossDCTest.java index 972be313dc..1a4e079fba 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/ActionTokenCrossDCTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/ActionTokenCrossDCTest.java @@ -41,6 +41,7 @@ import org.keycloak.testsuite.arquillian.annotation.JmxInfinispanCacheStatistics import org.keycloak.testsuite.arquillian.annotation.JmxInfinispanChannelStatistics; import org.keycloak.testsuite.arquillian.InfinispanStatistics; import org.keycloak.testsuite.arquillian.InfinispanStatistics.Constants; +import org.keycloak.testsuite.pages.ProceedPage; import java.util.concurrent.TimeUnit; import org.hamcrest.Matchers; import static org.hamcrest.Matchers.greaterThan; @@ -58,6 +59,9 @@ public class ActionTokenCrossDCTest extends AbstractAdminCrossDCTest { @Page protected LoginPasswordUpdatePage passwordUpdatePage; + @Page + protected ProceedPage proceedPage; + @Page protected ErrorPage errorPage; @@ -73,11 +77,11 @@ public class ActionTokenCrossDCTest extends AbstractAdminCrossDCTest { @Test public void sendResetPasswordEmailSuccessWorksInCrossDc( - @JmxInfinispanCacheStatistics(dcIndex=0, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.ACTION_TOKEN_CACHE) InfinispanStatistics cacheDc0Node0Statistics, - @JmxInfinispanCacheStatistics(dcIndex=0, dcNodeIndex=1, cacheName=InfinispanConnectionProvider.ACTION_TOKEN_CACHE) InfinispanStatistics cacheDc0Node1Statistics, - @JmxInfinispanCacheStatistics(dcIndex=1, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.ACTION_TOKEN_CACHE) InfinispanStatistics cacheDc1Node0Statistics, + @JmxInfinispanCacheStatistics(dc=DC.FIRST, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.ACTION_TOKEN_CACHE) InfinispanStatistics cacheDc0Node0Statistics, + @JmxInfinispanCacheStatistics(dc=DC.FIRST, dcNodeIndex=1, cacheName=InfinispanConnectionProvider.ACTION_TOKEN_CACHE) InfinispanStatistics cacheDc0Node1Statistics, + @JmxInfinispanCacheStatistics(dc=DC.SECOND, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.ACTION_TOKEN_CACHE) InfinispanStatistics cacheDc1Node0Statistics, @JmxInfinispanChannelStatistics() InfinispanStatistics channelStatisticsCrossDc) throws Exception { - startBackendNode(0, 1); + startBackendNode(DC.FIRST, 1); cacheDc0Node1Statistics.waitToBecomeAvailable(10, TimeUnit.SECONDS); Comparable originalNumberOfEntries = cacheDc0Node0Statistics.getSingleStatistics(Constants.STAT_CACHE_NUMBER_OF_ENTRIES); @@ -107,6 +111,8 @@ public class ActionTokenCrossDCTest extends AbstractAdminCrossDCTest { Matchers::is ); + proceedPage.assertCurrent(); + proceedPage.clickProceedLink(); passwordUpdatePage.assertCurrent(); // Verify that there was at least one message sent via the channel @@ -120,8 +126,8 @@ public class ActionTokenCrossDCTest extends AbstractAdminCrossDCTest { // Verify that there was an action token added in the node which was targetted by the link assertThat(cacheDc0Node0Statistics.getSingleStatistics(Constants.STAT_CACHE_NUMBER_OF_ENTRIES), greaterThan(originalNumberOfEntries)); - disableDcOnLoadBalancer(0); - enableDcOnLoadBalancer(1); + disableDcOnLoadBalancer(DC.FIRST); + enableDcOnLoadBalancer(DC.SECOND); // Make sure that after going to the link, the invalidated action token has been retrieved from Infinispan server cluster in the other DC assertSingleStatistics(cacheDc1Node0Statistics, Constants.STAT_CACHE_NUMBER_OF_ENTRIES, @@ -134,7 +140,7 @@ public class ActionTokenCrossDCTest extends AbstractAdminCrossDCTest { @Test public void sendResetPasswordEmailAfterNewNodeAdded() throws IOException, MessagingException { - disableDcOnLoadBalancer(1); + disableDcOnLoadBalancer(DC.SECOND); UserRepresentation userRep = new UserRepresentation(); userRep.setEnabled(true); @@ -156,14 +162,16 @@ public class ActionTokenCrossDCTest extends AbstractAdminCrossDCTest { driver.navigate().to(link); + proceedPage.assertCurrent(); + proceedPage.clickProceedLink(); passwordUpdatePage.assertCurrent(); passwordUpdatePage.changePassword("new-pass", "new-pass"); assertEquals("Your account has been updated.", driver.getTitle()); - disableDcOnLoadBalancer(0); - getManuallyStartedBackendNodes(1) + disableDcOnLoadBalancer(DC.FIRST); + getManuallyStartedBackendNodes(DC.SECOND) .findFirst() .ifPresent(c -> { containerController.start(c.getQualifier()); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/ConcurrentLoginCrossDCTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/ConcurrentLoginCrossDCTest.java new file mode 100644 index 0000000000..b710943d8e --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/ConcurrentLoginCrossDCTest.java @@ -0,0 +1,113 @@ +/* + * Copyright 2017 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.crossdc; + +import org.keycloak.admin.client.Keycloak; +import org.keycloak.admin.client.resource.RealmResource; +import java.util.List; + +import org.jboss.arquillian.container.test.api.ContainerController; +import org.jboss.arquillian.test.api.ArquillianResource; +import org.keycloak.testsuite.admin.concurrency.ConcurrentLoginTest; +import org.keycloak.testsuite.arquillian.ContainerInfo; +import org.keycloak.testsuite.arquillian.LoadBalancerController; +import org.keycloak.testsuite.arquillian.annotation.LoadBalancer; +import java.util.Arrays; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; +import org.apache.http.client.methods.HttpUriRequest; +import org.apache.http.client.protocol.HttpClientContext; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.impl.client.LaxRedirectStrategy; +import org.junit.Ignore; +import org.junit.Test; + +/** + * @author Marek Posolda + */ +public class ConcurrentLoginCrossDCTest extends ConcurrentLoginTest { + + @ArquillianResource + @LoadBalancer(value = AbstractCrossDCTest.QUALIFIER_NODE_BALANCER) + protected LoadBalancerController loadBalancerCtrl; + + @ArquillianResource + protected ContainerController containerController; + + private static final int INVOCATIONS_BEFORE_SIMULATING_DC_FAILURE = 10; + private static final int LOGIN_TASK_DELAY_MS = 100; + private static final int LOGIN_TASK_RETRIES = 15; + + @Override + public void beforeAbstractKeycloakTestRealmImport() { + log.debug("Initializing load balancer - enabling all started nodes across DCs"); + this.loadBalancerCtrl.disableAllBackendNodes(); + + this.suiteContext.getDcAuthServerBackendsInfo().stream() + .flatMap(List::stream) + .filter(ContainerInfo::isStarted) + .map(ContainerInfo::getQualifier) + .forEach(loadBalancerCtrl::enableBackendNodeByName); + } + + @Test + public void concurrentLoginWithRandomDcFailures() throws Throwable { + log.info("*********************************************"); + long start = System.currentTimeMillis(); + + AtomicReference userSessionId = new AtomicReference<>(); + LoginTask loginTask = null; + + try (CloseableHttpClient httpClient = HttpClientBuilder.create().setRedirectStrategy(new LaxRedirectStrategy()).build()) { + loginTask = new LoginTask(httpClient, userSessionId, LOGIN_TASK_DELAY_MS, LOGIN_TASK_RETRIES, Arrays.asList( + createHttpClientContextForUser(httpClient, "test-user@localhost", "password") + )); + HttpUriRequest request = handleLogin(getPageContent(oauth.getLoginFormUrl(), httpClient, HttpClientContext.create()), "test-user@localhost", "password"); + log.debug("Executing login request"); + org.junit.Assert.assertTrue(parseAndCloseResponse(httpClient.execute(request)).contains("AUTH_RESPONSE")); + + run(DEFAULT_THREADS, DEFAULT_CLIENTS_COUNT, loginTask, new SwapDcAvailability()); + int clientSessionsCount = testingClient.testing().getClientSessionsCountInUserSession("test", userSessionId.get()); + org.junit.Assert.assertEquals(1 + DEFAULT_CLIENTS_COUNT, clientSessionsCount); + } finally { + long end = System.currentTimeMillis() - start; + log.infof("Statistics: %s", loginTask == null ? "??" : loginTask.getHistogram()); + log.info("concurrentLoginWithRandomDcFailures took " + (end/1000) + "s"); + log.info("*********************************************"); + } + } + + private class SwapDcAvailability implements KeycloakRunnable { + + private final AtomicInteger invocationCounter = new AtomicInteger(); + + @Override + public void run(int threadIndex, Keycloak keycloak, RealmResource realm) throws Throwable { + final int currentInvocarion = invocationCounter.getAndIncrement(); + if (currentInvocarion % INVOCATIONS_BEFORE_SIMULATING_DC_FAILURE == 0) { + int failureIndex = currentInvocarion / INVOCATIONS_BEFORE_SIMULATING_DC_FAILURE; + int dcToEnable = failureIndex % 2; + int dcToDisable = (failureIndex + 1) % 2; + suiteContext.getDcAuthServerBackendsInfo().get(dcToDisable).forEach(c -> loadBalancerCtrl.disableBackendNodeByName(c.getQualifier())); + suiteContext.getDcAuthServerBackendsInfo().get(dcToEnable).forEach(c -> loadBalancerCtrl.enableBackendNodeByName(c.getQualifier())); + } + } + } + +} diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/LastSessionRefreshCrossDCTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/LastSessionRefreshCrossDCTest.java new file mode 100644 index 0000000000..bf42536372 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/LastSessionRefreshCrossDCTest.java @@ -0,0 +1,239 @@ +/* + * Copyright 2017 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.crossdc; + + +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.Test; +import org.keycloak.connections.infinispan.InfinispanConnectionProvider; +import org.keycloak.representations.idm.RealmRepresentation; +import org.keycloak.testsuite.Assert; +import org.keycloak.testsuite.Retry; +import org.keycloak.testsuite.arquillian.ContainerInfo; +import org.keycloak.testsuite.client.KeycloakTestingClient; +import org.keycloak.testsuite.rest.representation.RemoteCacheStats; +import org.keycloak.testsuite.util.OAuthClient; + +/** + * @author Marek Posolda + */ +public class LastSessionRefreshCrossDCTest extends AbstractAdminCrossDCTest { + + @Test + public void testRevokeRefreshToken() { + // Enable revokeRefreshToken + RealmRepresentation realmRep = testRealm().toRepresentation(); + realmRep.setRevokeRefreshToken(true); + testRealm().update(realmRep); + + // Enable second DC + enableDcOnLoadBalancer(DC.SECOND); + + // Login + OAuthClient.AuthorizationEndpointResponse response1 = oauth.doLogin("test-user@localhost", "password"); + String code = response1.getCode(); + OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "password"); + Assert.assertNotNull(tokenResponse.getAccessToken()); + String sessionId = oauth.verifyToken(tokenResponse.getAccessToken()).getSessionState(); + String refreshToken1 = tokenResponse.getRefreshToken(); + + + // Get statistics + int lsr00 = getTestingClientForStartedNodeInDc(0).testing("test").getLastSessionRefresh("test", sessionId); + int lsr10 = getTestingClientForStartedNodeInDc(1).testing("test").getLastSessionRefresh("test", sessionId); + int lsrr0 = getTestingClientForStartedNodeInDc(0).testing("test").cache(InfinispanConnectionProvider.SESSION_CACHE_NAME).getRemoteCacheLastSessionRefresh(sessionId); + log.infof("lsr00: %d, lsr10: %d, lsrr0: %d", lsr00, lsr10, lsrr0); + + Assert.assertEquals(lsr00, lsr10); + Assert.assertEquals(lsr00, lsrr0); + + + // Set time offset to some point in future. TODO This won't be needed once we have single-use cache based solution for refresh tokens + setTimeOffset(10); + + // refresh token on DC0 + disableDcOnLoadBalancer(DC.SECOND); + tokenResponse = oauth.doRefreshTokenRequest(refreshToken1, "password"); + String refreshToken2 = tokenResponse.getRefreshToken(); + + // Assert times changed on DC0, DC1 and remoteCache + Retry.execute(() -> { + int lsr01 = getTestingClientForStartedNodeInDc(0).testing("test").getLastSessionRefresh("test", sessionId); + int lsr11 = getTestingClientForStartedNodeInDc(1).testing("test").getLastSessionRefresh("test", sessionId); + int lsrr1 = getTestingClientForStartedNodeInDc(0).testing("test").cache(InfinispanConnectionProvider.SESSION_CACHE_NAME).getRemoteCacheLastSessionRefresh(sessionId); + log.infof("lsr01: %d, lsr11: %d, lsrr1: %d", lsr01, lsr11, lsrr1); + + Assert.assertEquals(lsr01, lsr11); + Assert.assertEquals(lsr01, lsrr1); + Assert.assertTrue(lsr01 > lsr00); + }, 50, 50); + + // try refresh with old token on DC1. It should fail. + disableDcOnLoadBalancer(DC.FIRST); + enableDcOnLoadBalancer(DC.SECOND); + tokenResponse = oauth.doRefreshTokenRequest(refreshToken1, "password"); + Assert.assertNull(tokenResponse.getAccessToken()); + Assert.assertNotNull(tokenResponse.getError()); + + // try refresh with new token on DC1. It should pass. + tokenResponse = oauth.doRefreshTokenRequest(refreshToken2, "password"); + Assert.assertNotNull(tokenResponse.getAccessToken()); + Assert.assertNull(tokenResponse.getError()); + + // Revert + realmRep = testRealm().toRepresentation(); + realmRep.setRevokeRefreshToken(false); + testRealm().update(realmRep); + } + + + @Test + public void testLastSessionRefreshUpdate() { + // Disable DC1 on loadbalancer + disableDcOnLoadBalancer(DC.SECOND); + + // Get statistics + int stores0 = getRemoteCacheStats(0).getGlobalStores(); + + // Login + OAuthClient.AuthorizationEndpointResponse response1 = oauth.doLogin("test-user@localhost", "password"); + String code = response1.getCode(); + OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "password"); + Assert.assertNotNull(tokenResponse.getAccessToken()); + String sessionId = oauth.verifyToken(tokenResponse.getAccessToken()).getSessionState(); + String refreshToken1 = tokenResponse.getRefreshToken(); + + + // Get statistics + this.suiteContext.getDcAuthServerBackendsInfo().get(0).stream() + .filter(ContainerInfo::isStarted).findFirst().get(); + + AtomicInteger stores1 = new AtomicInteger(-1); + Retry.execute(() -> { + stores1.set(getRemoteCacheStats(0).getGlobalStores()); + log.infof("stores0=%d, stores1=%d", stores0, stores1.get()); + Assert.assertTrue(stores1.get() > stores0); + }, 50, 50); + + int lsr00 = getTestingClientForStartedNodeInDc(0).testing("test").getLastSessionRefresh("test", sessionId); + int lsr10 = getTestingClientForStartedNodeInDc(1).testing("test").getLastSessionRefresh("test", sessionId); + Assert.assertEquals(lsr00, lsr10); + + // Set time offset to some point in future. + setTimeOffset(10); + + // refresh token on DC0 + tokenResponse = oauth.doRefreshTokenRequest(refreshToken1, "password"); + String refreshToken2 = tokenResponse.getRefreshToken(); + + // assert that hotrod statistics were NOT updated + AtomicInteger stores2 = new AtomicInteger(-1); + + // TODO: not sure why stores2 < stores1 at first run. Probably should be replaced with JMX statistics + Retry.execute(() -> { + stores2.set(getRemoteCacheStats(0).getGlobalStores()); + log.infof("stores1=%d, stores2=%d", stores1.get(), stores2.get()); + Assert.assertEquals(stores1.get(), stores2.get()); + }, 50, 50); + + // assert that lastSessionRefresh on DC0 updated, but on DC1 still the same + AtomicInteger lsr01 = new AtomicInteger(-1); + AtomicInteger lsr11 = new AtomicInteger(-1); + Retry.execute(() -> { + lsr01.set(getTestingClientForStartedNodeInDc(0).testing("test").getLastSessionRefresh("test", sessionId)); + lsr11.set(getTestingClientForStartedNodeInDc(1).testing("test").getLastSessionRefresh("test", sessionId)); + log.infof("lsr01: %d, lsr11: %d", lsr01.get(), lsr11.get()); + Assert.assertTrue(lsr01.get() > lsr00); + }, 50, 100); + Assert.assertEquals(lsr10, lsr11.get()); + + // assert that lastSessionRefresh still the same on remoteCache + int lsrr1 = getTestingClientForStartedNodeInDc(0).testing("test").cache(InfinispanConnectionProvider.SESSION_CACHE_NAME).getRemoteCacheLastSessionRefresh(sessionId); + Assert.assertEquals(lsr00, lsrr1); + log.infof("lsrr1: %d", lsrr1); + + // setTimeOffset to greater value + setTimeOffset(100); + + // refresh token + tokenResponse = oauth.doRefreshTokenRequest(refreshToken1, "password"); + + // assert that lastSessionRefresh on both DC0 and DC1 was updated, but on remoteCache still the same + AtomicInteger lsr02 = new AtomicInteger(-1); + AtomicInteger lsr12 = new AtomicInteger(-1); + AtomicInteger lsrr2 = new AtomicInteger(-1); + AtomicInteger stores3 = new AtomicInteger(-1); + Retry.execute(() -> { + lsr02.set(getTestingClientForStartedNodeInDc(0).testing("test").getLastSessionRefresh("test", sessionId)); + lsr12.set(getTestingClientForStartedNodeInDc(1).testing("test").getLastSessionRefresh("test", sessionId)); + log.infof("lsr02: %d, lsr12: %d", lsr02.get(), lsr12.get()); + Assert.assertEquals(lsr02.get(), lsr12.get()); + Assert.assertTrue(lsr02.get() > lsr01.get()); + Assert.assertTrue(lsr12.get() > lsr11.get()); + + lsrr2.set(getTestingClientForStartedNodeInDc(0).testing("test").cache(InfinispanConnectionProvider.SESSION_CACHE_NAME).getRemoteCacheLastSessionRefresh(sessionId)); + log.infof("lsrr2: %d", lsrr2.get()); + Assert.assertEquals(lsrr1, lsrr2.get()); + + // assert that hotrod statistics were NOT updated on DC0 + stores3.set(getRemoteCacheStats(0).getGlobalStores()); + log.infof("stores2=%d, stores3=%d", stores2.get(), stores3.get()); + Assert.assertEquals(stores2.get(), stores3.get()); + }, 50, 100); + + // Increase time offset even more + setTimeOffset(1500); + + // refresh token + tokenResponse = oauth.doRefreshTokenRequest(refreshToken1, "password"); + Assert.assertNull("Error: " + tokenResponse.getError() + ", error description: " + tokenResponse.getErrorDescription(), tokenResponse.getError()); + Assert.assertNotNull(tokenResponse.getRefreshToken()); + + // assert that lastSessionRefresh updated everywhere including remoteCache + AtomicInteger lsr03 = new AtomicInteger(-1); + AtomicInteger lsr13 = new AtomicInteger(-1); + AtomicInteger lsrr3 = new AtomicInteger(-1); + AtomicInteger stores4 = new AtomicInteger(-1); + Retry.execute(() -> { + lsr03.set(getTestingClientForStartedNodeInDc(0).testing("test").getLastSessionRefresh("test", sessionId)); + lsr13.set(getTestingClientForStartedNodeInDc(1).testing("test").getLastSessionRefresh("test", sessionId)); + log.infof("lsr03: %d, lsr13: %d", lsr03.get(), lsr13.get()); + Assert.assertEquals(lsr03.get(), lsr13.get()); + Assert.assertTrue(lsr03.get() > lsr02.get()); + Assert.assertTrue(lsr13.get() > lsr12.get()); + + lsrr3.set(getTestingClientForStartedNodeInDc(0).testing("test").cache(InfinispanConnectionProvider.SESSION_CACHE_NAME).getRemoteCacheLastSessionRefresh(sessionId)); + log.infof("lsrr3: %d", lsrr3.get()); + Assert.assertTrue(lsrr3.get() > lsrr2.get()); + + // assert that hotrod statistics were NOT updated on DC0 + stores4.set(getRemoteCacheStats(0).getGlobalStores()); + log.infof("stores3=%d, stores4=%d", stores3.get(), stores4.get()); + Assert.assertTrue(stores4.get() > stores3.get()); + }, 50, 100); + } + + + private RemoteCacheStats getRemoteCacheStats(int dcIndex) { + return getTestingClientForStartedNodeInDc(dcIndex).testing("test") + .cache(InfinispanConnectionProvider.SESSION_CACHE_NAME) + .getRemoteCacheStats(); + } + +} diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/LoginCrossDCTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/LoginCrossDCTest.java new file mode 100644 index 0000000000..c0eb849048 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/LoginCrossDCTest.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.testsuite.crossdc; + +import javax.ws.rs.core.Response; + +import org.apache.http.client.methods.CloseableHttpResponse; +import org.junit.Test; +import org.keycloak.testsuite.Assert; +import org.keycloak.testsuite.Retry; +import org.keycloak.testsuite.util.Matchers; +import org.keycloak.testsuite.util.OAuthClient; + +import static org.junit.Assert.assertThat; + +/** + * @author Marek Posolda + */ +public class LoginCrossDCTest extends AbstractAdminCrossDCTest { + + @Test + public void loginTest() throws Exception { + enableDcOnLoadBalancer(DC.SECOND); + + //log.info("Started to sleep"); + //Thread.sleep(10000000); + for (int i=0 ; i<30 ; i++) { + OAuthClient.AuthorizationEndpointResponse response1 = oauth.doLogin("test-user@localhost", "password"); + String code = response1.getCode(); + OAuthClient.AccessTokenResponse response2 = oauth.doAccessTokenRequest(code, "password"); + Assert.assertNotNull(response2.getAccessToken()); + + try (CloseableHttpResponse response3 = oauth.doLogout(response2.getRefreshToken(), "password")) { + assertThat(response3, Matchers.statusCodeIsHC(Response.Status.NO_CONTENT)); + //assertNotNull(testingClient.testApp().getAdminLogoutAction()); + } + + log.infof("Iteration %d finished", i); + } + } +} diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/SessionExpirationCrossDCTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/SessionExpirationCrossDCTest.java new file mode 100644 index 0000000000..96a59d84e0 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/SessionExpirationCrossDCTest.java @@ -0,0 +1,396 @@ +/* + * Copyright 2017 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.crossdc; + + +import javax.ws.rs.NotFoundException; + +import org.hamcrest.Matchers; +import org.junit.Before; +import org.junit.Test; +import org.keycloak.OAuth2Constants; +import org.keycloak.connections.infinispan.InfinispanConnectionProvider; +import org.keycloak.models.Constants; +import org.keycloak.representations.idm.ClientRepresentation; +import org.keycloak.representations.idm.RealmRepresentation; +import org.keycloak.representations.idm.UserRepresentation; +import org.keycloak.testsuite.Assert; +import org.keycloak.testsuite.Retry; +import org.keycloak.testsuite.admin.ApiUtil; +import org.keycloak.testsuite.arquillian.InfinispanStatistics; +import org.keycloak.testsuite.arquillian.annotation.JmxInfinispanCacheStatistics; +import org.keycloak.testsuite.arquillian.annotation.JmxInfinispanChannelStatistics; +import org.keycloak.testsuite.util.ClientBuilder; +import org.keycloak.testsuite.util.OAuthClient; +import org.keycloak.testsuite.util.RealmBuilder; +import org.keycloak.testsuite.util.UserBuilder; + +/** + * Tests the bulk removal of user sessions and expiration scenarios (eg. removing realm, removing user etc) + * + * @author Marek Posolda + */ +public class SessionExpirationCrossDCTest extends AbstractAdminCrossDCTest { + + private static final String REALM_NAME = "expiration-test"; + + private static final int SESSIONS_COUNT = 20; + + private int sessions01; + private int sessions02; + private int remoteSessions01; + private int remoteSessions02; + + private int authSessions01; + private int authSessions02; + + + @Before + public void beforeTest() { + try { + adminClient.realm(REALM_NAME).remove(); + } catch (NotFoundException ignore) { + } + + UserRepresentation user = UserBuilder.create() + .id("login-test") + .username("login-test") + .email("login@test.com") + .enabled(true) + .password("password") + .addRoles(Constants.OFFLINE_ACCESS_ROLE) + .build(); + + ClientRepresentation client = ClientBuilder.create() + .clientId("test-app") + .directAccessGrants() + .redirectUris("http://localhost:8180/auth/realms/master/app/*") + .addWebOrigin("http://localhost:8180") + .secret("password") + .build(); + + RealmRepresentation realmRep = RealmBuilder.create() + .name(REALM_NAME) + .user(user) + .client(client) + .build(); + + adminClient.realms().create(realmRep); + } + + + @Test + public void testRealmRemoveSessions( + @JmxInfinispanCacheStatistics(dc=DC.FIRST, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.SESSION_CACHE_NAME) InfinispanStatistics cacheDc1Statistics, + @JmxInfinispanCacheStatistics(dc=DC.SECOND, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.SESSION_CACHE_NAME) InfinispanStatistics cacheDc2Statistics, + @JmxInfinispanChannelStatistics() InfinispanStatistics channelStatisticsCrossDc) throws Exception { + createInitialSessions(InfinispanConnectionProvider.SESSION_CACHE_NAME, false, cacheDc1Statistics, cacheDc2Statistics); + +// log.infof("Sleeping!"); +// Thread.sleep(10000000); + + channelStatisticsCrossDc.reset(); + + // Remove test realm + getAdminClient().realm(REALM_NAME).remove(); + + // Assert sessions removed on node1 and node2 and on remote caches. Assert that count of messages sent between DCs is not too big. + assertStatisticsExpected("After realm remove", InfinispanConnectionProvider.SESSION_CACHE_NAME, cacheDc1Statistics, cacheDc2Statistics, channelStatisticsCrossDc, + sessions01, sessions02, remoteSessions01, remoteSessions02, 40l); + } + + + // Return last used accessTokenResponse + private OAuthClient.AccessTokenResponse createInitialSessions(String cacheName, boolean offline, InfinispanStatistics cacheDc1Statistics, InfinispanStatistics cacheDc2Statistics) throws Exception { + + // Enable second DC + enableDcOnLoadBalancer(DC.SECOND); + + // Check sessions count before test + sessions01 = getTestingClientForStartedNodeInDc(0).testing().cache(cacheName).size(); + sessions02 = getTestingClientForStartedNodeInDc(1).testing().cache(cacheName).size(); + remoteSessions01 = (Integer) cacheDc1Statistics.getSingleStatistics(InfinispanStatistics.Constants.STAT_CACHE_NUMBER_OF_ENTRIES); + remoteSessions02 = (Integer) cacheDc2Statistics.getSingleStatistics(InfinispanStatistics.Constants.STAT_CACHE_NUMBER_OF_ENTRIES); + log.infof("Before creating sessions: sessions01: %d, sessions02: %d, remoteSessions01: %d, remoteSessions02: %d", sessions01, sessions02, remoteSessions01, remoteSessions02); + + // Create 20 user sessions + oauth.realm(REALM_NAME); + + if (offline) { + oauth.scope(OAuth2Constants.OFFLINE_ACCESS); + } + + OAuthClient.AccessTokenResponse lastAccessTokenResponse = null; + for (int i=0 ; i { + int sessions11 = getTestingClientForStartedNodeInDc(0).testing().cache(cacheName).size(); + int sessions12 = getTestingClientForStartedNodeInDc(1).testing().cache(cacheName).size(); + int remoteSessions11 = (Integer) cacheDc1Statistics.getSingleStatistics(InfinispanStatistics.Constants.STAT_CACHE_NUMBER_OF_ENTRIES); + int remoteSessions12 = (Integer) cacheDc2Statistics.getSingleStatistics(InfinispanStatistics.Constants.STAT_CACHE_NUMBER_OF_ENTRIES); + log.infof("After creating sessions: sessions11: %d, sessions12: %d, remoteSessions11: %d, remoteSessions12: %d", sessions11, sessions12, remoteSessions11, remoteSessions12); + + Assert.assertEquals(sessions11, sessions01 + SESSIONS_COUNT); + Assert.assertEquals(sessions12, sessions02 + SESSIONS_COUNT); + Assert.assertEquals(remoteSessions11, remoteSessions01 + SESSIONS_COUNT); + Assert.assertEquals(remoteSessions12, remoteSessions02 + SESSIONS_COUNT); + }, 50, 50); + + return lastAccessTokenResponse; + } + + + private void assertStatisticsExpected(String messagePrefix, String cacheName, InfinispanStatistics cacheDc1Statistics, InfinispanStatistics cacheDc2Statistics, InfinispanStatistics channelStatisticsCrossDc, + int sessions1Expected, int sessions2Expected, int remoteSessions1Expected, int remoteSessions2Expected, long sentMessagesHigherBound) { + Retry.execute(() -> { + int sessions1 = getTestingClientForStartedNodeInDc(0).testing().cache(cacheName).size(); + int sessions2 = getTestingClientForStartedNodeInDc(1).testing().cache(cacheName).size(); + int remoteSessions1 = (Integer) cacheDc1Statistics.getSingleStatistics(InfinispanStatistics.Constants.STAT_CACHE_NUMBER_OF_ENTRIES); + int remoteSessions2 = (Integer) cacheDc2Statistics.getSingleStatistics(InfinispanStatistics.Constants.STAT_CACHE_NUMBER_OF_ENTRIES); + long messagesCount = (Long) channelStatisticsCrossDc.getSingleStatistics(InfinispanStatistics.Constants.STAT_CHANNEL_SENT_MESSAGES); + log.infof(messagePrefix + ": sessions1: %d, sessions2: %d, remoteSessions1: %d, remoteSessions2: %d, sentMessages: %d", sessions1, sessions2, remoteSessions1, remoteSessions2, messagesCount); + + Assert.assertEquals(sessions1, sessions1Expected); + Assert.assertEquals(sessions2, sessions2Expected); + Assert.assertEquals(remoteSessions1, remoteSessions1Expected); + Assert.assertEquals(remoteSessions2, remoteSessions2Expected); + + // Workaround... + if (sentMessagesHigherBound > 5) { + Assert.assertThat(messagesCount, Matchers.greaterThan(0l)); + } + + Assert.assertThat(messagesCount, Matchers.lessThan(sentMessagesHigherBound)); + }, 50, 50); + } + + + @Test + public void testRealmRemoveOfflineSessions( + @JmxInfinispanCacheStatistics(dc=DC.FIRST, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.OFFLINE_SESSION_CACHE_NAME) InfinispanStatistics cacheDc1Statistics, + @JmxInfinispanCacheStatistics(dc=DC.SECOND, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.OFFLINE_SESSION_CACHE_NAME) InfinispanStatistics cacheDc2Statistics, + @JmxInfinispanChannelStatistics() InfinispanStatistics channelStatisticsCrossDc) throws Exception { + + createInitialSessions(InfinispanConnectionProvider.OFFLINE_SESSION_CACHE_NAME, true, cacheDc1Statistics, cacheDc2Statistics); + + channelStatisticsCrossDc.reset(); + + // Remove test realm + getAdminClient().realm(REALM_NAME).remove(); + + // Assert sessions removed on node1 and node2 and on remote caches. Assert that count of messages sent between DCs is not too big. + assertStatisticsExpected("After realm remove", InfinispanConnectionProvider.OFFLINE_SESSION_CACHE_NAME, cacheDc1Statistics, cacheDc2Statistics, channelStatisticsCrossDc, + sessions01, sessions02, remoteSessions01, remoteSessions02, 70l); // Might be bigger messages as online sessions removed too. + } + + + @Test + public void testLogoutAllInRealm( + @JmxInfinispanCacheStatistics(dc=DC.FIRST, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.SESSION_CACHE_NAME) InfinispanStatistics cacheDc1Statistics, + @JmxInfinispanCacheStatistics(dc=DC.SECOND, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.SESSION_CACHE_NAME) InfinispanStatistics cacheDc2Statistics, + @JmxInfinispanChannelStatistics() InfinispanStatistics channelStatisticsCrossDc) throws Exception { + + createInitialSessions(InfinispanConnectionProvider.SESSION_CACHE_NAME, false, cacheDc1Statistics, cacheDc2Statistics); + + channelStatisticsCrossDc.reset(); + + // Logout all in realm + getAdminClient().realm(REALM_NAME).logoutAll(); + + // Assert sessions removed on node1 and node2 and on remote caches. Assert that count of messages sent between DCs is not too big. + assertStatisticsExpected("After realm logout", InfinispanConnectionProvider.SESSION_CACHE_NAME, cacheDc1Statistics, cacheDc2Statistics, channelStatisticsCrossDc, + sessions01, sessions02, remoteSessions01, remoteSessions02, 40l); + } + + + @Test + public void testPeriodicExpiration( + @JmxInfinispanCacheStatistics(dc=DC.FIRST, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.SESSION_CACHE_NAME) InfinispanStatistics cacheDc1Statistics, + @JmxInfinispanCacheStatistics(dc=DC.SECOND, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.SESSION_CACHE_NAME) InfinispanStatistics cacheDc2Statistics, + @JmxInfinispanChannelStatistics() InfinispanStatistics channelStatisticsCrossDc) throws Exception { + + OAuthClient.AccessTokenResponse lastAccessTokenResponse = createInitialSessions(InfinispanConnectionProvider.SESSION_CACHE_NAME, false, cacheDc1Statistics, cacheDc2Statistics); + + // Assert I am able to refresh + OAuthClient.AccessTokenResponse refreshResponse = oauth.doRefreshTokenRequest(lastAccessTokenResponse.getRefreshToken(), "password"); + Assert.assertNotNull(refreshResponse.getRefreshToken()); + Assert.assertNull(refreshResponse.getError()); + + channelStatisticsCrossDc.reset(); + + // Remove expired in DC0 + getTestingClientForStartedNodeInDc(0).testing().removeExpired(REALM_NAME); + + // Nothing yet expired. Limit 5 for sent_messages is just if "lastSessionRefresh" periodic thread happened + assertStatisticsExpected("After remove expired - 1", InfinispanConnectionProvider.SESSION_CACHE_NAME, cacheDc1Statistics, cacheDc2Statistics, channelStatisticsCrossDc, + sessions01 + SESSIONS_COUNT, sessions02 + SESSIONS_COUNT, remoteSessions01 + SESSIONS_COUNT, remoteSessions02 + SESSIONS_COUNT, 5l); + + + // Set time offset + setTimeOffset(10000000); + + // Assert I am not able to refresh anymore + refreshResponse = oauth.doRefreshTokenRequest(lastAccessTokenResponse.getRefreshToken(), "password"); + Assert.assertNull(refreshResponse.getRefreshToken()); + Assert.assertNotNull(refreshResponse.getError()); + + + channelStatisticsCrossDc.reset(); + + // Remove expired in DC0 + getTestingClientForStartedNodeInDc(0).testing().removeExpired(REALM_NAME); + + // Assert sessions removed on node1 and node2 and on remote caches. Assert that count of messages sent between DCs is not too big. + assertStatisticsExpected("After remove expired - 2", InfinispanConnectionProvider.SESSION_CACHE_NAME, cacheDc1Statistics, cacheDc2Statistics, channelStatisticsCrossDc, + sessions01, sessions02, remoteSessions01, remoteSessions02, 40l); + } + + + + + // AUTH SESSIONS + + @Test + public void testPeriodicExpirationAuthSessions( + @JmxInfinispanCacheStatistics(dc=DC.FIRST, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.AUTHENTICATION_SESSIONS_CACHE_NAME) InfinispanStatistics cacheDc1Statistics, + @JmxInfinispanCacheStatistics(dc=DC.SECOND, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.AUTHENTICATION_SESSIONS_CACHE_NAME) InfinispanStatistics cacheDc2Statistics, + @JmxInfinispanChannelStatistics() InfinispanStatistics channelStatisticsCrossDc) throws Exception { + createInitialAuthSessions(); + + channelStatisticsCrossDc.reset(); + + // Remove expired in DC0 and DC1 + getTestingClientForStartedNodeInDc(0).testing().removeExpired(REALM_NAME); + getTestingClientForStartedNodeInDc(1).testing().removeExpired(REALM_NAME); + + // Nothing yet expired. Limit 5 for sent_messages is just if "lastSessionRefresh" periodic thread happened + assertAuthSessionsStatisticsExpected("After remove expired auth sessions - 1", channelStatisticsCrossDc, + SESSIONS_COUNT, 5l); + + // Set time offset + setTimeOffset(10000000); + + channelStatisticsCrossDc.reset(); + + // Remove expired in DC0 and DC1. Need to trigger it on both! + getTestingClientForStartedNodeInDc(0).testing().removeExpired(REALM_NAME); + getTestingClientForStartedNodeInDc(1).testing().removeExpired(REALM_NAME); + + // Assert sessions removed on node1 and node2 and on remote caches. Assert that count of messages sent between DCs is not too big. + assertAuthSessionsStatisticsExpected("After remove expired auth sessions - 2", channelStatisticsCrossDc, + 0, 5l); + + } + + + // Return last used accessTokenResponse + private void createInitialAuthSessions() throws Exception { + + // Enable second DC + enableDcOnLoadBalancer(DC.SECOND); + + // Check sessions count before test + authSessions01 = getTestingClientForStartedNodeInDc(0).testing().cache(InfinispanConnectionProvider.AUTHENTICATION_SESSIONS_CACHE_NAME).size(); + authSessions02 = getTestingClientForStartedNodeInDc(1).testing().cache(InfinispanConnectionProvider.AUTHENTICATION_SESSIONS_CACHE_NAME).size(); + log.infof("Before creating authentication sessions: authSessions01: %d, authSessions02: %d", authSessions01, authSessions02); + + // Create 20 authentication sessions + oauth.realm(REALM_NAME); + + for (int i=0 ; i { + int authSessions11 = getTestingClientForStartedNodeInDc(0).testing().cache(InfinispanConnectionProvider.AUTHENTICATION_SESSIONS_CACHE_NAME).size(); + int authSessions12 = getTestingClientForStartedNodeInDc(1).testing().cache(InfinispanConnectionProvider.AUTHENTICATION_SESSIONS_CACHE_NAME).size(); + log.infof("After creating authentication sessions: sessions11: %d, authSessions12: %d", authSessions11, authSessions12); + + // There are 20 new authentication sessions created totally in both datacenters + int diff1 = authSessions11 - authSessions01; + int diff2 = authSessions12 - authSessions02; + Assert.assertEquals(SESSIONS_COUNT, diff1 + diff2); + }, 50, 50); + } + + + private void assertAuthSessionsStatisticsExpected(String messagePrefix, InfinispanStatistics channelStatisticsCrossDc, + int expectedAuthSessionsCountDiff, long sentMessagesHigherBound) { + Retry.execute(() -> { + int authSessions1 = getTestingClientForStartedNodeInDc(0).testing().cache(InfinispanConnectionProvider.AUTHENTICATION_SESSIONS_CACHE_NAME).size(); + int authSessions2 = getTestingClientForStartedNodeInDc(1).testing().cache(InfinispanConnectionProvider.AUTHENTICATION_SESSIONS_CACHE_NAME).size(); + long messagesCount = (Long) channelStatisticsCrossDc.getSingleStatistics(InfinispanStatistics.Constants.STAT_CHANNEL_SENT_MESSAGES); + log.infof(messagePrefix + ": authSessions1: %d, authSessions2: %d, sentMessages: %d", authSessions1, authSessions2, messagesCount); + + int diff1 = authSessions1 - authSessions01; + int diff2 = authSessions2 - authSessions02; + + Assert.assertEquals(expectedAuthSessionsCountDiff, diff1 + diff2); + + // Workaround... + if (sentMessagesHigherBound > 5) { + Assert.assertThat(messagesCount, Matchers.greaterThan(0l)); + } + + Assert.assertThat(messagesCount, Matchers.lessThan(sentMessagesHigherBound)); + }, 50, 50); + } + + + @Test + public void testRealmRemoveAuthSessions( + @JmxInfinispanChannelStatistics() InfinispanStatistics channelStatisticsCrossDc) throws Exception { + + createInitialAuthSessions(); + + channelStatisticsCrossDc.reset(); + + // Remove test realm + getAdminClient().realm(REALM_NAME).remove(); + + // Assert sessions removed on node1 and node2 and on remote caches. Assert that count of messages sent between DCs is not too big, however there are some messages due to removed realm + assertAuthSessionsStatisticsExpected("After realm removed", channelStatisticsCrossDc, + 0, 40l); + } + + + @Test + public void testClientRemoveAuthSessions( + @JmxInfinispanChannelStatistics() InfinispanStatistics channelStatisticsCrossDc) throws Exception { + + createInitialAuthSessions(); + + channelStatisticsCrossDc.reset(); + + // Remove test-app client + ApiUtil.findClientByClientId(getAdminClient().realm(REALM_NAME), "test-app").remove(); + + // Assert sessions removed on node1 and node2 and on remote caches. Assert that count of messages sent between DCs is not too big, however there are some messages due to removed client + assertAuthSessionsStatisticsExpected("After client removed", channelStatisticsCrossDc, + 0, 5l); + } + + + + +} diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/docker/DockerClientTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/docker/DockerClientTest.java new file mode 100644 index 0000000000..f947d9e2d5 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/docker/DockerClientTest.java @@ -0,0 +1,200 @@ +package org.keycloak.testsuite.docker; + +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.Test; +import org.keycloak.common.Profile; +import org.keycloak.representations.idm.RealmRepresentation; +import org.keycloak.testsuite.AbstractKeycloakTest; +import org.keycloak.testsuite.ProfileAssume; +import org.keycloak.testsuite.util.WaitUtils; +import org.rnorth.ducttape.ratelimits.RateLimiterBuilder; +import org.rnorth.ducttape.unreliables.Unreliables; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testcontainers.containers.BindMode; +import org.testcontainers.containers.Container; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.output.Slf4jLogConsumer; +import org.testcontainers.images.builder.ImageFromDockerfile; +import org.testcontainers.shaded.com.github.dockerjava.api.model.ContainerNetwork; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.Callable; +import java.util.concurrent.TimeUnit; +import java.util.function.Predicate; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.junit.Assume.assumeTrue; +import static org.keycloak.testsuite.admin.AbstractAdminTest.loadJson; + +public class DockerClientTest extends AbstractKeycloakTest { + public static final Logger LOGGER = LoggerFactory.getLogger(DockerClientTest.class); + + public static final String REALM_ID = "docker-test-realm"; + public static final String AUTH_FLOW = "docker-basic-auth-flow"; + public static final String CLIENT_ID = "docker-test-client"; + public static final String DOCKER_USER = "docker-user"; + public static final String DOCKER_USER_PASSWORD = "password"; + + public static final String REGISTRY_HOSTNAME = "registry.localdomain"; + public static final Integer REGISTRY_PORT = 5000; + public static final String MINIMUM_DOCKER_VERSION = "1.8.0"; + public static final String IMAGE_NAME = "busybox"; + + private GenericContainer dockerRegistryContainer = null; + private GenericContainer dockerClientContainer = null; + + private static String hostIp; + + @BeforeClass + public static void verifyEnvironment() { + ProfileAssume.assumeFeatureEnabled(Profile.Feature.DOCKER); + + final Optional dockerVersion = new DockerHostVersionSupplier().get(); + assumeTrue("Could not determine docker version for host machine. It either is not present or accessible to the JVM running the test harness.", dockerVersion.isPresent()); + assumeTrue("Docker client on host machine is not a supported version. Please upgrade and try again.", DockerVersion.COMPARATOR.compare(dockerVersion.get(), DockerVersion.parseVersionString(MINIMUM_DOCKER_VERSION)) >= 0); + LOGGER.debug("Discovered valid docker client on host. version: {}", dockerVersion); + + hostIp = System.getProperty("host.ip"); + + if (hostIp == null) { + final Optional foundHostIp = new DockerHostIpSupplier().get(); + if (foundHostIp.isPresent()) { + hostIp = foundHostIp.get(); + } + } + Assert.assertNotNull("Could not resolve host machine's IP address for docker adapter, and 'host.ip' system poperty not set. Client will not be able to authenticate against the keycloak server!", hostIp); + } + + @Override + public void addTestRealms(final List testRealms) { + final RealmRepresentation dockerRealm = loadJson(getClass().getResourceAsStream("/docker-test-realm.json"), RealmRepresentation.class); + + /** + * TODO fix test harness/importer NPEs when attempting to create realm from scratch. + * Need to fix those, would be preferred to do this programmatically such that we don't have to keep realm elements + * (I.E. certs, realm url) in sync with a flat file + * + * final RealmRepresentation dockerRealm = DockerTestRealmSetup.createRealm(REALM_ID); + * DockerTestRealmSetup.configureDockerAuthenticationFlow(dockerRealm, AUTH_FLOW); + */ + + DockerTestRealmSetup.configureDockerRegistryClient(dockerRealm, CLIENT_ID); + DockerTestRealmSetup.configureUser(dockerRealm, DOCKER_USER, DOCKER_USER_PASSWORD); + + testRealms.add(dockerRealm); + } + + @Override + public void beforeAbstractKeycloakTest() throws Exception { + super.beforeAbstractKeycloakTest(); + + final Map environment = new HashMap<>(); + environment.put("REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY", "/tmp"); + environment.put("REGISTRY_HTTP_TLS_CERTIFICATE", "/opt/certs/localhost.crt"); + environment.put("REGISTRY_HTTP_TLS_KEY", "/opt/certs/localhost.key"); + environment.put("REGISTRY_AUTH_TOKEN_REALM", "http://" + hostIp + ":8180/auth/realms/docker-test-realm/protocol/docker-v2/auth"); + environment.put("REGISTRY_AUTH_TOKEN_SERVICE", CLIENT_ID); + environment.put("REGISTRY_AUTH_TOKEN_ISSUER", "http://" + hostIp + ":8180/auth/realms/docker-test-realm"); + environment.put("REGISTRY_AUTH_TOKEN_ROOTCERTBUNDLE", "/opt/certs/docker-realm-public-key.pem"); + environment.put("INSECURE_REGISTRY", "--insecure-registry " + REGISTRY_HOSTNAME + ":" + REGISTRY_PORT); + + String dockerioPrefix = Boolean.parseBoolean(System.getProperty("docker.io-prefix-explicit")) ? "docker.io/" : ""; + + // TODO this required me to turn selinux off :(. Add BindMode options for :z and :Z. Make selinux enforcing again! + dockerRegistryContainer = new GenericContainer(dockerioPrefix + "registry:2") + .withClasspathResourceMapping("dockerClientTest/keycloak-docker-compose-yaml/certs", "/opt/certs", BindMode.READ_ONLY) + .withEnv(environment) + .withPrivilegedMode(true); + dockerRegistryContainer.start(); + dockerRegistryContainer.followOutput(new Slf4jLogConsumer(LOGGER)); + + dockerClientContainer = new GenericContainer( + new ImageFromDockerfile() + .withDockerfileFromBuilder(dockerfileBuilder -> { + dockerfileBuilder.from("centos/systemd:latest") + .run("yum", "install", "-y", "docker", "iptables", ";", "yum", "clean", "all") + .cmd("/usr/sbin/init") + .volume("/sys/fs/cgroup") + .build(); + }) + ) + .withClasspathResourceMapping("dockerClientTest/keycloak-docker-compose-yaml/certs/localhost.crt", "/opt/docker/certs.d/" + REGISTRY_HOSTNAME + "/localhost.crt", BindMode.READ_ONLY) + .withClasspathResourceMapping("dockerClientTest/keycloak-docker-compose-yaml/sysconfig_docker", "/etc/sysconfig/docker", BindMode.READ_WRITE) + .withPrivilegedMode(true); + + final Optional network = dockerRegistryContainer.getContainerInfo().getNetworkSettings().getNetworks().values().stream().findFirst(); + assumeTrue("Could not find a network adapter whereby the docker client container could connect to host!", network.isPresent()); + dockerClientContainer.withExtraHost(REGISTRY_HOSTNAME, network.get().getIpAddress()); + + dockerClientContainer.start(); + dockerClientContainer.followOutput(new Slf4jLogConsumer(LOGGER)); + + int i = 0; + String stdErr = ""; + while (i++ < 30) { + log.infof("Trying to start docker service; attempt: %d", i); + stdErr = dockerClientContainer.execInContainer("systemctl", "start", "docker.service").getStderr(); + if (stdErr.isEmpty()) { + break; + } + else { + log.info("systemctl failed: " + stdErr); + } + WaitUtils.pause(1000); + } + + assumeTrue("Cannot start docker service!", stdErr.isEmpty()); + + log.info("Waiting for docker service..."); + validateDockerStarted(); + log.info("Docker service successfully started"); + } + + private void validateDockerStarted() { + final Callable checkStrategy = () -> { + try { + final String commandResult = dockerClientContainer.execInContainer("docker", "ps").getStderr(); + return !commandResult.contains("Cannot connect"); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } catch (Exception e) { + return false; + } + }; + + Unreliables.retryUntilTrue(30, TimeUnit.SECONDS, () -> RateLimiterBuilder.newBuilder().withRate(1, TimeUnit.SECONDS).withConstantThroughput().build().getWhenReady(() -> { + try { + return checkStrategy.call(); + } catch (Exception e) { + throw new RuntimeException(e); + } + })); + } + + @Test + public void shouldPerformDockerAuthAgainstRegistry() throws Exception { + Container.ExecResult dockerLoginResult = dockerClientContainer.execInContainer("docker", "login", "-u", DOCKER_USER, "-p", DOCKER_USER_PASSWORD, REGISTRY_HOSTNAME + ":" + REGISTRY_PORT); + printNonEmpties(dockerLoginResult.getStdout(), dockerLoginResult.getStderr()); + assertThat(dockerLoginResult.getStdout(), containsString("Login Succeeded")); + } + + private static void printNonEmpties(final String... results) { + Arrays.stream(results) + .forEachOrdered(DockerClientTest::printNonEmpty); + } + + private static void printNonEmpty(final String result) { + if (nullOrEmpty.negate().test(result)) { + LOGGER.info(result); + } + } + + public static final Predicate nullOrEmpty = string -> string == null || string.isEmpty(); +} diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/docker/DockerHostIpSupplier.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/docker/DockerHostIpSupplier.java new file mode 100644 index 0000000000..b73471c7e4 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/docker/DockerHostIpSupplier.java @@ -0,0 +1,45 @@ +package org.keycloak.testsuite.docker; + +import java.net.InetAddress; +import java.net.NetworkInterface; +import java.net.SocketException; +import java.util.Collections; +import java.util.Enumeration; +import java.util.Optional; +import java.util.function.Supplier; +import java.util.regex.Pattern; + +/** + * Docker doesn't provide a static/reliable way to grab the host machine's IP. + *

    + * this currently just returns the first address for the bridge adapter starting with 'docker'. Not the most elegant solution, + * but I'm open to suggestions. + * + * @see https://github.com/moby/moby/issues/1143 and related issues referenced therein. + */ +public class DockerHostIpSupplier implements Supplier> { + + @Override + public Optional get() { + final Enumeration networkInterfaces; + try { + networkInterfaces = NetworkInterface.getNetworkInterfaces(); + } catch (SocketException e) { + return Optional.empty(); + } + + return Collections.list(networkInterfaces).stream() + .filter(networkInterface -> networkInterface.getDisplayName().startsWith("docker")) + .flatMap(networkInterface -> Collections.list(networkInterface.getInetAddresses()).stream()) + .map(InetAddress::getHostAddress) + .filter(DockerHostIpSupplier::looksLikeIpv4Address) + .findFirst(); + } + + public static boolean looksLikeIpv4Address(final String ip) { + return IPv4RegexPattern.matcher(ip).matches(); + } + + private static final Pattern IPv4RegexPattern = Pattern.compile("^(([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\.){3}([01]?\\d\\d?|2[0-4]\\d|25[0-5])$"); + +} diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/docker/DockerHostVersionSupplier.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/docker/DockerHostVersionSupplier.java new file mode 100644 index 0000000000..eac009228e --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/docker/DockerHostVersionSupplier.java @@ -0,0 +1,43 @@ +package org.keycloak.testsuite.docker; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.Optional; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +public class DockerHostVersionSupplier implements Supplier> { + private static final Logger log = LoggerFactory.getLogger(DockerHostVersionSupplier.class); + + @Override + public Optional get() { + try { + Process process = new ProcessBuilder() + .command("docker", "version", "--format", "'{{.Client.Version}}'") + .start(); + + final BufferedReader stdout = getReader(process, Process::getInputStream); + final BufferedReader err = getReader(process, Process::getErrorStream); + + int exitCode = process.waitFor(); + if (exitCode == 0) { + final String versionString = stdout.lines().collect(Collectors.joining()).replaceAll("'", ""); + return Optional.ofNullable(DockerVersion.parseVersionString(versionString)); + } + } catch (IOException | InterruptedException e) { + log.error("Could not determine host machine's docker version: ", e); + } + + return Optional.empty(); + } + + private static BufferedReader getReader(final Process process, final Function streamSelector) { + return new BufferedReader(new InputStreamReader(streamSelector.apply(process))); + } +} diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/docker/DockerTestRealmSetup.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/docker/DockerTestRealmSetup.java new file mode 100644 index 0000000000..727af1dbb8 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/docker/DockerTestRealmSetup.java @@ -0,0 +1,87 @@ +package org.keycloak.testsuite.docker; + +import org.keycloak.models.AuthenticationExecutionModel; +import org.keycloak.protocol.docker.DockerAuthV2Protocol; +import org.keycloak.protocol.docker.DockerAuthenticator; +import org.keycloak.representations.idm.AuthenticationExecutionExportRepresentation; +import org.keycloak.representations.idm.AuthenticationFlowRepresentation; +import org.keycloak.representations.idm.ClientRepresentation; +import org.keycloak.representations.idm.CredentialRepresentation; +import org.keycloak.representations.idm.RealmRepresentation; +import org.keycloak.representations.idm.UserRepresentation; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +public final class DockerTestRealmSetup { + + private DockerTestRealmSetup() { + } + + public static RealmRepresentation createRealm(final String realmId) { + final RealmRepresentation createdRealm = new RealmRepresentation(); + createdRealm.setId(UUID.randomUUID().toString()); + createdRealm.setRealm(realmId); + createdRealm.setEnabled(true); + createdRealm.setAuthenticatorConfig(new ArrayList<>()); + + return createdRealm; + } + + public static void configureDockerAuthenticationFlow(final RealmRepresentation dockerRealm, final String authFlowAlais) { + final AuthenticationFlowRepresentation dockerBasicAuthFlow = new AuthenticationFlowRepresentation(); + dockerBasicAuthFlow.setId(UUID.randomUUID().toString()); + dockerBasicAuthFlow.setAlias(authFlowAlais); + dockerBasicAuthFlow.setProviderId("basic-flow"); + dockerBasicAuthFlow.setTopLevel(true); + dockerBasicAuthFlow.setBuiltIn(false); + + final AuthenticationExecutionExportRepresentation dockerBasicAuthExecution = new AuthenticationExecutionExportRepresentation(); + dockerBasicAuthExecution.setAuthenticator(DockerAuthenticator.ID); + dockerBasicAuthExecution.setRequirement(AuthenticationExecutionModel.Requirement.REQUIRED.name()); + dockerBasicAuthExecution.setPriority(0); + dockerBasicAuthExecution.setUserSetupAllowed(false); + dockerBasicAuthExecution.setAutheticatorFlow(false); + + final List authenticationExecutions = Optional.ofNullable(dockerBasicAuthFlow.getAuthenticationExecutions()).orElse(new ArrayList<>()); + authenticationExecutions.add(dockerBasicAuthExecution); + dockerBasicAuthFlow.setAuthenticationExecutions(authenticationExecutions); + + final List authenticationFlows = Optional.ofNullable(dockerRealm.getAuthenticationFlows()).orElse(new ArrayList<>()); + authenticationFlows.add(dockerBasicAuthFlow); + dockerRealm.setAuthenticationFlows(authenticationFlows); + dockerRealm.setBrowserFlow(dockerBasicAuthFlow.getAlias()); + } + + + public static void configureDockerRegistryClient(final RealmRepresentation dockerRealm, final String clientId) { + final ClientRepresentation dockerClient = new ClientRepresentation(); + dockerClient.setClientId(clientId); + dockerClient.setProtocol(DockerAuthV2Protocol.LOGIN_PROTOCOL); + dockerClient.setEnabled(true); + + final List clients = Optional.ofNullable(dockerRealm.getClients()).orElse(new ArrayList<>()); + clients.add(dockerClient); + dockerRealm.setClients(clients); + } + + public static void configureUser(final RealmRepresentation dockerRealm, final String username, final String password) { + final UserRepresentation dockerUser = new UserRepresentation(); + dockerUser.setUsername(username); + dockerUser.setEnabled(true); + dockerUser.setEmail("docker-users@localhost.localdomain"); + dockerUser.setFirstName("docker"); + dockerUser.setLastName("user"); + + final CredentialRepresentation dockerUserCreds = new CredentialRepresentation(); + dockerUserCreds.setType(CredentialRepresentation.PASSWORD); + dockerUserCreds.setValue(password); + dockerUser.setCredentials(Collections.singletonList(dockerUserCreds)); + + dockerRealm.setUsers(Collections.singletonList(dockerUser)); + } + +} diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/docker/DockerVersion.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/docker/DockerVersion.java new file mode 100644 index 0000000000..7182c54ecd --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/docker/DockerVersion.java @@ -0,0 +1,99 @@ +package org.keycloak.testsuite.docker; + +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Collectors; + +public class DockerVersion { + + public static final Integer MAJOR_VERSION_INDEX = 0; + public static final Integer MINOR_VERSION_INDEX = 1; + public static final Integer PATCH_VERSION_INDEX = 2; + + private final Integer major; + private final Integer minor; + private final Integer patch; + + public static final Comparator COMPARATOR = (lhs, rhs) -> Comparator.comparing(DockerVersion::getMajor) + .thenComparing(Comparator.comparing(DockerVersion::getMinor) + .thenComparing(Comparator.comparing(DockerVersion::getPatch))) + .compare(lhs, rhs); + + /** + * Major version is required. minor and patch versions will be assumed '0' if not provided. + */ + public DockerVersion(final Integer major, final Optional minor, final Optional patch) { + Objects.requireNonNull(major, "Invalid docker version - no major release number given"); + + this.major = major; + this.minor = minor.orElse(0); + this.patch = patch.orElse(0); + } + + /** + * @param versionString given in the form '1.12.6' + */ + public static DockerVersion parseVersionString(final String versionString) { + Objects.requireNonNull(versionString, "Cannot parse null docker version string"); + + final List versionNumberList = Arrays.stream(stripDashAndEdition(versionString).trim().split("\\.")) + .map(Integer::parseInt) + .collect(Collectors.toList()); + + return new DockerVersion(versionNumberList.get(MAJOR_VERSION_INDEX), + Optional.ofNullable(versionNumberList.get(MINOR_VERSION_INDEX)), + Optional.ofNullable(versionNumberList.get(PATCH_VERSION_INDEX))); + } + + private static String stripDashAndEdition(final String versionString) { + if (versionString.contains("-")) { + return versionString.substring(0, versionString.indexOf("-")); + } + + return versionString; + } + + public Integer getMajor() { + return major; + } + + public Integer getMinor() { + return minor; + } + + public Integer getPatch() { + return patch; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + DockerVersion that = (DockerVersion) o; + + if (major != null ? !major.equals(that.major) : that.major != null) return false; + if (minor != null ? !minor.equals(that.minor) : that.minor != null) return false; + return patch != null ? patch.equals(that.patch) : that.patch == null; + } + + @Override + public int hashCode() { + int result = major != null ? major.hashCode() : 0; + result = 31 * result + (minor != null ? minor.hashCode() : 0); + result = 31 * result + (patch != null ? patch.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return "DockerVersion{" + + "major=" + major + + ", minor=" + minor + + ", patch=" + patch + + '}'; + } +} diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/BrowserButtonsTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/BrowserButtonsTest.java index 08d826522e..da54a72301 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/BrowserButtonsTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/BrowserButtonsTest.java @@ -263,11 +263,12 @@ public class BrowserButtonsTest extends AbstractTestRealmKeycloakTest { registerPage.clickBackToLogin(); loginPage.assertCurrent(); - // Click browser "back" button. Should be back on register page + // Click browser "back" button. driver.navigate().back(); registerPage.assertCurrent(); } + @Test public void clickBackButtonFromRegisterPage() { loginPage.open(); @@ -280,6 +281,28 @@ public class BrowserButtonsTest extends AbstractTestRealmKeycloakTest { } + // KEYCLOAK-5136 + @Test + public void clickRefreshButtonOnRegisterPage() { + loginPage.open(); + loginPage.clickRegister(); + registerPage.assertCurrent(); + + // Click browser "refresh" button. Should be still on register page + driver.navigate().refresh(); + registerPage.assertCurrent(); + + // Click 'back to login'. Should be on login page + registerPage.clickBackToLogin(); + loginPage.assertCurrent(); + + // Click browser 'refresh'. Should be still on login page + driver.navigate().refresh(); + loginPage.assertCurrent(); + + } + + @Test public void backButtonToAuthorizationEndpoint() { loginPage.open(); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/ResetPasswordTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/ResetPasswordTest.java index 16d85ef370..c5147b9d70 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/ResetPasswordTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/ResetPasswordTest.java @@ -371,7 +371,7 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest { loginPage.assertCurrent(); - assertEquals("You took too long to login. Login process starting from beginning.", loginPage.getError()); + assertEquals("Action expired. Please start again.", loginPage.getError()); events.expectRequiredAction(EventType.EXECUTE_ACTION_TOKEN_ERROR).error("expired_code").client((String) null).user(userId).session((String) null).clearDetails().detail(Details.ACTION, ResetCredentialsActionToken.TOKEN_TYPE).assertEvent(); } finally { @@ -407,7 +407,7 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest { loginPage.assertCurrent(); - assertEquals("You took too long to login. Login process starting from beginning.", loginPage.getError()); + assertEquals("Action expired. Please start again.", loginPage.getError()); events.expectRequiredAction(EventType.EXECUTE_ACTION_TOKEN_ERROR).error("expired_code").client((String) null).user(userId).session((String) null).clearDetails().detail(Details.ACTION, ResetCredentialsActionToken.TOKEN_TYPE).assertEvent(); } finally { @@ -450,7 +450,7 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest { driver.navigate().to(changePasswordUrl.trim()); errorPage.assertCurrent(); - Assert.assertEquals("Reset Credential not allowed", errorPage.getError()); + Assert.assertEquals("Action expired.", errorPage.getError()); String backToAppLink = errorPage.getBackToApplicationLink(); Assert.assertTrue(backToAppLink.endsWith("/app/auth")); @@ -463,6 +463,57 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest { } } + + // KEYCLOAK-5061 + @Test + public void resetPasswordExpiredCodeForgotPasswordFlow() throws IOException, MessagingException, InterruptedException { + final AtomicInteger originalValue = new AtomicInteger(); + + RealmRepresentation realmRep = testRealm().toRepresentation(); + originalValue.set(realmRep.getActionTokenGeneratedByUserLifespan()); + realmRep.setActionTokenGeneratedByUserLifespan(60); + testRealm().update(realmRep); + + try { + // Redirect directly to KC "forgot password" endpoint instead of "authenticate" endpoint + String loginUrl = oauth.getLoginFormUrl(); + String forgotPasswordUrl = loginUrl.replace("/auth?", "/forgot-credentials?"); // Workaround, but works + + driver.navigate().to(forgotPasswordUrl); + resetPasswordPage.assertCurrent(); + resetPasswordPage.changePassword("login-test"); + + loginPage.assertCurrent(); + assertEquals("You should receive an email shortly with further instructions.", loginPage.getSuccessMessage()); + expectedMessagesCount++; + + events.expectRequiredAction(EventType.SEND_RESET_PASSWORD) + .session((String)null) + .user(userId).detail(Details.USERNAME, "login-test").detail(Details.EMAIL, "login@test.com").assertEvent(); + + assertEquals(1, greenMail.getReceivedMessages().length); + + MimeMessage message = greenMail.getReceivedMessages()[0]; + + String changePasswordUrl = getPasswordResetEmailLink(message); + + setTimeOffset(70); + + driver.navigate().to(changePasswordUrl.trim()); + + resetPasswordPage.assertCurrent(); + + assertEquals("Action expired. Please start again.", loginPage.getError()); + + events.expectRequiredAction(EventType.EXECUTE_ACTION_TOKEN_ERROR).error("expired_code").client((String) null).user(userId).session((String) null).clearDetails().detail(Details.ACTION, ResetCredentialsActionToken.TOKEN_TYPE).assertEvent(); + } finally { + setTimeOffset(0); + + realmRep.setActionTokenGeneratedByUserLifespan(originalValue.get()); + testRealm().update(realmRep); + } + } + @Test public void resetPasswordDisabledUser() throws IOException, MessagingException, InterruptedException { UserRepresentation user = findUser("login-test"); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/keys/KeyRotationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/keys/KeyRotationTest.java index 068c426be1..9700a296ab 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/keys/KeyRotationTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/keys/KeyRotationTest.java @@ -23,6 +23,9 @@ import org.jboss.arquillian.graphene.page.Page; import org.junit.Rule; import org.junit.Test; import org.keycloak.RSATokenVerifier; +import org.keycloak.client.registration.Auth; +import org.keycloak.client.registration.ClientRegistration; +import org.keycloak.client.registration.ClientRegistrationException; import org.keycloak.common.VerificationException; import org.keycloak.common.util.KeyUtils; import org.keycloak.common.util.MultivaluedHashMap; @@ -31,6 +34,8 @@ import org.keycloak.keys.Attributes; import org.keycloak.keys.GeneratedHmacKeyProviderFactory; import org.keycloak.keys.KeyProvider; import org.keycloak.keys.ImportedRsaKeyProviderFactory; +import org.keycloak.representations.idm.ClientInitialAccessCreatePresentation; +import org.keycloak.representations.idm.ClientInitialAccessPresentation; import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.ComponentRepresentation; import org.keycloak.representations.idm.KeysMetadataRepresentation; @@ -41,11 +46,11 @@ import org.keycloak.testsuite.admin.ApiUtil; import org.keycloak.testsuite.pages.AppPage; import org.keycloak.testsuite.pages.AppPage.RequestType; import org.keycloak.testsuite.pages.LoginPage; +import org.keycloak.testsuite.util.ClientBuilder; import org.keycloak.testsuite.util.KeycloakModelUtils; import org.keycloak.testsuite.util.OAuthClient; import org.keycloak.testsuite.util.UserInfoClientUtil; -import javax.ws.rs.client.ClientBuilder; import javax.ws.rs.core.Response; import java.io.IOException; import java.security.KeyPair; @@ -127,12 +132,27 @@ public class KeyRotationTest extends AbstractKeycloakTest { assertTokenSignature(key1, response.getAccessToken()); assertTokenSignature(key1, response.getRefreshToken()); + // Create client with keys #1 + ClientInitialAccessCreatePresentation initialToken = new ClientInitialAccessCreatePresentation(); + initialToken.setCount(100); + initialToken.setExpiration(0); + ClientInitialAccessPresentation accessRep = adminClient.realm("test").clientInitialAccess().create(initialToken); + String initialAccessToken = accessRep.getToken(); + + ClientRegistration reg = ClientRegistration.create().url(suiteContext.getAuthServerInfo().getContextRoot() + "/auth", "test").build(); + reg.auth(Auth.token(initialAccessToken)); + ClientRepresentation clientRep = reg.create(ClientBuilder.create().clientId("test").build()); + // Userinfo with keys #1 assertUserInfo(response.getAccessToken(), 200); // Token introspection with keys #1 assertTokenIntrospection(response.getAccessToken(), true); + // Get client with keys #1 - registration access token should not have changed + ClientRepresentation clientRep2 = reg.auth(Auth.token(clientRep.getRegistrationAccessToken())).get("test"); + assertEquals(clientRep.getRegistrationAccessToken(), clientRep2.getRegistrationAccessToken()); + // Create keys #2 PublicKey key2 = createKeys2(); @@ -148,6 +168,10 @@ public class KeyRotationTest extends AbstractKeycloakTest { // Token introspection with keys #2 assertTokenIntrospection(response.getAccessToken(), true); + // Get client with keys #2 - registration access token should be changed + ClientRepresentation clientRep3 = reg.auth(Auth.token(clientRep.getRegistrationAccessToken())).get("test"); + assertNotEquals(clientRep.getRegistrationAccessToken(), clientRep3.getRegistrationAccessToken()); + // Drop key #1 dropKeys1(); @@ -162,6 +186,17 @@ public class KeyRotationTest extends AbstractKeycloakTest { // Token introspection with keys #1 dropped assertTokenIntrospection(response.getAccessToken(), true); + // Get client with keys #1 - should fail + try { + reg.auth(Auth.token(clientRep.getRegistrationAccessToken())).get("test"); + fail("Expected to fail"); + } catch (ClientRegistrationException e) { + } + + // Get client with keys #2 - should succeed + ClientRepresentation clientRep4 = reg.auth(Auth.token(clientRep3.getRegistrationAccessToken())).get("test"); + assertNotEquals(clientRep2.getRegistrationAccessToken(), clientRep4.getRegistrationAccessToken()); + // Drop key #2 dropKeys2(); @@ -292,7 +327,7 @@ public class KeyRotationTest extends AbstractKeycloakTest { } private void assertUserInfo(String token, int expectedStatus) { - Response userInfoResponse = UserInfoClientUtil.executeUserInfoRequest_getMethod(ClientBuilder.newClient(), token); + Response userInfoResponse = UserInfoClientUtil.executeUserInfoRequest_getMethod(javax.ws.rs.client.ClientBuilder.newClient(), token); assertEquals(expectedStatus, userInfoResponse.getStatus()); userInfoResponse.close(); } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/MigrationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/MigrationTest.java index a769687f0d..cfdf0b7979 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/MigrationTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/MigrationTest.java @@ -59,6 +59,7 @@ import org.keycloak.testsuite.runonserver.RunHelpers; import org.keycloak.testsuite.runonserver.RunOnServerDeployment; import org.keycloak.testsuite.util.OAuthClient; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.keycloak.models.AccountRoles.MANAGE_ACCOUNT; import static org.keycloak.models.AccountRoles.MANAGE_ACCOUNT_LINKS; @@ -216,6 +217,20 @@ public class MigrationTest extends AbstractKeycloakTest { private void testMigrationTo3_2_0() { assertNull(masterRealm.toRepresentation().getPasswordPolicy()); assertNull(migrationRealm.toRepresentation().getPasswordPolicy()); + + testDockerAuthenticationFlow(masterRealm, migrationRealm); + } + + private void testDockerAuthenticationFlow(RealmResource... realms) { + for (RealmResource realm : realms) { + AuthenticationFlowRepresentation flow = null; + for (AuthenticationFlowRepresentation f : realm.flows().getFlows()) { + if (DefaultAuthenticationFlows.DOCKER_AUTH.equals(f.getAlias())) { + flow = f; + } + } + assertNotNull(flow); + } } private void testRoleManageAccountLinks(RealmResource... realms) { diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/LoginStatusIframeEndpointTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/LoginStatusIframeEndpointTest.java index 84144f6702..b480cdbca4 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/LoginStatusIframeEndpointTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/LoginStatusIframeEndpointTest.java @@ -30,13 +30,19 @@ import org.apache.http.impl.client.BasicCookieStore; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; import org.apache.http.message.BasicNameValuePair; +import org.jboss.arquillian.container.test.api.Deployment; +import org.jboss.shrinkwrap.api.spec.WebArchive; import org.junit.Test; import org.keycloak.admin.client.resource.ClientResource; +import org.keycloak.admin.client.resource.UserResource; +import org.keycloak.common.Version; import org.keycloak.models.Constants; import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.testsuite.AbstractKeycloakTest; import org.keycloak.testsuite.ActionURIUtils; +import org.keycloak.testsuite.runonserver.RunOnServerDeployment; +import org.keycloak.testsuite.runonserver.ServerVersion; import java.io.IOException; import java.net.URLEncoder; @@ -56,6 +62,11 @@ import static org.junit.Assert.assertTrue; */ public class LoginStatusIframeEndpointTest extends AbstractKeycloakTest { + @Deployment + public static WebArchive deploy() { + return RunOnServerDeployment.create(LoginStatusIframeEndpointTest.class, ServerVersion.class); + } + @Test public void checkIframe() throws IOException { CookieStore cookieStore = new BasicCookieStore(); @@ -185,6 +196,28 @@ public class LoginStatusIframeEndpointTest extends AbstractKeycloakTest { } } + @Test + public void checkIframeCache() throws IOException { + String version = testingClient.server().fetch(new ServerVersion()); + + CloseableHttpClient client = HttpClients.createDefault(); + try { + HttpGet get = new HttpGet(suiteContext.getAuthServerInfo().getContextRoot() + "/auth/realms/master/protocol/openid-connect/login-status-iframe.html"); + CloseableHttpResponse response = client.execute(get); + + assertEquals(200, response.getStatusLine().getStatusCode()); + assertEquals("no-cache, must-revalidate, no-transform, no-store", response.getHeaders("Cache-Control")[0].getValue()); + + get = new HttpGet(suiteContext.getAuthServerInfo().getContextRoot() + "/auth/realms/master/protocol/openid-connect/login-status-iframe.html?version=" + version); + response = client.execute(get); + + assertEquals(200, response.getStatusLine().getStatusCode()); + assertTrue(response.getHeaders("Cache-Control")[0].getValue().contains("max-age")); + } finally { + client.close(); + } + } + @Override public void addTestRealms(List testRealms) { } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OAuthRedirectUriStateTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OAuthRedirectUriStateTest.java new file mode 100644 index 0000000000..db410d5e71 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OAuthRedirectUriStateTest.java @@ -0,0 +1,68 @@ +/* + * Copyright 2016 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.testsuite.oauth; + +import java.net.MalformedURLException; +import java.net.URL; + +import org.junit.Before; +import org.junit.Test; +import org.keycloak.protocol.oidc.utils.OIDCResponseType; +import org.keycloak.representations.idm.RealmRepresentation; +import org.keycloak.testsuite.AbstractTestRealmKeycloakTest; +import org.keycloak.testsuite.Assert; +import org.keycloak.testsuite.util.OAuthClient; + +public class OAuthRedirectUriStateTest extends AbstractTestRealmKeycloakTest { + + @Override + public void configureTestRealm(RealmRepresentation testRealm) { + } + + @Before + public void clientConfiguration() { + oauth.clientId("test-app"); + oauth.responseType(OIDCResponseType.CODE); + oauth.stateParamRandom(); + } + + void assertStateReflected(String state) { + oauth.stateParamHardcoded(state); + + OAuthClient.AuthorizationEndpointResponse response = oauth.doLogin("test-user@localhost", "password"); + Assert.assertNotNull(response.getCode()); + + URL url; + try { + url = new URL(driver.getCurrentUrl()); + } catch (MalformedURLException e) { + throw new RuntimeException(e); + } + Assert.assertTrue(url.getQuery().contains("state=" + state)); + } + + @Test + public void testSimpleStateParameter() { + assertStateReflected("VeryLittleGravitasIndeed"); + } + + @Test + public void testJsonStateParameter() { + assertStateReflected("%7B%22csrf_token%22%3A%2B%22hlvZNIsWyqdkEhbjlQIia0ty2YY4TXat%22%2C%2B%22destination%22%3A%2B%22eyJhbGciOiJIUzI1NiJ9.Imh0dHA6Ly9sb2NhbGhvc3Q6NTAwMC9wcml2YXRlIg.T18WeIV29komDl8jav-3bSnUZDlMD8VOfIrd2ikP5zE%22%7D"); + } +} 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 c0c8601614..6f4e394a55 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 @@ -142,6 +142,14 @@ public class OAuthRedirectUriTest extends AbstractKeycloakTest { Assert.assertEquals("Invalid parameter: redirect_uri", errorPage.getError()); } + @Test + public void testFileUri() throws IOException { + oauth.redirectUri("file://test"); + oauth.openLoginForm(); + Assert.assertTrue(errorPage.isCurrent()); + Assert.assertEquals("Invalid parameter: redirect_uri", errorPage.getError()); + } + @Test public void testNoParamMultipleValidUris() throws IOException { ClientManager.realm(adminClient.realm("test")).clientId("test-app").addRedirectUris("http://localhost:8180/app2"); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/RefreshTokenTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/RefreshTokenTest.java index 208e6e3a9e..7e3594f897 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/RefreshTokenTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/RefreshTokenTest.java @@ -27,15 +27,18 @@ import org.keycloak.events.Details; import org.keycloak.events.Errors; import org.keycloak.protocol.oidc.OIDCLoginProtocolService; import org.keycloak.representations.AccessToken; +import org.keycloak.representations.IDToken; import org.keycloak.representations.RefreshToken; import org.keycloak.representations.idm.EventRepresentation; import org.keycloak.representations.idm.RealmRepresentation; +import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.testsuite.AbstractKeycloakTest; import org.keycloak.testsuite.AssertEvents; import org.keycloak.testsuite.util.ClientManager; import org.keycloak.testsuite.util.OAuthClient; import org.keycloak.testsuite.util.RealmBuilder; import org.keycloak.testsuite.util.RealmManager; +import org.keycloak.testsuite.util.UserManager; import org.keycloak.util.BasicAuthHelper; import javax.ws.rs.client.Client; @@ -488,6 +491,61 @@ public class RefreshTokenTest extends AbstractKeycloakTest { } + @Test + public void refreshTokenUserDisabled() throws Exception { + oauth.doLogin("test-user@localhost", "password"); + + EventRepresentation loginEvent = events.expectLogin().assertEvent(); + + String sessionId = loginEvent.getSessionId(); + String codeId = loginEvent.getDetails().get(Details.CODE_ID); + + String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE); + + OAuthClient.AccessTokenResponse response = oauth.doAccessTokenRequest(code, "password"); + String refreshTokenString = response.getRefreshToken(); + RefreshToken refreshToken = oauth.verifyRefreshToken(refreshTokenString); + + events.expectCodeToToken(codeId, sessionId).assertEvent(); + + try { + UserManager.realm(adminClient.realm("test")).username("test-user@localhost").enabled(false); + response = oauth.doRefreshTokenRequest(refreshTokenString, "password"); + assertEquals(400, response.getStatusCode()); + assertEquals("invalid_grant", response.getError()); + + events.expectRefresh(refreshToken.getId(), sessionId).clearDetails().error(Errors.INVALID_TOKEN).assertEvent(); + } finally { + UserManager.realm(adminClient.realm("test")).username("test-user@localhost").enabled(true); + } + } + + @Test + public void refreshTokenUserDeleted() throws Exception { + String userId = createUser("test", "temp-user@localhost", "password"); + oauth.doLogin("temp-user@localhost", "password"); + + EventRepresentation loginEvent = events.expectLogin().user(userId).assertEvent(); + + String sessionId = loginEvent.getSessionId(); + String codeId = loginEvent.getDetails().get(Details.CODE_ID); + + String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE); + + OAuthClient.AccessTokenResponse response = oauth.doAccessTokenRequest(code, "password"); + String refreshTokenString = response.getRefreshToken(); + RefreshToken refreshToken = oauth.verifyRefreshToken(refreshTokenString); + + events.expectCodeToToken(codeId, sessionId).user(userId).assertEvent(); + + adminClient.realm("test").users().delete(userId); + response = oauth.doRefreshTokenRequest(refreshTokenString, "password"); + assertEquals(400, response.getStatusCode()); + assertEquals("invalid_grant", response.getError()); + + events.expectRefresh(refreshToken.getId(), sessionId).user(userId).clearDetails().error(Errors.INVALID_TOKEN).assertEvent(); + } + protected Response executeRefreshToken(WebTarget refreshTarget, String refreshToken) { String header = BasicAuthHelper.createHeader("test-app", "password"); Form form = new Form(); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/TokenExchangeTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/TokenExchangeTest.java new file mode 100755 index 0000000000..2889118efd --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/TokenExchangeTest.java @@ -0,0 +1,204 @@ +/* + * Copyright 2016 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.testsuite.oauth; + +import org.jboss.arquillian.container.test.api.Deployment; +import org.jboss.shrinkwrap.api.spec.WebArchive; +import org.junit.Rule; +import org.junit.Test; +import org.keycloak.OAuth2Constants; +import org.keycloak.TokenVerifier; +import org.keycloak.authorization.model.Policy; +import org.keycloak.authorization.model.ResourceServer; +import org.keycloak.models.ClientModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.models.RoleModel; +import org.keycloak.models.UserCredentialModel; +import org.keycloak.models.UserModel; +import org.keycloak.protocol.oidc.OIDCLoginProtocol; +import org.keycloak.representations.AccessToken; +import org.keycloak.representations.idm.RealmRepresentation; +import org.keycloak.representations.idm.authorization.ClientPolicyRepresentation; +import org.keycloak.representations.idm.authorization.UserPolicyRepresentation; +import org.keycloak.services.resources.admin.permissions.AdminPermissionManagement; +import org.keycloak.services.resources.admin.permissions.AdminPermissions; +import org.keycloak.testsuite.AbstractKeycloakTest; +import org.keycloak.testsuite.Assert; +import org.keycloak.testsuite.AssertEvents; +import org.keycloak.testsuite.runonserver.RunOnServerDeployment; +import org.keycloak.testsuite.util.OAuthClient; + +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.keycloak.testsuite.auth.page.AuthRealm.TEST; + +/** + * @author Stian Thorgersen + */ +public class TokenExchangeTest extends AbstractKeycloakTest { + + @Rule + public AssertEvents events = new AssertEvents(this); + + @Deployment + public static WebArchive deploy() { + return RunOnServerDeployment.create(TokenExchangeTest.class); + } + + @Override + public void addTestRealms(List testRealms) { + RealmRepresentation testRealmRep = new RealmRepresentation(); + testRealmRep.setId(TEST); + testRealmRep.setRealm(TEST); + testRealmRep.setEnabled(true); + testRealms.add(testRealmRep); + } + + public static void setupRealm(KeycloakSession session) { + RealmModel realm = session.realms().getRealmByName(TEST); + + RoleModel exampleRole = realm.addRole("example"); + + ClientModel target = realm.addClient("target"); + target.setDirectAccessGrantsEnabled(true); + target.setEnabled(true); + target.setSecret("secret"); + target.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL); + target.setFullScopeAllowed(false); + target.addScopeMapping(exampleRole); + + ClientModel clientExchanger = realm.addClient("client-exchanger"); + clientExchanger.setClientId("client-exchanger"); + clientExchanger.setPublicClient(false); + clientExchanger.setDirectAccessGrantsEnabled(true); + clientExchanger.setEnabled(true); + clientExchanger.setSecret("secret"); + clientExchanger.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL); + clientExchanger.setFullScopeAllowed(false); + + ClientModel illegal = realm.addClient("illegal"); + illegal.setClientId("illegal"); + illegal.setPublicClient(false); + illegal.setDirectAccessGrantsEnabled(true); + illegal.setEnabled(true); + illegal.setSecret("secret"); + illegal.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL); + illegal.setFullScopeAllowed(false); + + ClientModel illegalTo = realm.addClient("illegal-to"); + illegalTo.setClientId("illegal-to"); + illegalTo.setPublicClient(false); + illegalTo.setDirectAccessGrantsEnabled(true); + illegalTo.setEnabled(true); + illegalTo.setSecret("secret"); + illegalTo.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL); + illegalTo.setFullScopeAllowed(false); + + ClientModel legal = realm.addClient("legal"); + legal.setClientId("legal"); + legal.setPublicClient(false); + legal.setDirectAccessGrantsEnabled(true); + legal.setEnabled(true); + legal.setSecret("secret"); + legal.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL); + legal.setFullScopeAllowed(false); + + AdminPermissionManagement management = AdminPermissions.management(session, realm); + + management.clients().setPermissionsEnabled(target, true); + ClientPolicyRepresentation clientRep = new ClientPolicyRepresentation(); + clientRep.setName("to"); + clientRep.addClient(clientExchanger.getId()); + clientRep.addClient(legal.getId()); + ResourceServer server = management.realmResourceServer(); + Policy clientPolicy = management.authz().getStoreFactory().getPolicyStore().create(clientRep, server); + management.clients().exchangeToPermission(target).addAssociatedPolicy(clientPolicy); + + management.clients().setPermissionsEnabled(clientExchanger, true); + ClientPolicyRepresentation client2Rep = new ClientPolicyRepresentation(); + client2Rep.setName("from"); + client2Rep.addClient(legal.getId()); + client2Rep.addClient(illegalTo.getId()); + Policy client2Policy = management.authz().getStoreFactory().getPolicyStore().create(client2Rep, server); + management.clients().exchangeFromPermission(clientExchanger).addAssociatedPolicy(client2Policy); + + + UserModel user = session.users().addUser(realm, "user"); + user.setEnabled(true); + session.userCredentialManager().updateCredential(realm, user, UserCredentialModel.password("password")); + user.grantRole(exampleRole); + + } + + @Override + protected boolean isImportAfterEachMethod() { + return true; + } + + + @Test + public void testExchange() throws Exception { + testingClient.server().run(TokenExchangeTest::setupRealm); + + oauth.realm(TEST); + oauth.clientId("client-exchanger"); + + OAuthClient.AccessTokenResponse response = oauth.doGrantAccessTokenRequest("secret", "user", "password"); + String accessToken = response.getAccessToken(); + TokenVerifier accessTokenVerifier = TokenVerifier.create(accessToken, AccessToken.class); + AccessToken token = accessTokenVerifier.parse().getToken(); + Assert.assertEquals(token.getPreferredUsername(), "user"); + Assert.assertTrue(token.getRealmAccess() == null || !token.getRealmAccess().isUserInRole("example")); + + { + response = oauth.doTokenExchange(TEST, accessToken, "target", "client-exchanger", "secret"); + + String exchangedTokenString = response.getAccessToken(); + TokenVerifier verifier = TokenVerifier.create(exchangedTokenString, AccessToken.class); + AccessToken exchangedToken = verifier.parse().getToken(); + Assert.assertEquals("client-exchanger", exchangedToken.getIssuedFor()); + Assert.assertEquals("target", exchangedToken.getAudience()[0]); + Assert.assertEquals(exchangedToken.getPreferredUsername(), "user"); + Assert.assertTrue(exchangedToken.getRealmAccess().isUserInRole("example")); + } + + { + response = oauth.doTokenExchange(TEST, accessToken, "target", "legal", "secret"); + + String exchangedTokenString = response.getAccessToken(); + TokenVerifier verifier = TokenVerifier.create(exchangedTokenString, AccessToken.class); + AccessToken exchangedToken = verifier.parse().getToken(); + Assert.assertEquals("legal", exchangedToken.getIssuedFor()); + Assert.assertEquals("target", exchangedToken.getAudience()[0]); + Assert.assertEquals(exchangedToken.getPreferredUsername(), "user"); + Assert.assertTrue(exchangedToken.getRealmAccess().isUserInRole("example")); + } + { + response = oauth.doTokenExchange(TEST, accessToken, "target", "illegal", "secret"); + Assert.assertEquals(403, response.getStatusCode()); + } + { + response = oauth.doTokenExchange(TEST, accessToken, "target", "illegal-to", "secret"); + Assert.assertEquals(403, response.getStatusCode()); + } + + + } +} 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 0203eb13f2..c4a6c459cf 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 @@ -29,6 +29,7 @@ import org.keycloak.representations.IDToken; import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.services.clientregistration.ClientRegistrationService; import org.keycloak.services.clientregistration.oidc.OIDCClientRegistrationProviderFactory; +import org.keycloak.services.resources.Cors; import org.keycloak.services.resources.RealmsResource; import org.keycloak.testsuite.AbstractKeycloakTest; import org.keycloak.testsuite.Assert; @@ -38,12 +39,16 @@ import org.keycloak.testsuite.util.OAuthClient; import javax.ws.rs.client.Client; import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.client.Invocation; import javax.ws.rs.client.WebTarget; import javax.ws.rs.core.Response; import javax.ws.rs.core.UriBuilder; import java.net.URI; import java.util.List; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + /** * @author Marek Posolda */ @@ -75,10 +80,10 @@ public class OIDCWellKnownProviderTest extends AbstractKeycloakTest { OIDCConfigurationRepresentation oidcConfig = getOIDCDiscoveryConfiguration(client); // URIs are filled - Assert.assertEquals(oidcConfig.getAuthorizationEndpoint(), OIDCLoginProtocolService.authUrl(UriBuilder.fromUri(OAuthClient.AUTH_SERVER_ROOT)).build("test").toString()); - Assert.assertEquals(oidcConfig.getTokenEndpoint(), oauth.getAccessTokenUrl()); - Assert.assertEquals(oidcConfig.getUserinfoEndpoint(), OIDCLoginProtocolService.userInfoUrl(UriBuilder.fromUri(OAuthClient.AUTH_SERVER_ROOT)).build("test").toString()); - Assert.assertEquals(oidcConfig.getJwksUri(), oauth.getCertsUrl("test")); + assertEquals(oidcConfig.getAuthorizationEndpoint(), OIDCLoginProtocolService.authUrl(UriBuilder.fromUri(OAuthClient.AUTH_SERVER_ROOT)).build("test").toString()); + assertEquals(oidcConfig.getTokenEndpoint(), oauth.getAccessTokenUrl()); + assertEquals(oidcConfig.getUserinfoEndpoint(), OIDCLoginProtocolService.userInfoUrl(UriBuilder.fromUri(OAuthClient.AUTH_SERVER_ROOT)).build("test").toString()); + assertEquals(oidcConfig.getJwksUri(), oauth.getCertsUrl("test")); String registrationUri = UriBuilder .fromUri(OAuthClient.AUTH_SERVER_ROOT) @@ -87,7 +92,7 @@ public class OIDCWellKnownProviderTest extends AbstractKeycloakTest { .path(ClientRegistrationService.class, "provider") .build("test", OIDCClientRegistrationProviderFactory.ID) .toString(); - Assert.assertEquals(oidcConfig.getRegistrationEndpoint(), registrationUri); + assertEquals(oidcConfig.getRegistrationEndpoint(), registrationUri); // Support standard + implicit + hybrid flow assertContains(oidcConfig.getResponseTypesSupported(), OAuth2Constants.CODE, OIDCResponseType.ID_TOKEN, "id_token token", "code id_token", "code token", "code id_token token"); @@ -123,7 +128,7 @@ public class OIDCWellKnownProviderTest extends AbstractKeycloakTest { public void testIssuerMatches() throws Exception { OAuthClient.AuthorizationEndpointResponse authzResp = oauth.doLogin("test-user@localhost", "password"); OAuthClient.AccessTokenResponse response = oauth.doAccessTokenRequest(authzResp.getCode(), "password"); - Assert.assertEquals(200, response.getStatusCode()); + assertEquals(200, response.getStatusCode()); IDToken idToken = oauth.verifyIDToken(response.getIdToken()); Client client = ClientBuilder.newClient(); @@ -131,18 +136,36 @@ public class OIDCWellKnownProviderTest extends AbstractKeycloakTest { OIDCConfigurationRepresentation oidcConfig = getOIDCDiscoveryConfiguration(client); // assert issuer matches - Assert.assertEquals(idToken.getIssuer(), oidcConfig.getIssuer()); + assertEquals(idToken.getIssuer(), oidcConfig.getIssuer()); } finally { client.close(); } } + @Test + public void corsTest() { + Client client = ClientBuilder.newClient(); + UriBuilder builder = UriBuilder.fromUri(OAuthClient.AUTH_SERVER_ROOT); + URI oidcDiscoveryUri = RealmsResource.wellKnownProviderUrl(builder).build("test", OIDCWellKnownProviderFactory.PROVIDER_ID); + WebTarget oidcDiscoveryTarget = client.target(oidcDiscoveryUri); + + + Invocation.Builder request = oidcDiscoveryTarget.request(); + request.header(Cors.ORIGIN_HEADER, "http://somehost"); + Response response = request.get(); + + assertEquals("*", response.getHeaders().getFirst(Cors.ACCESS_CONTROL_ALLOW_ORIGIN)); + } + private OIDCConfigurationRepresentation getOIDCDiscoveryConfiguration(Client client) { UriBuilder builder = UriBuilder.fromUri(OAuthClient.AUTH_SERVER_ROOT); URI oidcDiscoveryUri = RealmsResource.wellKnownProviderUrl(builder).build("test", OIDCWellKnownProviderFactory.PROVIDER_ID); WebTarget oidcDiscoveryTarget = client.target(oidcDiscoveryUri); Response response = oidcDiscoveryTarget.request().get(); + + assertEquals("no-cache, must-revalidate, no-transform, no-store", response.getHeaders().getFirst("Cache-Control")); + return response.readEntity(OIDCConfigurationRepresentation.class); } 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 c57b64e3a0..2a846730d0 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 @@ -232,13 +232,12 @@ public class UserInfoTest extends AbstractKeycloakTest { Response response = UserInfoClientUtil.executeUserInfoRequest_getMethod(client, accessTokenResponse.getToken()); - assertEquals(Status.BAD_REQUEST.getStatusCode(), response.getStatus()); + assertEquals(Status.UNAUTHORIZED.getStatusCode(), response.getStatus()); response.close(); events.expect(EventType.USER_INFO_REQUEST_ERROR) .error(Errors.USER_SESSION_NOT_FOUND) - .client((String) null) .user(Matchers.nullValue(String.class)) .session(Matchers.nullValue(String.class)) .detail(Details.AUTH_METHOD, Details.VALIDATE_ACCESS_TOKEN) diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/runonserver/RunOnServerTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/runonserver/RunOnServerTest.java index 4101ffc150..4c0862d05b 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/runonserver/RunOnServerTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/runonserver/RunOnServerTest.java @@ -44,6 +44,12 @@ public class RunOnServerTest extends AbstractKeycloakTest { return RunOnServerDeployment.create(RunOnServerTest.class); } + @Test + public void runOnServerString() throws IOException { + String string = testingClient.server().fetch(session -> "Hello world!", String.class); + assertEquals("Hello world!", string); + } + @Test public void runOnServerRep() throws IOException { final String realmName = "master"; diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/runonserver/ServerVersion.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/runonserver/ServerVersion.java new file mode 100644 index 0000000000..3e565dc1a6 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/runonserver/ServerVersion.java @@ -0,0 +1,22 @@ +package org.keycloak.testsuite.runonserver; + +import org.keycloak.common.Version; +import org.keycloak.models.utils.ModelToRepresentation; +import org.keycloak.representations.idm.ComponentRepresentation; + +/** + * Created by st on 26.01.17. + */ +public class ServerVersion implements FetchOnServerWrapper { + + @Override + public FetchOnServer getRunOnServer() { + return (FetchOnServer) session -> Version.RESOURCES_VERSION; + } + + @Override + public Class getResultClass() { + return String.class; + } + +} diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/AuthnRequestNameIdFormatTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/AuthnRequestNameIdFormatTest.java index ffa565117e..484b9cc698 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/AuthnRequestNameIdFormatTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/AuthnRequestNameIdFormatTest.java @@ -19,42 +19,21 @@ package org.keycloak.testsuite.saml; import org.keycloak.admin.client.resource.ClientResource; import org.keycloak.admin.client.resource.ClientsResource; import org.keycloak.dom.saml.v2.assertion.NameIDType; -import org.keycloak.dom.saml.v2.protocol.AuthnRequestType; import org.keycloak.dom.saml.v2.protocol.NameIDPolicyType; import org.keycloak.dom.saml.v2.protocol.ResponseType; import org.keycloak.protocol.saml.SamlConfigAttributes; -import org.keycloak.protocol.saml.SamlProtocol; import org.keycloak.representations.idm.ClientRepresentation; -import org.keycloak.representations.idm.RealmRepresentation; -import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.saml.common.constants.JBossSAMLURIConstants; -import org.keycloak.saml.processing.api.saml.v2.request.SAML2Request; import org.keycloak.saml.processing.core.saml.v2.common.SAMLDocumentHolder; -import org.keycloak.services.resources.RealmsResource; -import org.keycloak.testsuite.AbstractAuthTest; -import org.keycloak.testsuite.util.SamlClient; - -import java.io.IOException; +import org.keycloak.testsuite.util.SamlClientBuilder; import java.net.URI; import java.util.List; -import javax.ws.rs.core.Response; -import javax.ws.rs.core.UriBuilder; -import javax.ws.rs.core.UriBuilderException; -import org.apache.http.client.methods.CloseableHttpResponse; -import org.apache.http.client.methods.HttpUriRequest; -import org.apache.http.client.protocol.HttpClientContext; -import org.apache.http.impl.client.CloseableHttpClient; -import org.apache.http.impl.client.HttpClientBuilder; -import org.apache.http.util.EntityUtils; import org.hamcrest.Matcher; import org.junit.Test; -import org.w3c.dom.Document; import static org.hamcrest.Matchers.*; import static org.keycloak.testsuite.util.SamlClient.*; import static org.junit.Assert.assertThat; -import static org.keycloak.testsuite.util.IOUtil.loadRealm; -import static org.keycloak.testsuite.util.Matchers.statusCodeIsHC; /** * @@ -63,12 +42,18 @@ import static org.keycloak.testsuite.util.Matchers.statusCodeIsHC; public class AuthnRequestNameIdFormatTest extends AbstractSamlTest { private void testLoginWithNameIdPolicy(Binding requestBinding, Binding responseBinding, NameIDPolicyType nameIDPolicy, Matcher nameIdMatcher) throws Exception { - AuthnRequestType loginRep = createLoginRequestDocument(SAML_CLIENT_ID_SALES_POST, SAML_ASSERTION_CONSUMER_URL_SALES_POST, REALM_NAME); - loginRep.setProtocolBinding(requestBinding.getBindingUri()); - loginRep.setNameIDPolicy(nameIDPolicy); + SAMLDocumentHolder res = new SamlClientBuilder() + .authnRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST, SAML_ASSERTION_CONSUMER_URL_SALES_POST, requestBinding) + .transformObject(so -> { + so.setProtocolBinding(requestBinding.getBindingUri()); + so.setNameIDPolicy(nameIDPolicy); + return so; + }) + .build() - Document samlRequest = SAML2Request.convert(loginRep); - SAMLDocumentHolder res = login(bburkeUser, getAuthServerSamlEndpoint(REALM_NAME), samlRequest, null, requestBinding, responseBinding); + .login().user(bburkeUser).build() + + .getSamlResponse(responseBinding); assertThat(res.getSamlObject(), notNullValue()); assertThat(res.getSamlObject(), instanceOf(ResponseType.class)); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/BasicSamlTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/BasicSamlTest.java index 78cf93d510..abfd00160f 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/BasicSamlTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/BasicSamlTest.java @@ -12,6 +12,7 @@ import org.keycloak.services.resources.RealmsResource; import org.keycloak.testsuite.util.SamlClient; import org.keycloak.testsuite.util.SamlClient.Binding; import org.keycloak.testsuite.util.SamlClient.RedirectStrategyWithSwitchableFollowRedirect; +import org.keycloak.testsuite.util.SamlClientBuilder; import javax.ws.rs.core.Response; import javax.ws.rs.core.UriBuilder; import org.apache.http.client.methods.CloseableHttpResponse; @@ -25,10 +26,10 @@ import org.w3c.dom.Document; import static org.hamcrest.CoreMatchers.not; import static org.hamcrest.Matchers.containsString; import static org.junit.Assert.assertThat; +import static org.keycloak.testsuite.saml.AbstractSamlTest.REALM_NAME; import static org.keycloak.testsuite.util.IOUtil.documentToString; import static org.keycloak.testsuite.util.IOUtil.setDocElementAttributeValue; import static org.keycloak.testsuite.util.Matchers.statusCodeIsHC; -import static org.keycloak.testsuite.util.SamlClient.login; /** * @author mhajas @@ -38,13 +39,15 @@ public class BasicSamlTest extends AbstractSamlTest { // KEYCLOAK-4160 @Test public void testPropertyValueInAssertion() throws ParsingException, ConfigurationException, ProcessingException { - AuthnRequestType loginRep = createLoginRequestDocument(SAML_CLIENT_ID_SALES_POST, SAML_ASSERTION_CONSUMER_URL_SALES_POST, REALM_NAME); - - Document doc = SAML2Request.convert(loginRep); - - setDocElementAttributeValue(doc, "samlp:AuthnRequest", "ID", "${java.version}" ); - - SAMLDocumentHolder document = login(bburkeUser, getAuthServerSamlEndpoint(REALM_NAME), doc, null, SamlClient.Binding.POST, SamlClient.Binding.POST); + SAMLDocumentHolder document = new SamlClientBuilder() + .authnRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST, SAML_ASSERTION_CONSUMER_URL_SALES_POST, Binding.POST) + .transformDocument(doc -> { + setDocElementAttributeValue(doc, "samlp:AuthnRequest", "ID", "${java.version}" ); + return doc; + }) + .build() + .login().user(bburkeUser).build() + .getSamlResponse(Binding.POST); assertThat(documentToString(document.getSamlDocument()), not(containsString("InResponseTo=\"" + System.getProperty("java.version") + "\""))); } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/ConcurrentAuthnRequestTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/ConcurrentAuthnRequestTest.java index 31cc14dc71..13370acd4d 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/ConcurrentAuthnRequestTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/ConcurrentAuthnRequestTest.java @@ -22,6 +22,7 @@ import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.saml.processing.api.saml.v2.request.SAML2Request; import org.keycloak.testsuite.util.SamlClient; +import org.keycloak.testsuite.util.saml.LoginBuilder; import java.io.IOException; import java.net.URI; import java.util.Collection; @@ -90,7 +91,7 @@ public class ConcurrentAuthnRequestTest extends AbstractSamlTest { String loginPageText = EntityUtils.toString(response.getEntity(), "UTF-8"); response.close(); - HttpUriRequest loginRequest = handleLoginPage(user, loginPageText); + HttpUriRequest loginRequest = LoginBuilder.handleLoginPage(user, loginPageText); strategy.setRedirectable(false); response = client.execute(loginRequest, context); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/IncludeOneTimeUseConditionTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/IncludeOneTimeUseConditionTest.java index cec6e1a6bb..5c9ee3e296 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/IncludeOneTimeUseConditionTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/IncludeOneTimeUseConditionTest.java @@ -23,24 +23,21 @@ import org.keycloak.admin.client.resource.ClientsResource; import org.keycloak.dom.saml.v2.assertion.ConditionAbstractType; import org.keycloak.dom.saml.v2.assertion.ConditionsType; import org.keycloak.dom.saml.v2.assertion.OneTimeUseType; -import org.keycloak.dom.saml.v2.protocol.AuthnRequestType; import org.keycloak.dom.saml.v2.protocol.ResponseType; import org.keycloak.protocol.saml.SamlConfigAttributes; import org.keycloak.representations.idm.ClientRepresentation; -import org.keycloak.saml.common.exceptions.ConfigurationException; -import org.keycloak.saml.common.exceptions.ParsingException; -import org.keycloak.saml.common.exceptions.ProcessingException; -import org.keycloak.saml.processing.api.saml.v2.request.SAML2Request; import org.keycloak.saml.processing.core.saml.v2.common.SAMLDocumentHolder; -import org.keycloak.testsuite.util.SamlClient; -import org.w3c.dom.Document; +import org.keycloak.testsuite.updaters.ClientAttributeUpdater; +import org.keycloak.testsuite.util.SamlClient.Binding; +import org.keycloak.testsuite.util.SamlClientBuilder; +import java.io.Closeable; +import java.io.IOException; import java.util.Collection; import java.util.List; import static org.hamcrest.Matchers.*; import static org.junit.Assert.assertThat; -import static org.keycloak.testsuite.util.SamlClient.login; /** * KEYCLOAK-4360 @@ -60,38 +57,38 @@ public class IncludeOneTimeUseConditionTest extends AbstractSamlTest testOneTimeUseConditionIncluded(Boolean.FALSE); } - private void testOneTimeUseConditionIncluded(Boolean oneTimeUseConditionShouldBeIncluded) throws ProcessingException, ConfigurationException, ParsingException + private void testOneTimeUseConditionIncluded(Boolean oneTimeUseConditionShouldBeIncluded) throws IOException { ClientsResource clients = adminClient.realm(REALM_NAME).clients(); List foundClients = clients.findByClientId(SAML_CLIENT_ID_SALES_POST); assertThat(foundClients, hasSize(1)); ClientResource clientRes = clients.get(foundClients.get(0).getId()); - ClientRepresentation client = clientRes.toRepresentation(); - client.getAttributes().put(SamlConfigAttributes.SAML_ONETIMEUSE_CONDITION, oneTimeUseConditionShouldBeIncluded.toString()); - clientRes.update(client); - AuthnRequestType loginRep = createLoginRequestDocument(SAML_CLIENT_ID_SALES_POST, SAML_ASSERTION_CONSUMER_URL_SALES_POST, REALM_NAME); - loginRep.setProtocolBinding(SamlClient.Binding.POST.getBindingUri()); + try (Closeable c = new ClientAttributeUpdater(clientRes) + .setAttribute(SamlConfigAttributes.SAML_ONETIMEUSE_CONDITION, oneTimeUseConditionShouldBeIncluded.toString()) + .update()) { - Document samlRequest = SAML2Request.convert(loginRep); - SAMLDocumentHolder res = login(bburkeUser, getAuthServerSamlEndpoint(REALM_NAME), samlRequest, null, SamlClient.Binding.POST, - SamlClient.Binding.POST); + SAMLDocumentHolder res = new SamlClientBuilder() + .authnRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST, SAML_ASSERTION_CONSUMER_URL_SALES_POST, Binding.POST).build() + .login().user(bburkeUser).build() + .getSamlResponse(Binding.POST); - assertThat(res.getSamlObject(), notNullValue()); - assertThat(res.getSamlObject(), instanceOf(ResponseType.class)); + assertThat(res.getSamlObject(), notNullValue()); + assertThat(res.getSamlObject(), instanceOf(ResponseType.class)); - ResponseType rt = (ResponseType) res.getSamlObject(); - assertThat(rt.getAssertions(), not(empty())); - final ConditionsType conditionsType = rt.getAssertions().get(0).getAssertion().getConditions(); - assertThat(conditionsType, notNullValue()); - assertThat(conditionsType.getConditions(), not(empty())); + ResponseType rt = (ResponseType) res.getSamlObject(); + assertThat(rt.getAssertions(), not(empty())); + final ConditionsType conditionsType = rt.getAssertions().get(0).getAssertion().getConditions(); + assertThat(conditionsType, notNullValue()); + assertThat(conditionsType.getConditions(), not(empty())); - final List conditions = conditionsType.getConditions(); + final List conditions = conditionsType.getConditions(); - final Collection oneTimeUseConditions = Collections2.filter(conditions, input -> input instanceof OneTimeUseType); + final Collection oneTimeUseConditions = Collections2.filter(conditions, input -> input instanceof OneTimeUseType); - final boolean oneTimeUseConditionAdded = !oneTimeUseConditions.isEmpty(); - assertThat(oneTimeUseConditionAdded, is(oneTimeUseConditionShouldBeIncluded)); + final boolean oneTimeUseConditionAdded = !oneTimeUseConditions.isEmpty(); + assertThat(oneTimeUseConditionAdded, is(oneTimeUseConditionShouldBeIncluded)); + } } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/LogoutTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/LogoutTest.java index 7870ebaa18..eb0888b886 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/LogoutTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/LogoutTest.java @@ -16,34 +16,27 @@ */ package org.keycloak.testsuite.saml; +import org.keycloak.dom.saml.v2.SAML2Object; import org.keycloak.dom.saml.v2.assertion.AssertionType; import org.keycloak.dom.saml.v2.assertion.AuthnStatementType; import org.keycloak.dom.saml.v2.assertion.NameIDType; -import org.keycloak.dom.saml.v2.protocol.AuthnRequestType; import org.keycloak.dom.saml.v2.protocol.LogoutRequestType; import org.keycloak.dom.saml.v2.protocol.ResponseType; +import org.keycloak.dom.saml.v2.protocol.StatusResponseType; import org.keycloak.protocol.saml.SamlProtocol; import org.keycloak.representations.idm.ClientRepresentation; -import org.keycloak.saml.SAML2LogoutRequestBuilder; import org.keycloak.saml.SAML2LogoutResponseBuilder; import org.keycloak.saml.common.constants.JBossSAMLURIConstants; -import org.keycloak.saml.common.exceptions.ConfigurationException; -import org.keycloak.saml.common.exceptions.ParsingException; -import org.keycloak.saml.common.exceptions.ProcessingException; -import org.keycloak.saml.processing.api.saml.v2.request.SAML2Request; +import org.keycloak.saml.processing.core.parsers.saml.SAMLParser; import org.keycloak.saml.processing.core.saml.v2.common.SAMLDocumentHolder; import org.keycloak.testsuite.util.ClientBuilder; -import org.keycloak.testsuite.util.Matchers; -import org.keycloak.testsuite.util.SamlClient; -import javax.ws.rs.core.Response; -import javax.ws.rs.core.UriBuilderException; -import org.apache.http.client.methods.CloseableHttpResponse; -import org.apache.http.client.methods.HttpUriRequest; -import org.apache.http.client.protocol.HttpClientContext; +import org.keycloak.testsuite.util.SamlClientBuilder; +import java.util.concurrent.atomic.AtomicReference; +import javax.xml.transform.dom.DOMSource; import org.junit.Before; import org.junit.Test; -import org.w3c.dom.Document; import static org.hamcrest.CoreMatchers.instanceOf; +import static org.hamcrest.Matchers.is; import static org.junit.Assert.assertThat; import static org.keycloak.testsuite.util.Matchers.*; import static org.keycloak.testsuite.util.SamlClient.Binding.*; @@ -57,7 +50,8 @@ public class LogoutTest extends AbstractSamlTest { private ClientRepresentation salesRep; private ClientRepresentation sales2Rep; - private SamlClient samlClient; + private final AtomicReference nameIdRef = new AtomicReference<>(); + private final AtomicReference sessionIndexRef = new AtomicReference<>(); @Before public void setup() { @@ -71,7 +65,8 @@ public class LogoutTest extends AbstractSamlTest { .attribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_POST_ATTRIBUTE, "http://url") .build()); - samlClient = new SamlClient(getAuthServerSamlEndpoint(REALM_NAME)); + nameIdRef.set(null); + sessionIndexRef.set(null); } @Override @@ -79,49 +74,35 @@ public class LogoutTest extends AbstractSamlTest { return true; } - private Document prepareLogoutFromSalesAfterLoggingIntoTwoApps() throws ParsingException, IllegalArgumentException, UriBuilderException, ConfigurationException, ProcessingException { - AuthnRequestType loginRep = createLoginRequestDocument(SAML_CLIENT_ID_SALES_POST, SAML_ASSERTION_CONSUMER_URL_SALES_POST, REALM_NAME); - Document doc = SAML2Request.convert(loginRep); - SAMLDocumentHolder resp = samlClient.login(bburkeUser, doc, null, POST, POST, false, true); - assertThat(resp.getSamlObject(), isSamlResponse(JBossSAMLURIConstants.STATUS_SUCCESS)); - ResponseType loginResp1 = (ResponseType) resp.getSamlObject(); + private SamlClientBuilder prepareLogIntoTwoApps() { + return new SamlClientBuilder() + .authnRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST, SAML_ASSERTION_CONSUMER_URL_SALES_POST, POST).build() + .login().user(bburkeUser).build() + .processSamlResponse(POST).transformObject(so -> { + assertThat(so, isSamlResponse(JBossSAMLURIConstants.STATUS_SUCCESS)); + ResponseType loginResp1 = (ResponseType) so; + final AssertionType firstAssertion = loginResp1.getAssertions().get(0).getAssertion(); + assertThat(firstAssertion, org.hamcrest.Matchers.notNullValue()); + assertThat(firstAssertion.getSubject().getSubType().getBaseID(), instanceOf(NameIDType.class)); - loginRep = createLoginRequestDocument(SAML_CLIENT_ID_SALES_POST2, SAML_ASSERTION_CONSUMER_URL_SALES_POST2, REALM_NAME); - doc = SAML2Request.convert(loginRep); - resp = samlClient.subsequentLoginViaSSO(doc, null, POST, POST); - assertThat(resp.getSamlObject(), isSamlResponse(JBossSAMLURIConstants.STATUS_SUCCESS)); - ResponseType loginResp2 = (ResponseType) resp.getSamlObject(); + NameIDType nameId = (NameIDType) firstAssertion.getSubject().getSubType().getBaseID(); + AuthnStatementType firstAssertionStatement = (AuthnStatementType) firstAssertion.getStatements().iterator().next(); - AssertionType firstAssertion = loginResp1.getAssertions().get(0).getAssertion(); - assertThat(firstAssertion.getSubject().getSubType().getBaseID(), instanceOf(NameIDType.class)); - NameIDType nameId = (NameIDType) firstAssertion.getSubject().getSubType().getBaseID(); - AuthnStatementType firstAssertionStatement = (AuthnStatementType) firstAssertion.getStatements().iterator().next(); + nameIdRef.set(nameId); + sessionIndexRef.set(firstAssertionStatement.getSessionIndex()); + return null; // Do not follow the redirect to the app from the returned response + }).build() - return new SAML2LogoutRequestBuilder() - .destination(getAuthServerSamlEndpoint(REALM_NAME).toString()) - .issuer(SAML_CLIENT_ID_SALES_POST) - .sessionIndex(firstAssertionStatement.getSessionIndex()) - .userPrincipal(nameId.getValue(), nameId.getFormat().toString()) - .buildDocument(); + .authnRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST2, SAML_ASSERTION_CONSUMER_URL_SALES_POST2, POST).build() + .login().sso(true).build() // This is a formal step + .processSamlResponse(POST).transformObject(so -> { + assertThat(so, isSamlResponse(JBossSAMLURIConstants.STATUS_SUCCESS)); + return null; // Do not follow the redirect to the app from the returned response + }).build(); } @Test - public void testLogoutInSameBrowser() throws ParsingException, ConfigurationException, ProcessingException { - adminClient.realm(REALM_NAME) - .clients().get(sales2Rep.getId()) - .update(ClientBuilder.edit(sales2Rep) - .frontchannelLogout(false) - .attribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_POST_ATTRIBUTE, "") - .removeAttribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_REDIRECT_ATTRIBUTE) - .build()); - - Document logoutDoc = prepareLogoutFromSalesAfterLoggingIntoTwoApps(); - - samlClient.logout(logoutDoc, null, POST, POST); - } - - @Test - public void testLogoutDifferentBrowser() throws ParsingException, ConfigurationException, ProcessingException { + public void testLogoutDifferentBrowser() { // This is in fact the same as admin logging out a session from admin console. // This always succeeds as it is essentially the same as backend logout which // does not report errors to client but only to the server log @@ -130,135 +111,155 @@ public class LogoutTest extends AbstractSamlTest { .update(ClientBuilder.edit(sales2Rep) .frontchannelLogout(false) .attribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_POST_ATTRIBUTE, "") - .removeAttribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_REDIRECT_ATTRIBUTE) + .removeAttribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_REDIRECT_ATTRIBUTE) .build()); - Document logoutDoc = prepareLogoutFromSalesAfterLoggingIntoTwoApps(); + SAMLDocumentHolder samlResponse = prepareLogIntoTwoApps() + .clearCookies() - samlClient.execute((client, context, strategy) -> { - HttpUriRequest post = POST.createSamlUnsignedRequest(getAuthServerSamlEndpoint(REALM_NAME), null, logoutDoc); - CloseableHttpResponse response = client.execute(post, HttpClientContext.create()); - assertThat(response, statusCodeIsHC(Response.Status.OK)); - return response; - }); + .logoutRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST, POST) + .nameId(nameIdRef::get) + .sessionIndex(sessionIndexRef::get) + .build() + + .getSamlResponse(POST); + + assertThat(samlResponse.getSamlObject(), isSamlStatusResponse(JBossSAMLURIConstants.STATUS_SUCCESS)); } @Test - public void testFrontchannelLogoutInSameBrowser() throws ParsingException, ConfigurationException, ProcessingException { + public void testFrontchannelLogoutInSameBrowser() { adminClient.realm(REALM_NAME) .clients().get(sales2Rep.getId()) .update(ClientBuilder.edit(sales2Rep) .frontchannelLogout(true) .attribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_POST_ATTRIBUTE, "") - .removeAttribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_REDIRECT_ATTRIBUTE) .build()); - Document logoutDoc = prepareLogoutFromSalesAfterLoggingIntoTwoApps(); + SAMLDocumentHolder samlResponse = prepareLogIntoTwoApps() + .logoutRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST, POST) + .nameId(nameIdRef::get) + .sessionIndex(sessionIndexRef::get) + .build() - samlClient.execute((client, context, strategy) -> { - HttpUriRequest post = POST.createSamlUnsignedRequest(getAuthServerSamlEndpoint(REALM_NAME), null, logoutDoc); - CloseableHttpResponse response = client.execute(post, context); - assertThat(response, statusCodeIsHC(Response.Status.OK)); - return response; - }); + .getSamlResponse(POST); + + assertThat(samlResponse.getSamlObject(), isSamlStatusResponse(JBossSAMLURIConstants.STATUS_SUCCESS)); } @Test - public void testFrontchannelLogoutNoLogoutServiceUrlSetInSameBrowser() throws ParsingException, ConfigurationException, ProcessingException { - adminClient.realm(REALM_NAME) - .clients().get(sales2Rep.getId()) - .update(ClientBuilder.edit(sales2Rep) - .frontchannelLogout(true) - .removeAttribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_POST_ATTRIBUTE) - .removeAttribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_REDIRECT_ATTRIBUTE) - .build()); - - Document logoutDoc = prepareLogoutFromSalesAfterLoggingIntoTwoApps(); - - samlClient.execute((client, context, strategy) -> { - HttpUriRequest post = POST.createSamlUnsignedRequest(getAuthServerSamlEndpoint(REALM_NAME), null, logoutDoc); - CloseableHttpResponse response = client.execute(post, context); - assertThat(response, statusCodeIsHC(Response.Status.OK)); - return response; - }); - } - - @Test - public void testFrontchannelLogoutDifferentBrowser() throws ParsingException, ConfigurationException, ProcessingException { + public void testFrontchannelLogoutNoLogoutServiceUrlSetInSameBrowser() { adminClient.realm(REALM_NAME) .clients().get(sales2Rep.getId()) .update(ClientBuilder.edit(sales2Rep) .frontchannelLogout(true) .attribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_POST_ATTRIBUTE, "") - .removeAttribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_REDIRECT_ATTRIBUTE) + .attribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_REDIRECT_ATTRIBUTE, "") .build()); - Document logoutDoc = prepareLogoutFromSalesAfterLoggingIntoTwoApps(); + SAMLDocumentHolder samlResponse = prepareLogIntoTwoApps() + .logoutRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST, POST) + .nameId(nameIdRef::get) + .sessionIndex(sessionIndexRef::get) + .build() - samlClient.execute((client, context, strategy) -> { - HttpUriRequest post = POST.createSamlUnsignedRequest(getAuthServerSamlEndpoint(REALM_NAME), null, logoutDoc); - CloseableHttpResponse response = client.execute(post, HttpClientContext.create()); - assertThat(response, statusCodeIsHC(Response.Status.OK)); - return response; - }); + .getSamlResponse(POST); + + assertThat(samlResponse.getSamlObject(), isSamlStatusResponse(JBossSAMLURIConstants.STATUS_SUCCESS)); } @Test - public void testFrontchannelLogoutWithRedirectUrlDifferentBrowser() throws ParsingException, ConfigurationException, ProcessingException { + public void testFrontchannelLogoutDifferentBrowser() { adminClient.realm(REALM_NAME) .clients().get(sales2Rep.getId()) .update(ClientBuilder.edit(sales2Rep) .frontchannelLogout(true) - .removeAttribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_POST_ATTRIBUTE) + .attribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_POST_ATTRIBUTE, "") + .build()); + + SAMLDocumentHolder samlResponse = prepareLogIntoTwoApps() + .clearCookies() + + .logoutRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST, POST) + .nameId(nameIdRef::get) + .sessionIndex(sessionIndexRef::get) + .build() + + .getSamlResponse(POST); + + assertThat(samlResponse.getSamlObject(), isSamlStatusResponse(JBossSAMLURIConstants.STATUS_SUCCESS)); + } + + @Test + public void testFrontchannelLogoutWithRedirectUrlDifferentBrowser() { + adminClient.realm(REALM_NAME) + .clients().get(salesRep.getId()) + .update(ClientBuilder.edit(salesRep) + .frontchannelLogout(true) + .attribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_POST_ATTRIBUTE, "") .attribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_REDIRECT_ATTRIBUTE, "http://url") .build()); - Document logoutDoc = prepareLogoutFromSalesAfterLoggingIntoTwoApps(); + adminClient.realm(REALM_NAME) + .clients().get(sales2Rep.getId()) + .update(ClientBuilder.edit(sales2Rep) + .frontchannelLogout(true) + .attribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_POST_ATTRIBUTE, "") + .attribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_REDIRECT_ATTRIBUTE, "") + .build()); - samlClient.execute((client, context, strategy) -> { - HttpUriRequest post = POST.createSamlUnsignedRequest(getAuthServerSamlEndpoint(REALM_NAME), null, logoutDoc); - CloseableHttpResponse response = client.execute(post, HttpClientContext.create()); - assertThat(response, statusCodeIsHC(Response.Status.OK)); - return response; - }); + SAMLDocumentHolder samlResponse = prepareLogIntoTwoApps() + .clearCookies() + + .logoutRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST, REDIRECT) + .nameId(nameIdRef::get) + .sessionIndex(sessionIndexRef::get) + .build() + + .getSamlResponse(REDIRECT); + + assertThat(samlResponse.getSamlObject(), isSamlStatusResponse(JBossSAMLURIConstants.STATUS_SUCCESS)); } @Test - public void testLogoutWithPostBindingUnsetRedirectBindingSet() throws ParsingException, ConfigurationException, ProcessingException { + public void testLogoutWithPostBindingUnsetRedirectBindingSet() { // https://issues.jboss.org/browse/KEYCLOAK-4779 adminClient.realm(REALM_NAME) .clients().get(sales2Rep.getId()) .update(ClientBuilder.edit(sales2Rep) .frontchannelLogout(true) .attribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_POST_ATTRIBUTE, "") - .attribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_REDIRECT_ATTRIBUTE, "http://url") + .attribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_REDIRECT_ATTRIBUTE, "http://url-to-sales-2") .build()); - Document logoutDoc = prepareLogoutFromSalesAfterLoggingIntoTwoApps(); + SAMLDocumentHolder samlResponse = prepareLogIntoTwoApps() + .logoutRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST, POST) + .nameId(nameIdRef::get) + .sessionIndex(sessionIndexRef::get) + .build() - SAMLDocumentHolder resp = samlClient.getSamlResponse(REDIRECT, (client, context, strategy) -> { - strategy.setRedirectable(false); - HttpUriRequest post = POST.createSamlUnsignedRequest(getAuthServerSamlEndpoint(REALM_NAME), null, logoutDoc); - return client.execute(post, context); - }); + .processSamlResponse(REDIRECT) + .transformDocument(doc -> { + // Expect logout request for sales-post2 + SAML2Object so = (SAML2Object) new SAMLParser().parse(new DOMSource(doc)); + assertThat(so, isSamlLogoutRequest("http://url-to-sales-2")); - // Expect logout request for sales-post2 - assertThat(resp.getSamlObject(), isSamlLogoutRequest("http://url")); - Document logoutRespDoc = new SAML2LogoutResponseBuilder() - .destination(getAuthServerSamlEndpoint(REALM_NAME).toString()) - .issuer(SAML_CLIENT_ID_SALES_POST2) - .logoutRequestID(((LogoutRequestType) resp.getSamlObject()).getID()) - .buildDocument(); + // Emulate successful logout response from sales-post2 logout + return new SAML2LogoutResponseBuilder() + .destination(getAuthServerSamlEndpoint(REALM_NAME).toString()) + .issuer(SAML_CLIENT_ID_SALES_POST2) + .logoutRequestID(((LogoutRequestType) so).getID()) + .buildDocument(); + }) + .targetAttributeSamlResponse() + .targetUri(getAuthServerSamlEndpoint(REALM_NAME)) + .build() - // Emulate successful logout response from sales-post2 logout - resp = samlClient.getSamlResponse(POST, (client, context, strategy) -> { - strategy.setRedirectable(false); - HttpUriRequest post = POST.createSamlUnsignedResponse(getAuthServerSamlEndpoint(REALM_NAME), null, logoutRespDoc); - return client.execute(post, context); - }); + .getSamlResponse(POST); // Expect final successful logout response from auth server signalling final successful logout - assertThat(resp.getSamlObject(), isSamlStatusResponse(JBossSAMLURIConstants.STATUS_SUCCESS)); + assertThat(samlResponse.getSamlObject(), isSamlStatusResponse(JBossSAMLURIConstants.STATUS_SUCCESS)); + assertThat(((StatusResponseType) samlResponse.getSamlObject()).getDestination(), is("http://url")); } } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/SamlConsentTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/SamlConsentTest.java index 3fcf0c36d6..bd30eea540 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/SamlConsentTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/SamlConsentTest.java @@ -9,16 +9,15 @@ import org.keycloak.saml.common.exceptions.ProcessingException; import org.keycloak.saml.processing.core.saml.v2.common.SAMLDocumentHolder; import org.keycloak.testsuite.util.ClientBuilder; import org.keycloak.testsuite.util.IOUtil; -import org.keycloak.testsuite.util.SamlClient; -import java.net.URI; +import org.keycloak.testsuite.util.SamlClient.Binding; +import org.keycloak.testsuite.util.SamlClientBuilder; import java.util.List; import static org.hamcrest.CoreMatchers.containsString; import static org.hamcrest.CoreMatchers.not; import static org.junit.Assert.assertThat; import static org.keycloak.testsuite.util.IOUtil.loadRealm; -import static org.keycloak.testsuite.util.SamlClient.idpInitiatedLoginWithRequiredConsent; /** * @author mhajas @@ -48,13 +47,17 @@ public class SamlConsentTest extends AbstractSamlTest { .build()); log.debug("Log in using idp initiated login"); - String idpInitiatedLogin = getAuthServerRoot() + "realms/" + REALM_NAME + "/protocol/saml/clients/sales-post-enc"; - SAMLDocumentHolder documentHolder = idpInitiatedLoginWithRequiredConsent(bburkeUser, URI.create(idpInitiatedLogin), SamlClient.Binding.POST, false); + SAMLDocumentHolder documentHolder = new SamlClientBuilder() + .idpInitiatedLogin(getAuthServerSamlEndpoint(REALM_NAME), "sales-post-enc").build() + .login().user(bburkeUser).build() + .consentRequired().approveConsent(false).build() + .getSamlResponse(Binding.POST); - assertThat(IOUtil.documentToString(documentHolder.getSamlDocument()), containsString("Marek Posolda + */ +public class LastSessionRefreshUnitTest extends AbstractKeycloakTest { + + @Deployment + public static WebArchive deploy() { + return RunOnServerDeployment.create(UserResource.class) + .addPackages(true, "org.keycloak.testsuite"); + } + + + @Override + public void addTestRealms(List testRealms) { + + } + + + @Test + public void testLastSessionRefreshCounters() { + testingClient.server().run(new LastSessionRefreshServerCounterTest()); + } + + public static class LastSessionRefreshServerCounterTest extends LastSessionRefreshServerTest { + + + @Override + public void run(KeycloakSession session) { + LastSessionRefreshStore customStore = createStoreInstance(session, 1000000, 1000); + System.out.println("sss"); + + int lastSessionRefresh = Time.currentTime(); + + // Add 8 items. No message + for (int i=0 ; i<8 ; i++){ + customStore.putLastSessionRefresh(session, "session-" + i, "master", lastSessionRefresh); + } + Assert.assertEquals(0, counter.get()); + + // Add 2 other items. Message sent now due the maxCount is 10 + for (int i=8 ; i<10 ; i++){ + customStore.putLastSessionRefresh(session, "session-" + i, "master", lastSessionRefresh); + } + Assert.assertEquals(1, counter.get()); + + // Add 5 items. No additional message + for (int i=10 ; i<15 ; i++){ + customStore.putLastSessionRefresh(session, "session-" + i, "master", lastSessionRefresh); + } + Assert.assertEquals(1, counter.get()); + + // Add 20 items. 2 additional messages + for (int i=15 ; i<35 ; i++){ + customStore.putLastSessionRefresh(session, "session-" + i, "master", lastSessionRefresh); + } + Assert.assertEquals(3, counter.get()); + + } + + } + + + @Test + public void testLastSessionRefreshIntervals() { + testingClient.server().run(new LastSessionRefreshServerIntervalsTest()); + } + + public static class LastSessionRefreshServerIntervalsTest extends LastSessionRefreshServerTest { + + @Override + public void run(KeycloakSession session) { + // Long timer interval. No message due the timer wasn't executed + LastSessionRefreshStore customStore1 = createStoreInstance(session, 100000, 10); + Time.setOffset(100); + + try { + Thread.sleep(50); + } catch (InterruptedException ie) { + throw new RuntimeException(); + } + Assert.assertEquals(0, counter.get()); + + // Short timer interval 10 ms. 1 message due the interval is executed and lastRun was in the past due to Time.setOffset + LastSessionRefreshStore customStore2 = createStoreInstance(session, 10, 10); + Time.setOffset(200); + + Retry.execute(() -> { + Assert.assertEquals(1, counter.get()); + }, 100, 10); + + Assert.assertEquals(1, counter.get()); + + // Another sleep won't send message. lastRun was updated + try { + Thread.sleep(50); + } catch (InterruptedException ie) { + throw new RuntimeException(); + } + Assert.assertEquals(1, counter.get()); + + + Time.setOffset(0); + } + + } + + + public static abstract class LastSessionRefreshServerTest implements RunOnServer { + + AtomicInteger counter = new AtomicInteger(); + + LastSessionRefreshStore createStoreInstance(KeycloakSession session, long timerIntervalMs, int maxIntervalBetweenMessagesSeconds) { + LastSessionRefreshStoreFactory factory = new LastSessionRefreshStoreFactory() { + + @Override + protected LastSessionRefreshStore createStoreInstance(int maxIntervalBetweenMessagesSeconds, int maxCount, String eventKey) { + return new LastSessionRefreshStore(maxIntervalBetweenMessagesSeconds, maxCount, eventKey) { + + @Override + protected void sendMessage(KeycloakSession kcSession, Map refreshesToSend) { + counter.incrementAndGet(); + } + + }; + } + + }; + + Cache cache = session.getProvider(InfinispanConnectionProvider.class).getCache(InfinispanConnectionProvider.SESSION_CACHE_NAME); + return factory.createAndInit(session, cache, timerIntervalMs, maxIntervalBetweenMessagesSeconds, 10, false); + } + + } + + +} diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/SamlClient.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/SamlClient.java deleted file mode 100644 index 8c20b26af4..0000000000 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/SamlClient.java +++ /dev/null @@ -1,603 +0,0 @@ -/* - * Copyright 2017 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.util; - -import org.apache.http.NameValuePair; -import org.apache.http.client.entity.UrlEncodedFormEntity; -import org.apache.http.client.methods.CloseableHttpResponse; -import org.apache.http.client.methods.HttpGet; -import org.apache.http.client.methods.HttpPost; -import org.apache.http.client.methods.HttpUriRequest; -import org.apache.http.client.protocol.HttpClientContext; -import org.apache.http.client.utils.URLEncodedUtils; -import org.apache.http.impl.client.CloseableHttpClient; -import org.apache.http.impl.client.HttpClientBuilder; -import org.apache.http.impl.client.LaxRedirectStrategy; -import org.apache.http.message.BasicNameValuePair; -import org.apache.http.util.EntityUtils; -import org.jsoup.Jsoup; -import org.jsoup.nodes.Element; -import org.jsoup.select.Elements; -import org.keycloak.common.util.KeyUtils; -import org.keycloak.dom.saml.v2.protocol.AuthnRequestType; -import org.keycloak.representations.idm.UserRepresentation; -import org.keycloak.saml.BaseSAML2BindingBuilder; -import org.keycloak.saml.SAMLRequestParser; -import org.keycloak.saml.SignatureAlgorithm; -import org.keycloak.saml.common.constants.GeneralConstants; -import org.keycloak.saml.common.constants.JBossSAMLURIConstants; -import org.keycloak.saml.common.exceptions.ConfigurationException; -import org.keycloak.saml.common.exceptions.ProcessingException; -import org.keycloak.saml.processing.api.saml.v2.request.SAML2Request; -import org.keycloak.saml.processing.core.saml.v2.common.SAMLDocumentHolder; -import org.w3c.dom.Document; - -import javax.ws.rs.core.Response; -import javax.ws.rs.core.UriBuilder; -import java.io.IOException; -import java.io.UnsupportedEncodingException; -import java.net.URI; -import java.security.PrivateKey; -import java.security.PublicKey; -import java.util.LinkedList; -import java.util.List; -import java.util.Objects; -import java.util.UUID; - -import org.apache.http.protocol.HttpContext; -import static org.hamcrest.Matchers.*; -import static org.junit.Assert.assertThat; -import static org.keycloak.testsuite.admin.Users.getPasswordOf; -import static org.keycloak.testsuite.arquillian.AuthServerTestEnricher.getAuthServerContextRoot; -import static org.keycloak.testsuite.util.Matchers.statusCodeIsHC; - -/** - * @author hmlnarik - */ -public class SamlClient { - - /** - * SAML bindings and related HttpClient methods. - */ - public enum Binding { - POST { - @Override - public SAMLDocumentHolder extractResponse(CloseableHttpResponse response) throws IOException { - assertThat(response, statusCodeIsHC(Response.Status.OK)); - String responsePage = EntityUtils.toString(response.getEntity(), "UTF-8"); - response.close(); - return extractSamlResponseFromForm(responsePage); - } - - @Override - public HttpPost createSamlUnsignedRequest(URI samlEndpoint, String relayState, Document samlRequest) { - return createSamlPostMessage(samlEndpoint, relayState, samlRequest, GeneralConstants.SAML_REQUEST_KEY, null, null); - } - - @Override - public HttpPost createSamlUnsignedResponse(URI samlEndpoint, String relayState, Document samlRequest) { - return createSamlPostMessage(samlEndpoint, relayState, samlRequest, GeneralConstants.SAML_RESPONSE_KEY, null, null); - } - - @Override - public HttpPost createSamlSignedRequest(URI samlEndpoint, String relayState, Document samlRequest, String realmPrivateKey, String realmPublicKey) { - return createSamlPostMessage(samlEndpoint, relayState, samlRequest, GeneralConstants.SAML_REQUEST_KEY, realmPrivateKey, realmPublicKey); - } - - private HttpPost createSamlPostMessage(URI samlEndpoint, String relayState, Document samlRequest, String messageType, String privateKeyStr, String publicKeyStr) { - HttpPost post = new HttpPost(samlEndpoint); - - List parameters = new LinkedList<>(); - - - try { - BaseSAML2BindingBuilder binding = new BaseSAML2BindingBuilder(); - - if (privateKeyStr != null && publicKeyStr != null) { - PrivateKey privateKey = org.keycloak.testsuite.util.KeyUtils.privateKeyFromString(privateKeyStr); - PublicKey publicKey = org.keycloak.testsuite.util.KeyUtils.publicKeyFromString(publicKeyStr); - binding - .signatureAlgorithm(SignatureAlgorithm.RSA_SHA256) - .signWith(KeyUtils.createKeyId(privateKey), privateKey, publicKey) - .signDocument(); - } - - parameters.add( - new BasicNameValuePair(messageType, - binding - .postBinding(samlRequest) - .encoded()) - ); - } catch (IOException | ConfigurationException | ProcessingException ex) { - throw new RuntimeException(ex); - } - - if (relayState != null) { - parameters.add(new BasicNameValuePair(GeneralConstants.RELAY_STATE, relayState)); - } - - UrlEncodedFormEntity formEntity; - - try { - formEntity = new UrlEncodedFormEntity(parameters, "UTF-8"); - } catch (UnsupportedEncodingException e) { - throw new RuntimeException(e); - } - - post.setEntity(formEntity); - - return post; - } - - @Override - public URI getBindingUri() { - return URI.create(JBossSAMLURIConstants.SAML_HTTP_POST_BINDING.get()); - } - }, - - REDIRECT { - @Override - public SAMLDocumentHolder extractResponse(CloseableHttpResponse response) throws IOException { - assertThat(response, statusCodeIsHC(Response.Status.FOUND)); - String location = response.getFirstHeader("Location").getValue(); - response.close(); - return extractSamlResponseFromRedirect(location); - } - - @Override - public HttpGet createSamlUnsignedRequest(URI samlEndpoint, String relayState, Document samlRequest) { - try { - URI requestURI = new BaseSAML2BindingBuilder() - .relayState(relayState) - .redirectBinding(samlRequest) - .requestURI(samlEndpoint.toString()); - return new HttpGet(requestURI); - } catch (ProcessingException | ConfigurationException | IOException ex) { - throw new RuntimeException(ex); - } - } - - @Override - public URI getBindingUri() { - return URI.create(JBossSAMLURIConstants.SAML_HTTP_REDIRECT_BINDING.get()); - } - - @Override - public HttpUriRequest createSamlUnsignedResponse(URI samlEndpoint, String relayState, Document samlRequest) { - return null; - } - - @Override - public HttpUriRequest createSamlSignedRequest(URI samlEndpoint, String relayState, Document samlRequest, String realmPrivateKey, String realmPublicKey) { - return null; - } - }; - - public abstract SAMLDocumentHolder extractResponse(CloseableHttpResponse response) throws IOException; - - public abstract HttpUriRequest createSamlUnsignedRequest(URI samlEndpoint, String relayState, Document samlRequest); - - public abstract HttpUriRequest createSamlSignedRequest(URI samlEndpoint, String relayState, Document samlRequest, String realmPrivateKey, String realmPublicKey); - - public abstract URI getBindingUri(); - - public abstract HttpUriRequest createSamlUnsignedResponse(URI samlEndpoint, String relayState, Document samlRequest); - } - - public static class RedirectStrategyWithSwitchableFollowRedirect extends LaxRedirectStrategy { - - public boolean redirectable = true; - - @Override - protected boolean isRedirectable(String method) { - return redirectable && super.isRedirectable(method); - } - - public void setRedirectable(boolean redirectable) { - this.redirectable = redirectable; - } - } - - /** - * Extracts and parses value of SAMLResponse input field of a form present in the given page. - * - * @param responsePage HTML code of the page - * @return - */ - public static SAMLDocumentHolder extractSamlResponseFromForm(String responsePage) { - org.jsoup.nodes.Document theResponsePage = Jsoup.parse(responsePage); - Elements samlResponses = theResponsePage.select("input[name=SAMLResponse]"); - Elements samlRequests = theResponsePage.select("input[name=SAMLRequest]"); - int size = samlResponses.size() + samlRequests.size(); - assertThat("Checking uniqueness of SAMLResponse/SAMLRequest input field in the page", size, is(1)); - - Element respElement = samlResponses.isEmpty() ? samlRequests.first() : samlResponses.first(); - - return SAMLRequestParser.parseResponsePostBinding(respElement.val()); - } - - /** - * Extracts and parses value of SAMLResponse query parameter from the given URI. - * - * @param responseUri - * @return - */ - public static SAMLDocumentHolder extractSamlResponseFromRedirect(String responseUri) { - List params = URLEncodedUtils.parse(URI.create(responseUri), "UTF-8"); - - String samlDoc = null; - for (NameValuePair param : params) { - if ("SAMLResponse".equals(param.getName()) || "SAMLRequest".equals(param.getName())) { - assertThat("Only one SAMLRequest/SAMLResponse check", samlDoc, nullValue()); - samlDoc = param.getValue(); - } - } - - return SAMLRequestParser.parseResponseRedirectBinding(samlDoc); - } - - /** - * Prepares a GET/POST request for logging the given user into the given login page. The login page is expected - * to have at least input fields with id "username" and "password". - * - * @param user - * @param loginPage - * @return - */ - public static HttpUriRequest handleLoginPage(UserRepresentation user, String loginPage) { - String username = user.getUsername(); - String password = getPasswordOf(user); - org.jsoup.nodes.Document theLoginPage = Jsoup.parse(loginPage); - - List parameters = new LinkedList<>(); - for (Element form : theLoginPage.getElementsByTag("form")) { - String method = form.attr("method"); - String action = form.attr("action"); - boolean isPost = method != null && "post".equalsIgnoreCase(method); - - for (Element input : form.getElementsByTag("input")) { - if (Objects.equals(input.id(), "username")) { - parameters.add(new BasicNameValuePair(input.attr("name"), username)); - } else if (Objects.equals(input.id(), "password")) { - parameters.add(new BasicNameValuePair(input.attr("name"), password)); - } else { - parameters.add(new BasicNameValuePair(input.attr("name"), input.val())); - } - } - - if (isPost) { - HttpPost res = new HttpPost(action); - - UrlEncodedFormEntity formEntity; - try { - formEntity = new UrlEncodedFormEntity(parameters, "UTF-8"); - } catch (UnsupportedEncodingException e) { - throw new RuntimeException(e); - } - res.setEntity(formEntity); - - return res; - } else { - UriBuilder b = UriBuilder.fromPath(action); - for (NameValuePair parameter : parameters) { - b.queryParam(parameter.getName(), parameter.getValue()); - } - return new HttpGet(b.build()); - } - } - - throw new IllegalArgumentException("Invalid login form: " + loginPage); - } - - /** - * Prepares a GET/POST request for consent granting . The consent page is expected - * to have at least input fields with id "kc-login" and "kc-cancel". - * - * @param consentPage - * @param consent - * @return - */ - public static HttpUriRequest handleConsentPage(String consentPage, boolean consent) { - org.jsoup.nodes.Document theLoginPage = Jsoup.parse(consentPage); - - List parameters = new LinkedList<>(); - for (Element form : theLoginPage.getElementsByTag("form")) { - String method = form.attr("method"); - String action = form.attr("action"); - boolean isPost = method != null && "post".equalsIgnoreCase(method); - - for (Element input : form.getElementsByTag("input")) { - if (Objects.equals(input.id(), "kc-login")) { - if (consent) - parameters.add(new BasicNameValuePair(input.attr("name"), input.attr("value"))); - } else if (Objects.equals(input.id(), "kc-cancel")) { - if (!consent) - parameters.add(new BasicNameValuePair(input.attr("name"), input.attr("value"))); - } else { - parameters.add(new BasicNameValuePair(input.attr("name"), input.val())); - } - } - - if (isPost) { - HttpPost res = new HttpPost(getAuthServerContextRoot() + action); - - UrlEncodedFormEntity formEntity; - try { - formEntity = new UrlEncodedFormEntity(parameters, "UTF-8"); - } catch (UnsupportedEncodingException e) { - throw new RuntimeException(e); - } - res.setEntity(formEntity); - - return res; - } else { - UriBuilder b = UriBuilder.fromPath(action); - for (NameValuePair parameter : parameters) { - b.queryParam(parameter.getName(), parameter.getValue()); - } - return new HttpGet(b.build()); - } - } - - throw new IllegalArgumentException("Invalid consent page: " + consentPage); - } - - /** - * Creates a SAML login request document with the given parameters. See SAML <AuthnRequest> description for more details. - * - * @param issuer - * @param assertionConsumerURL - * @param destination - * @return - */ - public static AuthnRequestType createLoginRequestDocument(String issuer, String assertionConsumerURL, URI destination) { - try { - SAML2Request samlReq = new SAML2Request(); - AuthnRequestType loginReq = samlReq.createAuthnRequestType(UUID.randomUUID().toString(), assertionConsumerURL, destination.toString(), issuer); - - return loginReq; - } catch (ConfigurationException ex) { - throw new RuntimeException(ex); - } - } - - /** - * Send request for login form and then login using user param. This method is designed for clients without required consent - * - * @param user - * @param samlEndpoint - * @param samlRequest - * @param relayState - * @param requestBinding - * @param expectedResponseBinding - * @return - */ - public static SAMLDocumentHolder login(UserRepresentation user, URI samlEndpoint, - Document samlRequest, String relayState, Binding requestBinding, Binding expectedResponseBinding) { - return new SamlClient(samlEndpoint).login(user, samlRequest, relayState, requestBinding, expectedResponseBinding, false, true); - } - - private final HttpClientContext context = HttpClientContext.create(); - private final URI samlEndpoint; - - public SamlClient(URI samlEndpoint) { - this.samlEndpoint = samlEndpoint; - } - - public HttpClientContext getContext() { - return context; - } - - public URI getSamlEndpoint() { - return samlEndpoint; - } - - /** - * Send request for login form and then login using user param. Check whether client requires consent and handle consent page. - * - * @param user - * @param samlEndpoint - * @param samlRequest - * @param relayState - * @param requestBinding - * @param expectedResponseBinding - * @param consentRequired - * @param consent - * @return - */ - public SAMLDocumentHolder login(UserRepresentation user, - Document samlRequest, String relayState, Binding requestBinding, Binding expectedResponseBinding, boolean consentRequired, boolean consent) { - return getSamlResponse(expectedResponseBinding, (client, context, strategy) -> { - HttpUriRequest post = requestBinding.createSamlUnsignedRequest(samlEndpoint, relayState, samlRequest); - String loginPageText; - - try (CloseableHttpResponse response = client.execute(post, context)) { - assertThat(response, statusCodeIsHC(Response.Status.OK)); - loginPageText = EntityUtils.toString(response.getEntity(), "UTF-8"); - assertThat(loginPageText, containsString("login")); - } - - HttpUriRequest loginRequest = handleLoginPage(user, loginPageText); - - if (consentRequired) { - // Client requires consent - try (CloseableHttpResponse response = client.execute(loginRequest, context)) { - String consentPageText = EntityUtils.toString(response.getEntity(), "UTF-8"); - loginRequest = handleConsentPage(consentPageText, consent); - } - } - - strategy.setRedirectable(false); - return client.execute(loginRequest, context); - }); - } - - /** - * Send request for login form once already logged in, hence login using SSO. - * Check whether client requires consent and handle consent page. - * - * @param user - * @param samlEndpoint - * @param samlRequest - * @param relayState - * @param requestBinding - * @param expectedResponseBinding - * @return - */ - public SAMLDocumentHolder subsequentLoginViaSSO(Document samlRequest, String relayState, Binding requestBinding, Binding expectedResponseBinding) { - return getSamlResponse(expectedResponseBinding, (client, context, strategy) -> { - strategy.setRedirectable(false); - - HttpUriRequest post = requestBinding.createSamlUnsignedRequest(samlEndpoint, relayState, samlRequest); - CloseableHttpResponse response = client.execute(post, context); - assertThat(response, statusCodeIsHC(Response.Status.FOUND)); - String location = response.getFirstHeader("Location").getValue(); - - response = client.execute(new HttpGet(location), context); - assertThat(response, statusCodeIsHC(Response.Status.OK)); - return response; - }); - } - - /** - * Send request for login form once already logged in, hence login using SSO. - * Check whether client requires consent and handle consent page. - * - * @param user - * @param samlEndpoint - * @param samlRequest - * @param relayState - * @param requestBinding - * @param expectedResponseBinding - * @return - */ - public SAMLDocumentHolder logout(Document samlRequest, String relayState, Binding requestBinding, Binding expectedResponseBinding) { - return getSamlResponse(expectedResponseBinding, (client, context, strategy) -> { - strategy.setRedirectable(false); - - HttpUriRequest post = requestBinding.createSamlUnsignedRequest(samlEndpoint, relayState, samlRequest); - CloseableHttpResponse response = client.execute(post, context); - assertThat(response, statusCodeIsHC(Response.Status.OK)); - return response; - }); - } - - @FunctionalInterface - public interface HttpClientProcessor { - public CloseableHttpResponse process(CloseableHttpClient client, HttpContext context, RedirectStrategyWithSwitchableFollowRedirect strategy) throws Exception; - } - - public void execute(HttpClientProcessor body) { - CloseableHttpResponse response = null; - RedirectStrategyWithSwitchableFollowRedirect strategy = new RedirectStrategyWithSwitchableFollowRedirect(); - - try (CloseableHttpClient client = HttpClientBuilder.create().setRedirectStrategy(strategy).build()) { - response = body.process(client, context, strategy); - } catch (Exception ex) { - throw new RuntimeException(ex); - } finally { - if (response != null) { - EntityUtils.consumeQuietly(response.getEntity()); - try { - response.close(); - } catch (IOException ex) { - } - } - } - } - - public SAMLDocumentHolder getSamlResponse(Binding expectedResponseBinding, HttpClientProcessor body) { - CloseableHttpResponse response = null; - RedirectStrategyWithSwitchableFollowRedirect strategy = new RedirectStrategyWithSwitchableFollowRedirect(); - try (CloseableHttpClient client = HttpClientBuilder.create().setRedirectStrategy(strategy).build()) { - response = body.process(client, context, strategy); - - return expectedResponseBinding.extractResponse(response); - } catch (Exception ex) { - throw new RuntimeException(ex); - } finally { - if (response != null) { - EntityUtils.consumeQuietly(response.getEntity()); - try { - response.close(); - } catch (IOException ex) { - } - } - } - } - - /** - * Send request for login form and then login using user param for clients which doesn't require consent - * - * @param user - * @param idpInitiatedURI - * @param expectedResponseBinding - * @return - */ - public static SAMLDocumentHolder idpInitiatedLogin(UserRepresentation user, URI idpInitiatedURI, Binding expectedResponseBinding) { - return new SamlClient(idpInitiatedURI).idpInitiatedLogin(user, expectedResponseBinding, false, true); - } - - /** - * Send request for login form and then login using user param. For clients which requires consent - * - * @param user - * @param idpInitiatedURI - * @param expectedResponseBinding - * @param consent - * @return - */ - public static SAMLDocumentHolder idpInitiatedLoginWithRequiredConsent(UserRepresentation user, URI idpInitiatedURI, Binding expectedResponseBinding, boolean consent) { - return new SamlClient(idpInitiatedURI).idpInitiatedLogin(user, expectedResponseBinding, true, consent); - } - - /** - * Send request for login form and then login using user param. Checks whether client requires consent and handle consent page. - * - * @param user - * @param samlEndpoint - * @param expectedResponseBinding - * @param consent - * @return - */ - public SAMLDocumentHolder idpInitiatedLogin(UserRepresentation user, Binding expectedResponseBinding, boolean consentRequired, boolean consent) { - return getSamlResponse(expectedResponseBinding, (client, context, strategy) -> { - HttpGet get = new HttpGet(samlEndpoint); - CloseableHttpResponse response = client.execute(get); - assertThat(response, statusCodeIsHC(Response.Status.OK)); - - String loginPageText = EntityUtils.toString(response.getEntity(), "UTF-8"); - response.close(); - - assertThat(loginPageText, containsString("login")); - - HttpUriRequest loginRequest = handleLoginPage(user, loginPageText); - - if (consentRequired) { - // Client requires consent - response = client.execute(loginRequest, context); - String consentPageText = EntityUtils.toString(response.getEntity(), "UTF-8"); - loginRequest = handleConsentPage(consentPageText, consent); - } - - strategy.setRedirectable(false); - return client.execute(loginRequest, context); - }); - } - - -} diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/UserManager.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/UserManager.java index ed797b5baa..6bd3789696 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/UserManager.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/UserManager.java @@ -61,6 +61,12 @@ public class UserManager { userResource.update(user); } + public void enabled(Boolean enabled) { + UserRepresentation user = userResource.toRepresentation(); + user.setEnabled(enabled); + userResource.update(user); + } + private UserRepresentation initializeRequiredActions() { UserRepresentation user = userResource.toRepresentation(); 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 7a0e6b6a97..22d5ebc45f 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 @@ -108,7 +108,8 @@ "connectionsInfinispan": { "default": { "jgroupsUdpMcastAddr": "${keycloak.connectionsInfinispan.jgroupsUdpMcastAddr:234.56.78.90}", - "nodeName": "${keycloak.connectionsInfinispan.nodeName,jboss.node.name:defaultNodeName}", + "nodeName": "${keycloak.connectionsInfinispan.nodeName,jboss.node.name:}", + "siteName": "${keycloak.connectionsInfinispan.siteName,jboss.site.name:}", "clustered": "${keycloak.connectionsInfinispan.clustered:false}", "async": "${keycloak.connectionsInfinispan.async:false}", "sessionsOwners": "${keycloak.connectionsInfinispan.sessionsOwners:1}", @@ -118,13 +119,6 @@ "remoteStorePort": "${keycloak.connectionsInfinispan.remoteStorePort:11222}" } }, - - "stickySessionEncoder": { - "infinispan": { - "nodeName": "${keycloak.stickySessionEncoder.nodeName,jboss.node.name:defaultNodeName}" - } - }, - "truststore": { "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 e1901298d2..aed0231baf 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 @@ -8,6 +8,7 @@ "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" ], + "passwordPolicy": "hashIterations(1)", "defaultRoles": [ "user" ], "smtpServer": { "from": "auto@keycloak.org", diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/arquillian.xml b/testsuite/integration-arquillian/tests/base/src/test/resources/arquillian.xml index 58ef272b7c..acf153c810 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/resources/arquillian.xml +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/arquillian.xml @@ -50,7 +50,7 @@ ${auth.server.undertow} && ! ${auth.server.undertow.crossdc} - localhost + 0.0.0.0 org.keycloak.testsuite.arquillian.undertow.KeycloakOnUndertow ${auth.server.http.port} ${undertow.remote} @@ -68,6 +68,8 @@ -Djboss.socket.binding.port-offset=${auth.server.port.offset} -Djboss.bind.address=0.0.0.0 + -Dauth.server.http.port=${auth.server.http.port} + -Dauth.server.https.port=${auth.server.https.port} ${adapter.test.props} ${migration.import.properties} ${auth.server.profile} @@ -172,7 +174,7 @@ - ${auth.server.undertow.crossdc} + ${auth.server.undertow.crossdc} && ! ${cache.server.lifecycle.skip} org.jboss.as.arquillian.container.managed.ManagedDeployableContainer ${cache.server.home} clustered.xml @@ -195,7 +197,7 @@ - ${auth.server.undertow.crossdc} + ${auth.server.undertow.crossdc} && ! ${cache.server.lifecycle.skip} org.jboss.as.arquillian.container.managed.ManagedDeployableContainer ${cache.server.home} true @@ -240,6 +242,7 @@ 0 { "keycloak.connectionsInfinispan.jgroupsUdpMcastAddr": "234.56.78.1", + "keycloak.connectionsInfinispan.siteName": "dc-0", "keycloak.connectionsInfinispan.nodeName": "auth-server-undertow-cross-dc-0_1", "keycloak.connectionsInfinispan.clustered": "${keycloak.connectionsInfinispan.clustered:true}", "keycloak.connectionsInfinispan.remoteStoreServer": "${keycloak.connectionsInfinispan.remoteStoreServer:localhost}", @@ -263,6 +266,7 @@ 0 { "keycloak.connectionsInfinispan.jgroupsUdpMcastAddr": "234.56.78.1", + "keycloak.connectionsInfinispan.siteName": "dc-0", "keycloak.connectionsInfinispan.nodeName": "auth-server-undertow-cross-dc-0_2-manual", "keycloak.connectionsInfinispan.clustered": "${keycloak.connectionsInfinispan.clustered:true}", "keycloak.connectionsInfinispan.remoteStoreServer": "${keycloak.connectionsInfinispan.remoteStoreServer:localhost}", @@ -287,6 +291,7 @@ 1 { "keycloak.connectionsInfinispan.jgroupsUdpMcastAddr": "234.56.78.2", + "keycloak.connectionsInfinispan.siteName": "dc-1", "keycloak.connectionsInfinispan.nodeName": "auth-server-undertow-cross-dc-1_1", "keycloak.connectionsInfinispan.clustered": "${keycloak.connectionsInfinispan.clustered:true}", "keycloak.connectionsInfinispan.remoteStoreServer": "${keycloak.connectionsInfinispan.remoteStoreServer:localhost}", @@ -310,6 +315,7 @@ 1 { "keycloak.connectionsInfinispan.jgroupsUdpMcastAddr": "234.56.78.2", + "keycloak.connectionsInfinispan.siteName": "dc-1", "keycloak.connectionsInfinispan.nodeName": "auth-server-undertow-cross-dc-1_2-manual", "keycloak.connectionsInfinispan.clustered": "${keycloak.connectionsInfinispan.clustered:true}", "keycloak.connectionsInfinispan.remoteStoreServer": "${keycloak.connectionsInfinispan.remoteStoreServer:localhost}", diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/authorization-test/client-with-authz-settings.json b/testsuite/integration-arquillian/tests/base/src/test/resources/authorization-test/client-with-authz-settings.json new file mode 100644 index 0000000000..ccc3ccc975 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/authorization-test/client-with-authz-settings.json @@ -0,0 +1,866 @@ +{ + "clientId": "authz-client", + "enabled": true, + "publicClient": false, + "secret": "secret", + "directAccessGrantsEnabled": true, + "serviceAccountsEnabled": true, + "authorizationServicesEnabled": true, + "redirectUris": [ + "http://localhost/authz-client/*" + ], + "webOrigins": [ + "http://localhost" + ], + "authorizationSettings": { + "allowRemoteResourceManagement": true, + "policyEnforcementMode": "PERMISSIVE", + "resources": [ + { + "name": "Default Resource", + "uri": "/*", + "type": "urn:authz-client:resources:default" + }, + { + "name": "Resource 1", + "uri": "/protected/resource/1", + "scopes": [ + { + "name": "Scope B" + }, + { + "name": "Scope A" + }, + { + "name": "Scope D" + }, + { + "name": "Scope C" + }, + { + "name": "Scope E" + } + ] + }, + { + "name": "Resource 2", + "uri": "/protected/resource/2", + "scopes": [ + { + "name": "Scope B" + }, + { + "name": "Scope A" + }, + { + "name": "Scope D" + }, + { + "name": "Scope C" + }, + { + "name": "Scope E" + } + ] + }, + { + "name": "Resource 3", + "uri": "/protected/resource/3", + "scopes": [ + { + "name": "Scope B" + }, + { + "name": "Scope A" + }, + { + "name": "Scope D" + }, + { + "name": "Scope C" + }, + { + "name": "Scope E" + } + ] + }, + { + "name": "Resource 4", + "uri": "/protected/resource/4", + "scopes": [ + { + "name": "Scope B" + }, + { + "name": "Scope A" + }, + { + "name": "Scope D" + }, + { + "name": "Scope C" + }, + { + "name": "Scope E" + } + ] + }, + { + "name": "Resource 5", + "uri": "/protected/resource/5", + "scopes": [ + { + "name": "Scope B" + }, + { + "name": "Scope A" + }, + { + "name": "Scope D" + }, + { + "name": "Scope C" + }, + { + "name": "Scope E" + } + ] + }, + { + "name": "Resource 6", + "uri": "/protected/resource/6", + "scopes": [ + { + "name": "Scope B" + }, + { + "name": "Scope A" + }, + { + "name": "Scope D" + }, + { + "name": "Scope C" + }, + { + "name": "Scope E" + } + ] + }, + { + "name": "Resource 7", + "uri": "/protected/resource/7", + "scopes": [ + { + "name": "Scope B" + }, + { + "name": "Scope A" + }, + { + "name": "Scope D" + }, + { + "name": "Scope C" + }, + { + "name": "Scope E" + } + ] + }, + { + "name": "Resource 8", + "uri": "/protected/resource/8", + "scopes": [ + { + "name": "Scope B" + }, + { + "name": "Scope A" + }, + { + "name": "Scope D" + }, + { + "name": "Scope C" + }, + { + "name": "Scope E" + } + ] + }, + { + "name": "Resource 9", + "uri": "/protected/resource/9", + "scopes": [ + { + "name": "Scope B" + }, + { + "name": "Scope A" + }, + { + "name": "Scope D" + }, + { + "name": "Scope C" + }, + { + "name": "Scope E" + } + ] + }, + { + "name": "Resource 10", + "uri": "/protected/resource/10", + "scopes": [ + { + "name": "Scope B" + }, + { + "name": "Scope A" + }, + { + "name": "Scope D" + }, + { + "name": "Scope C" + }, + { + "name": "Scope E" + } + ] + }, + { + "name": "Resource 11", + "uri": "/protected/resource/11", + "scopes": [ + { + "name": "Scope B" + }, + { + "name": "Scope A" + }, + { + "name": "Scope D" + }, + { + "name": "Scope C" + }, + { + "name": "Scope E" + } + ] + }, + { + "name": "Resource 12", + "uri": "/protected/resource/12", + "scopes": [ + { + "name": "Scope B" + }, + { + "name": "Scope A" + }, + { + "name": "Scope D" + }, + { + "name": "Scope C" + }, + { + "name": "Scope E" + } + ] + }, + { + "name": "Resource 13", + "uri": "/protected/resource/13", + "scopes": [ + { + "name": "Scope B" + }, + { + "name": "Scope A" + }, + { + "name": "Scope D" + }, + { + "name": "Scope C" + }, + { + "name": "Scope E" + } + ] + }, + { + "name": "Resource 14", + "uri": "/protected/resource/14", + "scopes": [ + { + "name": "Scope B" + }, + { + "name": "Scope A" + }, + { + "name": "Scope D" + }, + { + "name": "Scope C" + }, + { + "name": "Scope E" + } + ] + }, + { + "name": "Resource 15", + "uri": "/protected/resource/15", + "scopes": [ + { + "name": "Scope B" + }, + { + "name": "Scope A" + }, + { + "name": "Scope D" + }, + { + "name": "Scope C" + }, + { + "name": "Scope E" + } + ] + }, + { + "name": "Resource 16", + "uri": "/protected/resource/16", + "scopes": [ + { + "name": "Scope B" + }, + { + "name": "Scope A" + }, + { + "name": "Scope D" + }, + { + "name": "Scope C" + }, + { + "name": "Scope E" + } + ] + }, + { + "name": "Resource 17", + "uri": "/protected/resource/17", + "scopes": [ + { + "name": "Scope B" + }, + { + "name": "Scope A" + }, + { + "name": "Scope D" + }, + { + "name": "Scope C" + }, + { + "name": "Scope E" + } + ] + }, + { + "name": "Resource 18", + "uri": "/protected/resource/18", + "scopes": [ + { + "name": "Scope B" + }, + { + "name": "Scope A" + }, + { + "name": "Scope D" + }, + { + "name": "Scope C" + }, + { + "name": "Scope E" + } + ] + }, + { + "name": "Resource 19", + "uri": "/protected/resource/19", + "scopes": [ + { + "name": "Scope B" + }, + { + "name": "Scope A" + }, + { + "name": "Scope D" + }, + { + "name": "Scope C" + }, + { + "name": "Scope E" + } + ] + }, + { + "name": "Resource 20", + "uri": "/protected/resource/20", + "scopes": [ + { + "name": "Scope B" + }, + { + "name": "Scope A" + }, + { + "name": "Scope D" + }, + { + "name": "Scope C" + }, + { + "name": "Scope E" + } + ] + } + ], + "policies": [ + { + "name": "Default Policy", + "description": "A policy that grants access only for users within this realm", + "type": "js", + "logic": "POSITIVE", + "decisionStrategy": "AFFIRMATIVE", + "config": { + "code": "// by default, grants any permission associated with this policy\n$evaluation.grant();\n" + } + }, + { + "name": "Resource 1 Policy", + "type": "role", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "config": { + "roles": "[{\"id\":\"authz-client/uma_protection\",\"required\":false}]" + } + }, + { + "name": "Resource 2 Policy", + "type": "role", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "config": { + "roles": "[{\"id\":\"authz-client/uma_protection\",\"required\":false}]" + } + }, + { + "name": "Resource 3 Policy", + "type": "role", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "config": { + "roles": "[{\"id\":\"authz-client/uma_protection\",\"required\":false}]" + } + }, + { + "name": "Resource 4 Policy", + "type": "role", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "config": { + "roles": "[{\"id\":\"authz-client/uma_protection\",\"required\":false}]" + } + }, + { + "name": "Resource 5 Policy", + "type": "role", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "config": { + "roles": "[{\"id\":\"authz-client/uma_protection\",\"required\":false}]" + } + }, + { + "name": "Resource 6 Policy", + "type": "role", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "config": { + "roles": "[{\"id\":\"authz-client/uma_protection\",\"required\":false}]" + } + }, + { + "name": "Resource 7 Policy", + "type": "role", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "config": { + "roles": "[{\"id\":\"authz-client/uma_protection\",\"required\":false}]" + } + }, + { + "name": "Resource 8 Policy", + "type": "role", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "config": { + "roles": "[{\"id\":\"authz-client/uma_protection\",\"required\":false}]" + } + }, + { + "name": "Resource 9 Policy", + "type": "role", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "config": { + "roles": "[{\"id\":\"authz-client/uma_protection\",\"required\":false}]" + } + }, + { + "name": "Resource 10 Policy", + "type": "role", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "config": { + "roles": "[{\"id\":\"authz-client/uma_protection\",\"required\":false}]" + } + }, + { + "name": "Resource 11 Policy", + "type": "role", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "config": { + "roles": "[{\"id\":\"authz-client/uma_protection\",\"required\":false}]" + } + }, + { + "name": "Resource 12 Policy", + "type": "role", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "config": { + "roles": "[{\"id\":\"authz-client/uma_protection\",\"required\":false}]" + } + }, + { + "name": "Resource 13 Policy", + "type": "role", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "config": { + "roles": "[{\"id\":\"authz-client/uma_protection\",\"required\":false}]" + } + }, + { + "name": "Resource 14 Policy", + "type": "role", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "config": { + "roles": "[{\"id\":\"authz-client/uma_protection\",\"required\":false}]" + } + }, + { + "name": "Resource 15 Policy", + "type": "role", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "config": { + "roles": "[{\"id\":\"authz-client/uma_protection\",\"required\":false}]" + } + }, + { + "name": "Resource 16 Policy", + "type": "role", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "config": { + "roles": "[{\"id\":\"authz-client/uma_protection\",\"required\":false}]" + } + }, + { + "name": "Resource 17 Policy", + "type": "role", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "config": { + "roles": "[{\"id\":\"authz-client/uma_protection\",\"required\":false}]" + } + }, + { + "name": "Resource 18 Policy", + "type": "role", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "config": { + "roles": "[{\"id\":\"authz-client/uma_protection\",\"required\":false}]" + } + }, + { + "name": "Resource 19 Policy", + "type": "role", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "config": { + "roles": "[{\"id\":\"authz-client/uma_protection\",\"required\":false}]" + } + }, + { + "name": "Resource 20 Policy", + "type": "role", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "config": { + "roles": "[{\"id\":\"authz-client/uma_protection\",\"required\":false}]" + } + }, + { + "name": "Default Permission", + "description": "A permission that applies to the default resource type", + "type": "resource", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "config": { + "defaultResourceType": "urn:authz-client:resources:default", + "applyPolicies": "[\"Default Policy\"]" + } + }, + { + "name": "Resource 1 Permission", + "type": "resource", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "config": { + "resources": "[\"Resource 1\"]", + "applyPolicies": "[\"Resource 1 Policy\"]" + } + }, + { + "name": "Resource 2 Permission", + "type": "resource", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "config": { + "resources": "[\"Resource 2\"]", + "applyPolicies": "[\"Resource 2 Policy\"]" + } + }, + { + "name": "Resource 3 Permission", + "type": "resource", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "config": { + "resources": "[\"Resource 3\"]", + "applyPolicies": "[\"Resource 3 Policy\"]" + } + }, + { + "name": "Resource 4 Permission", + "type": "resource", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "config": { + "resources": "[\"Resource 4\"]", + "applyPolicies": "[\"Resource 4 Policy\"]" + } + }, + { + "name": "Resource 5 Permission", + "type": "resource", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "config": { + "resources": "[\"Resource 5\"]", + "applyPolicies": "[\"Resource 5 Policy\"]" + } + }, + { + "name": "Resource 6 Permission", + "type": "resource", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "config": { + "resources": "[\"Resource 6\"]", + "applyPolicies": "[\"Resource 6 Policy\"]" + } + }, + { + "name": "Resource 7 Permission", + "type": "resource", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "config": { + "resources": "[\"Resource 7\"]", + "applyPolicies": "[\"Resource 7 Policy\"]" + } + }, + { + "name": "Resource 8 Permission", + "type": "resource", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "config": { + "resources": "[\"Resource 8\"]", + "applyPolicies": "[\"Resource 8 Policy\"]" + } + }, + { + "name": "Resource 9 Permission", + "type": "resource", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "config": { + "resources": "[\"Resource 9\"]", + "applyPolicies": "[\"Resource 9 Policy\"]" + } + }, + { + "name": "Resource 10 Permission", + "type": "resource", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "config": { + "resources": "[\"Resource 10\"]", + "applyPolicies": "[\"Resource 10 Policy\"]" + } + }, + { + "name": "Resource 11 Permission", + "type": "resource", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "config": { + "resources": "[\"Resource 11\"]", + "applyPolicies": "[\"Resource 11 Policy\"]" + } + }, + { + "name": "Resource 12 Permission", + "type": "resource", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "config": { + "resources": "[\"Resource 12\"]", + "applyPolicies": "[\"Resource 12 Policy\"]" + } + }, + { + "name": "Resource 13 Permission", + "type": "resource", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "config": { + "resources": "[\"Resource 13\"]", + "applyPolicies": "[\"Resource 13 Policy\"]" + } + }, + { + "name": "Resource 14 Permission", + "type": "resource", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "config": { + "resources": "[\"Resource 14\"]", + "applyPolicies": "[\"Resource 14 Policy\"]" + } + }, + { + "name": "Resource 15 Permission", + "type": "resource", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "config": { + "resources": "[\"Resource 15\"]", + "applyPolicies": "[\"Resource 15 Policy\"]" + } + }, + { + "name": "Resource 16 Permission", + "type": "resource", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "config": { + "resources": "[\"Resource 16\"]", + "applyPolicies": "[\"Resource 16 Policy\"]" + } + }, + { + "name": "Resource 17 Permission", + "type": "resource", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "config": { + "resources": "[\"Resource 17\"]", + "applyPolicies": "[\"Resource 17 Policy\"]" + } + }, + { + "name": "Resource 18 Permission", + "type": "resource", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "config": { + "resources": "[\"Resource 18\"]", + "applyPolicies": "[\"Resource 18 Policy\"]" + } + }, + { + "name": "Resource 19 Permission", + "type": "resource", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "config": { + "resources": "[\"Resource 19\"]", + "applyPolicies": "[\"Resource 19 Policy\"]" + } + }, + { + "name": "Resource 20 Permission", + "type": "resource", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "config": { + "resources": "[\"Resource 20\"]", + "applyPolicies": "[\"Resource 20 Policy\"]" + } + } + ], + "scopes": [ + { + "name": "Scope B" + }, + { + "name": "Scope A" + }, + { + "name": "Scope D" + }, + { + "name": "Scope C" + }, + { + "name": "Scope E" + } + ] + } +} \ No newline at end of file diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/docker-test-realm.json b/testsuite/integration-arquillian/tests/base/src/test/resources/docker-test-realm.json new file mode 100644 index 0000000000..9f9d2ff03a --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/docker-test-realm.json @@ -0,0 +1,1315 @@ +{ + "id" : "docker-test-realm", + "realm" : "docker-test-realm", + "notBefore" : 0, + "revokeRefreshToken" : false, + "accessTokenLifespan" : 300, + "accessTokenLifespanForImplicitFlow" : 900, + "ssoSessionIdleTimeout" : 1800, + "ssoSessionMaxLifespan" : 36000, + "offlineSessionIdleTimeout" : 2592000, + "accessCodeLifespan" : 60, + "accessCodeLifespanUserAction" : 300, + "accessCodeLifespanLogin" : 1800, + "enabled" : true, + "sslRequired" : "external", + "registrationAllowed" : false, + "registrationEmailAsUsername" : false, + "rememberMe" : false, + "verifyEmail" : false, + "loginWithEmailAllowed" : true, + "duplicateEmailsAllowed" : false, + "resetPasswordAllowed" : false, + "editUsernameAllowed" : false, + "bruteForceProtected" : false, + "maxFailureWaitSeconds" : 900, + "minimumQuickLoginWaitSeconds" : 60, + "waitIncrementSeconds" : 60, + "quickLoginCheckMilliSeconds" : 1000, + "maxDeltaTimeSeconds" : 43200, + "failureFactor" : 30, + "roles" : { + "realm" : [ { + "id" : "dbcbd18f-52cb-4e45-9372-7e2bbf255729", + "name" : "uma_authorization", + "description" : "${role_uma_authorization}", + "scopeParamRequired" : false, + "composite" : false, + "clientRole" : false, + "containerId" : "docker-test-realm" + }, { + "id" : "834687f7-29ce-43a2-a5f7-55c965026827", + "name" : "offline_access", + "description" : "${role_offline-access}", + "scopeParamRequired" : true, + "composite" : false, + "clientRole" : false, + "containerId" : "docker-test-realm" + } ], + "client" : { + "realm-management" : [ { + "id" : "11956a41-328d-4cec-a98c-f77fe6accda3", + "name" : "create-client", + "description" : "${role_create-client}", + "scopeParamRequired" : false, + "composite" : false, + "clientRole" : true, + "containerId" : "2d61e404-7444-4fad-8386-06b811b5f7c1" + }, { + "id" : "e65e7810-359b-429d-9389-c1cd041915fd", + "name" : "view-clients", + "description" : "${role_view-clients}", + "scopeParamRequired" : false, + "composite" : false, + "clientRole" : true, + "containerId" : "2d61e404-7444-4fad-8386-06b811b5f7c1" + }, { + "id" : "43d747fc-76c3-4a06-a492-44dea5a07edb", + "name" : "manage-clients", + "description" : "${role_manage-clients}", + "scopeParamRequired" : false, + "composite" : false, + "clientRole" : true, + "containerId" : "2d61e404-7444-4fad-8386-06b811b5f7c1" + }, { + "id" : "de324c4c-34ea-467b-b851-cca912d1cf60", + "name" : "view-authorization", + "description" : "${role_view-authorization}", + "scopeParamRequired" : false, + "composite" : false, + "clientRole" : true, + "containerId" : "2d61e404-7444-4fad-8386-06b811b5f7c1" + }, { + "id" : "b0f25ef8-404b-4370-a981-ca155eae6b83", + "name" : "manage-identity-providers", + "description" : "${role_manage-identity-providers}", + "scopeParamRequired" : false, + "composite" : false, + "clientRole" : true, + "containerId" : "2d61e404-7444-4fad-8386-06b811b5f7c1" + }, { + "id" : "c16eb517-5416-4b86-b86d-c312d3b98e09", + "name" : "impersonation", + "description" : "${role_impersonation}", + "scopeParamRequired" : false, + "composite" : false, + "clientRole" : true, + "containerId" : "2d61e404-7444-4fad-8386-06b811b5f7c1" + }, { + "id" : "1526f875-2d04-453a-aa29-979f61d1013c", + "name" : "view-events", + "description" : "${role_view-events}", + "scopeParamRequired" : false, + "composite" : false, + "clientRole" : true, + "containerId" : "2d61e404-7444-4fad-8386-06b811b5f7c1" + }, { + "id" : "7043cd10-a2b0-4568-8295-9840c9c2fa43", + "name" : "view-realm", + "description" : "${role_view-realm}", + "scopeParamRequired" : false, + "composite" : false, + "clientRole" : true, + "containerId" : "2d61e404-7444-4fad-8386-06b811b5f7c1" + }, { + "id" : "23bb0cd9-2c0e-4510-96af-73f0ba1251df", + "name" : "manage-realm", + "description" : "${role_manage-realm}", + "scopeParamRequired" : false, + "composite" : false, + "clientRole" : true, + "containerId" : "2d61e404-7444-4fad-8386-06b811b5f7c1" + }, { + "id" : "eff4c8dd-0c53-41ca-8013-336b9c19f55b", + "name" : "manage-authorization", + "description" : "${role_manage-authorization}", + "scopeParamRequired" : false, + "composite" : false, + "clientRole" : true, + "containerId" : "2d61e404-7444-4fad-8386-06b811b5f7c1" + }, { + "id" : "41cead2f-ed3f-4add-8fd2-ceaf3e20daf5", + "name" : "realm-admin", + "description" : "${role_realm-admin}", + "scopeParamRequired" : false, + "composite" : true, + "composites" : { + "client" : { + "realm-management" : [ "create-client", "view-clients", "manage-clients", "view-authorization", "manage-identity-providers", "impersonation", "view-events", "view-realm", "manage-realm", "manage-authorization", "view-users", "manage-events", "manage-users", "view-identity-providers" ] + } + }, + "clientRole" : true, + "containerId" : "2d61e404-7444-4fad-8386-06b811b5f7c1" + }, { + "id" : "025e57f5-73a2-4382-b6e7-ea2f447f86a5", + "name" : "view-users", + "description" : "${role_view-users}", + "scopeParamRequired" : false, + "composite" : false, + "clientRole" : true, + "containerId" : "2d61e404-7444-4fad-8386-06b811b5f7c1" + }, { + "id" : "86fce514-f5f4-4c7d-ae07-56caaeffe272", + "name" : "manage-events", + "description" : "${role_manage-events}", + "scopeParamRequired" : false, + "composite" : false, + "clientRole" : true, + "containerId" : "2d61e404-7444-4fad-8386-06b811b5f7c1" + }, { + "id" : "8f49cc1a-a3f1-4185-982e-765617c1ac88", + "name" : "manage-users", + "description" : "${role_manage-users}", + "scopeParamRequired" : false, + "composite" : false, + "clientRole" : true, + "containerId" : "2d61e404-7444-4fad-8386-06b811b5f7c1" + }, { + "id" : "e8d7cf8e-b970-4ada-a8b5-58b7d5fcc4e8", + "name" : "view-identity-providers", + "description" : "${role_view-identity-providers}", + "scopeParamRequired" : false, + "composite" : false, + "clientRole" : true, + "containerId" : "2d61e404-7444-4fad-8386-06b811b5f7c1" + } ], + "security-admin-console" : [ ], + "admin-cli" : [ ], + "broker" : [ { + "id" : "f0eb6730-f5ed-4216-a9db-d87fee982b08", + "name" : "read-token", + "description" : "${role_read-token}", + "scopeParamRequired" : false, + "composite" : false, + "clientRole" : true, + "containerId" : "f85d993b-f251-4f9c-87f9-6586cb7bb830" + } ], + "account" : [ { + "id" : "8a34db5e-26fb-4be0-ba09-d4e92bc9dd88", + "name" : "view-profile", + "description" : "${role_view-profile}", + "scopeParamRequired" : false, + "composite" : false, + "clientRole" : true, + "containerId" : "e6c8dc5d-ca52-4cfc-a9eb-5b86a2f6b6c9" + }, { + "id" : "5aef5567-004e-4a18-8ee4-b8a6d5fa0c85", + "name" : "manage-account", + "description" : "${role_manage-account}", + "scopeParamRequired" : false, + "composite" : true, + "composites" : { + "client" : { + "account" : [ "manage-account-links" ] + } + }, + "clientRole" : true, + "containerId" : "e6c8dc5d-ca52-4cfc-a9eb-5b86a2f6b6c9" + }, { + "id" : "3bf09e38-5f0d-41c8-adc2-1dba1cf5d819", + "name" : "manage-account-links", + "description" : "${role_manage-account-links}", + "scopeParamRequired" : false, + "composite" : false, + "clientRole" : true, + "containerId" : "e6c8dc5d-ca52-4cfc-a9eb-5b86a2f6b6c9" + } ] + } + }, + "groups" : [ ], + "defaultRoles" : [ "offline_access", "uma_authorization" ], + "requiredCredentials" : [ "password" ], + "passwordPolicy" : "hashIterations(20000)", + "otpPolicyType" : "totp", + "otpPolicyAlgorithm" : "HmacSHA1", + "otpPolicyInitialCounter" : 0, + "otpPolicyDigits" : 6, + "otpPolicyLookAheadWindow" : 1, + "otpPolicyPeriod" : 30, + "users" : [ { + "id" : "a413b2e2-5cff-43e4-ac6e-ab307e8c0652", + "createdTimestamp" : 1492117705870, + "username" : "user1", + "enabled" : true, + "totp" : false, + "emailVerified" : false, + "firstName" : "User", + "lastName" : "One", + "email" : "user1@redhat.com", + "credentials" : [ { + "type" : "password", + "hashedSaltedValue" : "A1B2lKKJ2npPjSoFo653q2H8Wu/CNoAVD9pYUnAJwMb0AJzAfXGkdX6eHSUEyUK1cDGVfn6iX/JRNo5XyoSH2w==", + "salt" : "5X0JI44mCfleW8qR08II1A==", + "hashIterations" : 20000, + "counter" : 0, + "algorithm" : "pbkdf2", + "digits" : 0, + "period" : 0, + "createdDate" : 1492117716198, + "config" : { } + } ], + "disableableCredentialTypes" : [ "password" ], + "requiredActions" : [ ], + "realmRoles" : [ "uma_authorization", "offline_access" ], + "clientRoles" : { + "account" : [ "view-profile", "manage-account" ] + }, + "groups" : [ ] + } ], + "clientScopeMappings" : { + "realm-management" : [ { + "client" : "admin-cli", + "roles" : [ "realm-admin" ] + }, { + "client" : "security-admin-console", + "roles" : [ "realm-admin" ] + } ] + }, + "clients" : [ { + "id" : "e6c8dc5d-ca52-4cfc-a9eb-5b86a2f6b6c9", + "clientId" : "account", + "name" : "${client_account}", + "baseUrl" : "/auth/realms/docker-test-realm/account", + "surrogateAuthRequired" : false, + "enabled" : true, + "clientAuthenticatorType" : "client-secret", + "secret" : "e4f21dc6-959f-4248-8e04-4fb606d9ceaf", + "defaultRoles" : [ "view-profile", "manage-account" ], + "redirectUris" : [ "/auth/realms/docker-test-realm/account/*" ], + "webOrigins" : [ ], + "notBefore" : 0, + "bearerOnly" : false, + "consentRequired" : false, + "standardFlowEnabled" : true, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : false, + "serviceAccountsEnabled" : false, + "publicClient" : false, + "frontchannelLogout" : false, + "attributes" : { }, + "fullScopeAllowed" : false, + "nodeReRegistrationTimeout" : 0, + "protocolMappers" : [ { + "id" : "d0e8f6a9-9442-443e-af03-7d31545af866", + "name" : "family name", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-property-mapper", + "consentRequired" : true, + "consentText" : "${familyName}", + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "lastName", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "family_name", + "jsonType.label" : "String" + } + }, { + "id" : "2afbd4f6-e9bc-45d1-92ee-1c4dc9c099d5", + "name" : "email", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-property-mapper", + "consentRequired" : true, + "consentText" : "${email}", + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "email", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "email", + "jsonType.label" : "String" + } + }, { + "id" : "1bd8d67f-3aac-42cc-8dba-e676a2b41bb1", + "name" : "docker-v2-allow-all-mapper", + "protocol" : "docker-v2", + "protocolMapper" : "docker-v2-allow-all-mapper", + "consentRequired" : false, + "config" : { } + }, { + "id" : "d7df006b-686a-41a8-958b-2525b9c48ff2", + "name" : "given name", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-property-mapper", + "consentRequired" : true, + "consentText" : "${givenName}", + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "firstName", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "given_name", + "jsonType.label" : "String" + } + }, { + "id" : "93bee57d-79e3-42fb-87da-71c05963aa49", + "name" : "role list", + "protocol" : "saml", + "protocolMapper" : "saml-role-list-mapper", + "consentRequired" : false, + "config" : { + "single" : "false", + "attribute.nameformat" : "Basic", + "attribute.name" : "Role" + } + }, { + "id" : "297ecd2f-4440-48aa-82aa-74901588f7c1", + "name" : "full name", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-full-name-mapper", + "consentRequired" : true, + "consentText" : "${fullName}", + "config" : { + "id.token.claim" : "true", + "access.token.claim" : "true" + } + }, { + "id" : "ac4d45a0-c127-4ba3-b243-49cc570a9871", + "name" : "username", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-property-mapper", + "consentRequired" : true, + "consentText" : "${username}", + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "username", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "preferred_username", + "jsonType.label" : "String" + } + } ], + "useTemplateConfig" : false, + "useTemplateScope" : false, + "useTemplateMappers" : false + }, { + "id" : "e0105ad8-27c3-471d-99c3-244762847563", + "clientId" : "admin-cli", + "name" : "${client_admin-cli}", + "surrogateAuthRequired" : false, + "enabled" : true, + "clientAuthenticatorType" : "client-secret", + "secret" : "72ff8162-b891-4ba3-9501-68e2e34d7cf0", + "redirectUris" : [ ], + "webOrigins" : [ ], + "notBefore" : 0, + "bearerOnly" : false, + "consentRequired" : false, + "standardFlowEnabled" : false, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : true, + "serviceAccountsEnabled" : false, + "publicClient" : true, + "frontchannelLogout" : false, + "attributes" : { }, + "fullScopeAllowed" : false, + "nodeReRegistrationTimeout" : 0, + "protocolMappers" : [ { + "id" : "c61ba5ee-a8e1-409c-9898-cb8b9697eb26", + "name" : "docker-v2-allow-all-mapper", + "protocol" : "docker-v2", + "protocolMapper" : "docker-v2-allow-all-mapper", + "consentRequired" : false, + "config" : { } + }, { + "id" : "879e8a4f-e4e9-402d-b867-59171fbcb370", + "name" : "family name", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-property-mapper", + "consentRequired" : true, + "consentText" : "${familyName}", + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "lastName", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "family_name", + "jsonType.label" : "String" + } + }, { + "id" : "ee335ebe-a3bd-426a-9622-268ad583fe67", + "name" : "role list", + "protocol" : "saml", + "protocolMapper" : "saml-role-list-mapper", + "consentRequired" : false, + "config" : { + "single" : "false", + "attribute.nameformat" : "Basic", + "attribute.name" : "Role" + } + }, { + "id" : "628083f3-62f0-454a-bc35-80728893513b", + "name" : "full name", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-full-name-mapper", + "consentRequired" : true, + "consentText" : "${fullName}", + "config" : { + "id.token.claim" : "true", + "access.token.claim" : "true" + } + }, { + "id" : "48efdb06-c88b-478f-9009-65bac264de00", + "name" : "username", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-property-mapper", + "consentRequired" : true, + "consentText" : "${username}", + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "username", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "preferred_username", + "jsonType.label" : "String" + } + }, { + "id" : "5683455d-bcaf-41ca-8b0e-da15dfd48753", + "name" : "email", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-property-mapper", + "consentRequired" : true, + "consentText" : "${email}", + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "email", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "email", + "jsonType.label" : "String" + } + }, { + "id" : "31d6698d-10f0-4fd9-b7f3-c4bc23b507dc", + "name" : "given name", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-property-mapper", + "consentRequired" : true, + "consentText" : "${givenName}", + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "firstName", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "given_name", + "jsonType.label" : "String" + } + } ], + "useTemplateConfig" : false, + "useTemplateScope" : false, + "useTemplateMappers" : false + }, { + "id" : "f85d993b-f251-4f9c-87f9-6586cb7bb830", + "clientId" : "broker", + "name" : "${client_broker}", + "surrogateAuthRequired" : false, + "enabled" : true, + "clientAuthenticatorType" : "client-secret", + "secret" : "1fbd3ca1-203f-4074-b1d5-b0c6c2739ea4", + "redirectUris" : [ ], + "webOrigins" : [ ], + "notBefore" : 0, + "bearerOnly" : false, + "consentRequired" : false, + "standardFlowEnabled" : true, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : false, + "serviceAccountsEnabled" : false, + "publicClient" : false, + "frontchannelLogout" : false, + "attributes" : { }, + "fullScopeAllowed" : false, + "nodeReRegistrationTimeout" : 0, + "protocolMappers" : [ { + "id" : "9b754f6b-0a03-4db5-80f9-3c4f656e0828", + "name" : "docker-v2-allow-all-mapper", + "protocol" : "docker-v2", + "protocolMapper" : "docker-v2-allow-all-mapper", + "consentRequired" : false, + "config" : { } + }, { + "id" : "384701c9-c08a-483f-8f44-b288c8694fe3", + "name" : "username", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-property-mapper", + "consentRequired" : true, + "consentText" : "${username}", + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "username", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "preferred_username", + "jsonType.label" : "String" + } + }, { + "id" : "c2e767e6-7744-457b-8dea-e6f170a5122c", + "name" : "given name", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-property-mapper", + "consentRequired" : true, + "consentText" : "${givenName}", + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "firstName", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "given_name", + "jsonType.label" : "String" + } + }, { + "id" : "796cc4cd-b7a5-4255-bf8b-3b99db7532ee", + "name" : "role list", + "protocol" : "saml", + "protocolMapper" : "saml-role-list-mapper", + "consentRequired" : false, + "config" : { + "single" : "false", + "attribute.nameformat" : "Basic", + "attribute.name" : "Role" + } + }, { + "id" : "528ba572-1438-4afc-88c7-02f5e511d433", + "name" : "email", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-property-mapper", + "consentRequired" : true, + "consentText" : "${email}", + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "email", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "email", + "jsonType.label" : "String" + } + }, { + "id" : "a14f7e92-23ea-444f-8bb8-f2dfb1f255dc", + "name" : "full name", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-full-name-mapper", + "consentRequired" : true, + "consentText" : "${fullName}", + "config" : { + "id.token.claim" : "true", + "access.token.claim" : "true" + } + }, { + "id" : "724e61f0-b490-46b1-b063-2ee122e4ac7a", + "name" : "family name", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-property-mapper", + "consentRequired" : true, + "consentText" : "${familyName}", + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "lastName", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "family_name", + "jsonType.label" : "String" + } + } ], + "useTemplateConfig" : false, + "useTemplateScope" : false, + "useTemplateMappers" : false + }, { + "id" : "2d61e404-7444-4fad-8386-06b811b5f7c1", + "clientId" : "realm-management", + "name" : "${client_realm-management}", + "surrogateAuthRequired" : false, + "enabled" : true, + "clientAuthenticatorType" : "client-secret", + "secret" : "403c5eae-8c79-4cfc-ba00-4bb2bfbaaf92", + "redirectUris" : [ ], + "webOrigins" : [ ], + "notBefore" : 0, + "bearerOnly" : true, + "consentRequired" : false, + "standardFlowEnabled" : true, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : false, + "serviceAccountsEnabled" : false, + "publicClient" : false, + "frontchannelLogout" : false, + "attributes" : { }, + "fullScopeAllowed" : false, + "nodeReRegistrationTimeout" : 0, + "protocolMappers" : [ { + "id" : "098aeaab-76f1-4742-8522-27e8c178e596", + "name" : "family name", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-property-mapper", + "consentRequired" : true, + "consentText" : "${familyName}", + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "lastName", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "family_name", + "jsonType.label" : "String" + } + }, { + "id" : "fbc2f08d-d6a0-49ad-9b61-601eec42d46f", + "name" : "full name", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-full-name-mapper", + "consentRequired" : true, + "consentText" : "${fullName}", + "config" : { + "id.token.claim" : "true", + "access.token.claim" : "true" + } + }, { + "id" : "b39438d3-a149-4e0f-a3a1-87c441d05123", + "name" : "docker-v2-allow-all-mapper", + "protocol" : "docker-v2", + "protocolMapper" : "docker-v2-allow-all-mapper", + "consentRequired" : false, + "config" : { } + }, { + "id" : "06706c9d-1f71-4cc8-afca-daea4e9fe9e8", + "name" : "email", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-property-mapper", + "consentRequired" : true, + "consentText" : "${email}", + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "email", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "email", + "jsonType.label" : "String" + } + }, { + "id" : "bcf14207-1f8e-4e53-8d2b-59939e82f8c4", + "name" : "username", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-property-mapper", + "consentRequired" : true, + "consentText" : "${username}", + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "username", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "preferred_username", + "jsonType.label" : "String" + } + }, { + "id" : "91e78da7-b049-41a5-9a22-1f833755c41b", + "name" : "given name", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-property-mapper", + "consentRequired" : true, + "consentText" : "${givenName}", + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "firstName", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "given_name", + "jsonType.label" : "String" + } + }, { + "id" : "3949f934-b86b-4e70-bcc4-52db0288d55b", + "name" : "role list", + "protocol" : "saml", + "protocolMapper" : "saml-role-list-mapper", + "consentRequired" : false, + "config" : { + "single" : "false", + "attribute.nameformat" : "Basic", + "attribute.name" : "Role" + } + } ], + "useTemplateConfig" : false, + "useTemplateScope" : false, + "useTemplateMappers" : false + }, { + "id" : "7d4ec353-1cf7-43a1-af4d-218fd9dd37ed", + "clientId" : "security-admin-console", + "name" : "${client_security-admin-console}", + "baseUrl" : "/auth/admin/docker-test-realm/console/index.html", + "surrogateAuthRequired" : false, + "enabled" : true, + "clientAuthenticatorType" : "client-secret", + "secret" : "a0e6ebf9-58fa-472c-a853-64c16c2f8ad8", + "redirectUris" : [ "/auth/admin/docker-test-realm/console/*" ], + "webOrigins" : [ ], + "notBefore" : 0, + "bearerOnly" : false, + "consentRequired" : false, + "standardFlowEnabled" : true, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : false, + "serviceAccountsEnabled" : false, + "publicClient" : true, + "frontchannelLogout" : false, + "attributes" : { }, + "fullScopeAllowed" : false, + "nodeReRegistrationTimeout" : 0, + "protocolMappers" : [ { + "id" : "c501a7bc-171b-4ce6-8d91-3f69ae32591d", + "name" : "given name", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-property-mapper", + "consentRequired" : true, + "consentText" : "${givenName}", + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "firstName", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "given_name", + "jsonType.label" : "String" + } + }, { + "id" : "bce6f7a9-b86d-4f5f-a262-f01e235b5622", + "name" : "locale", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "consentText" : "${locale}", + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "locale", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "locale", + "jsonType.label" : "String" + } + }, { + "id" : "9d28d5da-53f2-49f9-b0c0-ae3a51f5ac92", + "name" : "role list", + "protocol" : "saml", + "protocolMapper" : "saml-role-list-mapper", + "consentRequired" : false, + "config" : { + "single" : "false", + "attribute.nameformat" : "Basic", + "attribute.name" : "Role" + } + }, { + "id" : "00183de0-af80-47c5-807f-a62366b2e1b6", + "name" : "email", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-property-mapper", + "consentRequired" : true, + "consentText" : "${email}", + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "email", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "email", + "jsonType.label" : "String" + } + }, { + "id" : "31eccf32-3e16-44f2-b727-27c5cb2e9554", + "name" : "family name", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-property-mapper", + "consentRequired" : true, + "consentText" : "${familyName}", + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "lastName", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "family_name", + "jsonType.label" : "String" + } + }, { + "id" : "c26c0dc9-4cba-42f0-80e4-1f2363084b95", + "name" : "docker-v2-allow-all-mapper", + "protocol" : "docker-v2", + "protocolMapper" : "docker-v2-allow-all-mapper", + "consentRequired" : false, + "config" : { } + }, { + "id" : "db4d11d2-e243-4df7-811f-e4622b49950b", + "name" : "username", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-property-mapper", + "consentRequired" : true, + "consentText" : "${username}", + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "username", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "preferred_username", + "jsonType.label" : "String" + } + }, { + "id" : "e6d398a7-dbec-480f-93c4-8a9d1bfbad24", + "name" : "full name", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-full-name-mapper", + "consentRequired" : true, + "consentText" : "${fullName}", + "config" : { + "id.token.claim" : "true", + "access.token.claim" : "true" + } + } ], + "useTemplateConfig" : false, + "useTemplateScope" : false, + "useTemplateMappers" : false + } ], + "clientTemplates" : [ ], + "browserSecurityHeaders" : { + "xContentTypeOptions" : "nosniff", + "xRobotsTag" : "none", + "xFrameOptions" : "SAMEORIGIN", + "contentSecurityPolicy" : "frame-src 'self'" + }, + "smtpServer" : { }, + "eventsEnabled" : false, + "eventsListeners" : [ "jboss-logging" ], + "enabledEventTypes" : [ ], + "adminEventsEnabled" : false, + "adminEventsDetailsEnabled" : false, + "components" : { + "org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy" : [ { + "id" : "7f9cbf76-3ecb-49ed-850b-f2fce4ecc87f", + "name" : "Trusted Hosts", + "providerId" : "trusted-hosts", + "subType" : "anonymous", + "subComponents" : { }, + "config" : { + "host-sending-registration-request-must-match" : [ "true" ], + "client-uris-must-match" : [ "true" ] + } + }, { + "id" : "ea2db337-b9d9-463b-abea-0c5dadb5b5f0", + "name" : "Consent Required", + "providerId" : "consent-required", + "subType" : "anonymous", + "subComponents" : { }, + "config" : { } + }, { + "id" : "2d6e7a94-d73c-4f54-b9ea-64f563f5f8fa", + "name" : "Full Scope Disabled", + "providerId" : "scope", + "subType" : "anonymous", + "subComponents" : { }, + "config" : { } + }, { + "id" : "16f6705e-f671-4fde-ba7d-6254e404b503", + "name" : "Max Clients Limit", + "providerId" : "max-clients", + "subType" : "anonymous", + "subComponents" : { }, + "config" : { + "max-clients" : [ "200" ] + } + }, { + "id" : "e4baf3d7-e7af-48d0-890d-11304927be69", + "name" : "Allowed Protocol Mapper Types", + "providerId" : "allowed-protocol-mappers", + "subType" : "anonymous", + "subComponents" : { }, + "config" : { + "allowed-protocol-mapper-types" : [ "saml-user-attribute-mapper", "oidc-usermodel-attribute-mapper", "saml-user-property-mapper", "oidc-usermodel-property-mapper", "oidc-full-name-mapper", "oidc-address-mapper", "oidc-sha256-pairwise-sub-mapper", "saml-role-list-mapper" ], + "consent-required-for-all-mappers" : [ "true" ] + } + }, { + "id" : "c27ecc77-c0c3-462e-b803-33432c9a7813", + "name" : "Allowed Client Templates", + "providerId" : "allowed-client-templates", + "subType" : "anonymous", + "subComponents" : { }, + "config" : { } + }, { + "id" : "18bdc70c-5475-4ae4-8606-d52a6397a125", + "name" : "Allowed Protocol Mapper Types", + "providerId" : "allowed-protocol-mappers", + "subType" : "authenticated", + "subComponents" : { }, + "config" : { + "allowed-protocol-mapper-types" : [ "saml-user-attribute-mapper", "oidc-usermodel-attribute-mapper", "saml-user-property-mapper", "oidc-usermodel-property-mapper", "oidc-full-name-mapper", "oidc-address-mapper", "oidc-sha256-pairwise-sub-mapper", "saml-role-list-mapper" ], + "consent-required-for-all-mappers" : [ "true" ] + } + }, { + "id" : "95fd260b-36e9-4df5-aa6b-6c3b8138c766", + "name" : "Allowed Client Templates", + "providerId" : "allowed-client-templates", + "subType" : "authenticated", + "subComponents" : { }, + "config" : { } + } ], + "org.keycloak.keys.KeyProvider" : [ { + "id" : "9dc7e4c1-5bc2-4756-9486-fb64a06582ad", + "name" : "rsa-generated", + "providerId" : "rsa-generated", + "subComponents" : { }, + "config" : { + "privateKey" : [ "MIIEowIBAAKCAQEAk2ZfvP3znNH5EbBd6ckiT7Eq7loqBCa5o6fdOajD2X8cjT7roLG4GANhu075SUrCxfcx2A+P1kBnSsyPCc3dxMmCT7BUJsYScCF88q52GIskQc7E+eBkuIjeVmPMECLq3xhY7YONqIl47n17dEYYmVo1uRqbrVSFdSX9EDqn9vRn/7uJFLafdK9766Na2JMSZVKgnNsXRTtxxCjnU3LyMnNw5JdbnsfSPj1pgnOi+pTDPqlwfcAIaG72lmhWMXaStmwO1DYsBoUd4yEnv6/dtXQkAaDr6TthX7ITliaxXPrh+YMDAxnhV7X/PtbiFUpTaNBpSy3k87onYBiWrL44IQIDAQABAoIBAQCLigz0Q41OVlDt+ALQAYMj4lr8DgtcprRzQ8Tggu31hqom5Pv3woa+5OSuh9LjGY1OD/f1zLWkZI/kdcarx2I8m29rtUfU9QobcPhyXcqa7Y5DZlV/IHj5YUjqi8txMz0aOlhlcXa3qHz9eXlX18wN0SKuu4vJCQzWnEH4DS9ZTwXAp4uZUkOIUHIkACcRPBGBVHCNvwneLA7tPi5E1TK2fvlgyHOvbsomBh385WKrO6HFBmjV9XsMx3QU1EjRaXSpELdIDUR9Z8rgVg08nZ8z3LZ9UNHHdiAXoCm5oqqf8zP5gL6U79vybvjerCpx2AX60UkhpuHeUmZQQMcylLLhAoGBAP4xdt/gkBsC+9faAw3o9VW/6RsdW7ussptnt50Ymi/mlE8qHNe0oSbkGAhqdqCjAV0+cgygn2krOM+OUF/Lq87kBgRE0fAqaarEAryT/DrmvroNrp3Lnif9/kAcEWo8WhpIPgspqzVy7byAFR29/sdbVby2C37OeFYpw0ad/UVdAoGBAJRylgu59wM5ekrmJqNd326J+RLg76abF9TpW3Ka5CY12NgI60ZxRFBfncZKJCTovmoZgE89RHdz7n4ghxVg8D9ThPY7Kh4flAq8SIqAqmb2b7hkfyEMOgGpdwQq1T7uIcIefwYivLpb62C8cSK7leLXJ/wMza5bo8m5fD3t+a2VAoGAJZxqC2wtxmFlpCWU6Bz9GAgCVMm+RgGil8375Bm8zrOeZCxGAkCuy5NaXvxpuxEDZamUtHuburLzf/p9t/7p1/3zSfRo39FWuzavdPmsi4aS1/KoUJ7NMvupABFnHkH5zwO7cmli9NChjo+hEDqJlTPVdsu03bltIsqhIzTDQd0CgYAQ8owCxrZWnedCScg7emoZupK+/wMdKDOuUP3ptZk6a4dYEpyZrDC6ZFAk5S3/MLscbdDiOwJoCMo/iAMkA68p66UQX2zNh5llKF23wjyyCIx0prSE11p/+hLmXOV/i7w65zRlRO368KeMobbg2j2gaiPceLG6qCeozg5LG7IXiQKBgALwLpGKaIixsIaAD1Bzd5cLaKdPGXPyaJwG5xqog58XGVcHklGQRnaN/B3vlrHBgI/NGZNt83bWamCTVlN+A0q9AnMxGHXZHzL21lx6bNiZXX+3DVDm88m+ODPebZXxSZQRNjBrw1KotqUyyhzkbIjfE8752ofb4T+veViHkjW2" ], + "certificate" : [ "MIICsTCCAZkCBgFbaSTAdjANBgkqhkiG9w0BAQsFADAcMRowGAYDVQQDDBFkb2NrZXItdGVzdC1yZWFsbTAeFw0xNzA0MTMyMTA2MDdaFw0yNzA0MTMyMTA3NDdaMBwxGjAYBgNVBAMMEWRvY2tlci10ZXN0LXJlYWxtMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAk2ZfvP3znNH5EbBd6ckiT7Eq7loqBCa5o6fdOajD2X8cjT7roLG4GANhu075SUrCxfcx2A+P1kBnSsyPCc3dxMmCT7BUJsYScCF88q52GIskQc7E+eBkuIjeVmPMECLq3xhY7YONqIl47n17dEYYmVo1uRqbrVSFdSX9EDqn9vRn/7uJFLafdK9766Na2JMSZVKgnNsXRTtxxCjnU3LyMnNw5JdbnsfSPj1pgnOi+pTDPqlwfcAIaG72lmhWMXaStmwO1DYsBoUd4yEnv6/dtXQkAaDr6TthX7ITliaxXPrh+YMDAxnhV7X/PtbiFUpTaNBpSy3k87onYBiWrL44IQIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQB2u9hP3S1bP4+FBwOLPwI3p7WrWBlt2CgwTiyuXvV7u9GLiXqCDUWZd3dSks9vU4Y4NdVyToY4q9YFJ3oAQXlfRw2Yi6e/0nSPpU25o52TWwREnRY98fjVy1eC5K2GRwSu79HZKeqA0Tg/ONvGOrlYO1KPbWZGg9NcwAGeILkNdfI82w0KZTpTy+f5ATtV30pFkDNT0gfayFmDQvw3EgcD/x0/vI3PlnHLLGprV/ZlBmFWo0vk8iUBwP1YbTA0XqKasITFXJaPeZWzNMCjR1NxDqlIq095uX04E5XGS6XGJKS9PanvGXidk5xMgI7xwKE6jaxD9pspYPRgv66528Dc" ], + "priority" : [ "100" ] + } + }, { + "id" : "ae58bc1e-c60e-4889-986d-ea5648ea5989", + "name" : "hmac-generated", + "providerId" : "hmac-generated", + "subComponents" : { }, + "config" : { + "kid" : [ "5a0c54c4-fb3d-4b2c-8e1a-9bebb6251b6f" ], + "secret" : [ "-5XJ1f5410LDE1XIvQsvAuwwm4CdEyd6Rco0E3EsxG4" ], + "priority" : [ "100" ] + } + } ] + }, + "internationalizationEnabled" : false, + "supportedLocales" : [ ], + "authenticationFlows" : [ { + "id" : "6a3d3800-bea6-4fc4-958f-65365d23c33b", + "alias" : "Handle Existing Account", + "description" : "Handle what to do if there is existing account with same email/username like authenticated identity provider", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "idp-confirm-link", + "requirement" : "REQUIRED", + "priority" : 10, + "userSetupAllowed" : false, + "autheticatorFlow" : false + }, { + "authenticator" : "idp-email-verification", + "requirement" : "ALTERNATIVE", + "priority" : 20, + "userSetupAllowed" : false, + "autheticatorFlow" : false + }, { + "requirement" : "ALTERNATIVE", + "priority" : 30, + "flowAlias" : "Verify Existing Account by Re-authentication", + "userSetupAllowed" : false, + "autheticatorFlow" : true + } ] + }, { + "id" : "41de318f-6434-443a-bcf0-6632568f32b0", + "alias" : "Verify Existing Account by Re-authentication", + "description" : "Reauthentication of existing account", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "idp-username-password-form", + "requirement" : "REQUIRED", + "priority" : 10, + "userSetupAllowed" : false, + "autheticatorFlow" : false + }, { + "authenticator" : "auth-otp-form", + "requirement" : "OPTIONAL", + "priority" : 20, + "userSetupAllowed" : false, + "autheticatorFlow" : false + } ] + }, { + "id" : "8b2f90df-5a09-49b6-b978-acbb74a60670", + "alias" : "browser", + "description" : "browser based authentication", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "auth-cookie", + "requirement" : "ALTERNATIVE", + "priority" : 10, + "userSetupAllowed" : false, + "autheticatorFlow" : false + }, { + "authenticator" : "auth-spnego", + "requirement" : "DISABLED", + "priority" : 20, + "userSetupAllowed" : false, + "autheticatorFlow" : false + }, { + "authenticator" : "identity-provider-redirector", + "requirement" : "ALTERNATIVE", + "priority" : 25, + "userSetupAllowed" : false, + "autheticatorFlow" : false + }, { + "requirement" : "ALTERNATIVE", + "priority" : 30, + "flowAlias" : "forms", + "userSetupAllowed" : false, + "autheticatorFlow" : true + } ] + }, { + "id" : "6d0cba98-a1d9-4ca4-a877-ffe0d2c7f667", + "alias" : "clients", + "description" : "Base authentication for clients", + "providerId" : "client-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "client-secret", + "requirement" : "ALTERNATIVE", + "priority" : 10, + "userSetupAllowed" : false, + "autheticatorFlow" : false + }, { + "authenticator" : "client-jwt", + "requirement" : "ALTERNATIVE", + "priority" : 20, + "userSetupAllowed" : false, + "autheticatorFlow" : false + } ] + }, { + "id" : "8c752045-bd44-48fc-ae36-816625897545", + "alias" : "direct grant", + "description" : "OpenID Connect Resource Owner Grant", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "direct-grant-validate-username", + "requirement" : "REQUIRED", + "priority" : 10, + "userSetupAllowed" : false, + "autheticatorFlow" : false + }, { + "authenticator" : "direct-grant-validate-password", + "requirement" : "REQUIRED", + "priority" : 20, + "userSetupAllowed" : false, + "autheticatorFlow" : false + }, { + "authenticator" : "direct-grant-validate-otp", + "requirement" : "OPTIONAL", + "priority" : 30, + "userSetupAllowed" : false, + "autheticatorFlow" : false + } ] + }, { + "id" : "7c8e6906-6b5f-4766-b80d-f23b56595992", + "alias" : "docker-basic-auth-flow", + "description" : "", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : false, + "authenticationExecutions" : [ { + "authenticator" : "docker-http-basic-authenticator", + "requirement" : "REQUIRED", + "priority" : 0, + "userSetupAllowed" : false, + "autheticatorFlow" : false + } ] + }, { + "id" : "a41036cf-e368-46e0-9cf3-a96908c53609", + "alias" : "first broker login", + "description" : "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticatorConfig" : "review profile config", + "authenticator" : "idp-review-profile", + "requirement" : "REQUIRED", + "priority" : 10, + "userSetupAllowed" : false, + "autheticatorFlow" : false + }, { + "authenticatorConfig" : "create unique user config", + "authenticator" : "idp-create-user-if-unique", + "requirement" : "ALTERNATIVE", + "priority" : 20, + "userSetupAllowed" : false, + "autheticatorFlow" : false + }, { + "requirement" : "ALTERNATIVE", + "priority" : 30, + "flowAlias" : "Handle Existing Account", + "userSetupAllowed" : false, + "autheticatorFlow" : true + } ] + }, { + "id" : "49c349cc-f11e-461c-98e2-546327175ca4", + "alias" : "forms", + "description" : "Username, password, otp and other auth forms.", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "auth-username-password-form", + "requirement" : "REQUIRED", + "priority" : 10, + "userSetupAllowed" : false, + "autheticatorFlow" : false + }, { + "authenticator" : "auth-otp-form", + "requirement" : "OPTIONAL", + "priority" : 20, + "userSetupAllowed" : false, + "autheticatorFlow" : false + } ] + }, { + "id" : "2445867e-f9eb-46cc-8f68-c15d6cf962e4", + "alias" : "registration", + "description" : "registration flow", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "registration-page-form", + "requirement" : "REQUIRED", + "priority" : 10, + "flowAlias" : "registration form", + "userSetupAllowed" : false, + "autheticatorFlow" : true + } ] + }, { + "id" : "83a735c2-cf61-49fa-879b-e9b0ed5bb9e9", + "alias" : "registration form", + "description" : "registration form", + "providerId" : "form-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "registration-user-creation", + "requirement" : "REQUIRED", + "priority" : 20, + "userSetupAllowed" : false, + "autheticatorFlow" : false + }, { + "authenticator" : "registration-profile-action", + "requirement" : "REQUIRED", + "priority" : 40, + "userSetupAllowed" : false, + "autheticatorFlow" : false + }, { + "authenticator" : "registration-password-action", + "requirement" : "REQUIRED", + "priority" : 50, + "userSetupAllowed" : false, + "autheticatorFlow" : false + }, { + "authenticator" : "registration-recaptcha-action", + "requirement" : "DISABLED", + "priority" : 60, + "userSetupAllowed" : false, + "autheticatorFlow" : false + } ] + }, { + "id" : "32acb7cb-af8f-42b2-bd34-9ff534d87121", + "alias" : "reset credentials", + "description" : "Reset credentials for a user if they forgot their password or something", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "reset-credentials-choose-user", + "requirement" : "REQUIRED", + "priority" : 10, + "userSetupAllowed" : false, + "autheticatorFlow" : false + }, { + "authenticator" : "reset-credential-email", + "requirement" : "REQUIRED", + "priority" : 20, + "userSetupAllowed" : false, + "autheticatorFlow" : false + }, { + "authenticator" : "reset-password", + "requirement" : "REQUIRED", + "priority" : 30, + "userSetupAllowed" : false, + "autheticatorFlow" : false + }, { + "authenticator" : "reset-otp", + "requirement" : "OPTIONAL", + "priority" : 40, + "userSetupAllowed" : false, + "autheticatorFlow" : false + } ] + }, { + "id" : "1c67b912-70f4-4182-b055-08c3d6bb23c8", + "alias" : "saml ecp", + "description" : "SAML ECP Profile Authentication Flow", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "http-basic-authenticator", + "requirement" : "REQUIRED", + "priority" : 10, + "userSetupAllowed" : false, + "autheticatorFlow" : false + } ] + } ], + "authenticatorConfig" : [ { + "id" : "30fd72e5-eb98-4ae5-a695-c959ec626ac6", + "alias" : "create unique user config", + "config" : { + "require.password.update.after.registration" : "false" + } + }, { + "id" : "e0ea82a7-98d7-4ffb-8444-8d240a94d83b", + "alias" : "review profile config", + "config" : { + "update.profile.on.first.login" : "missing" + } + } ], + "requiredActions" : [ { + "alias" : "CONFIGURE_TOTP", + "name" : "Configure OTP", + "providerId" : "CONFIGURE_TOTP", + "enabled" : true, + "defaultAction" : false, + "config" : { } + }, { + "alias" : "UPDATE_PASSWORD", + "name" : "Update Password", + "providerId" : "UPDATE_PASSWORD", + "enabled" : true, + "defaultAction" : false, + "config" : { } + }, { + "alias" : "UPDATE_PROFILE", + "name" : "Update Profile", + "providerId" : "UPDATE_PROFILE", + "enabled" : true, + "defaultAction" : false, + "config" : { } + }, { + "alias" : "VERIFY_EMAIL", + "name" : "Verify Email", + "providerId" : "VERIFY_EMAIL", + "enabled" : true, + "defaultAction" : false, + "config" : { } + }, { + "alias" : "terms_and_conditions", + "name" : "Terms and Conditions", + "providerId" : "terms_and_conditions", + "enabled" : false, + "defaultAction" : false, + "config" : { } + } ], + "browserFlow" : "docker-basic-auth-flow", + "registrationFlow" : "registration", + "directGrantFlow" : "direct grant", + "resetCredentialsFlow" : "reset credentials", + "clientAuthenticationFlow" : "clients", + "attributes" : { + "_browser_header.xFrameOptions" : "SAMEORIGIN", + "failureFactor" : "30", + "quickLoginCheckMilliSeconds" : "1000", + "maxDeltaTimeSeconds" : "43200", + "_browser_header.xContentTypeOptions" : "nosniff", + "_browser_header.xRobotsTag" : "none", + "bruteForceProtected" : "false", + "maxFailureWaitSeconds" : "900", + "_browser_header.contentSecurityPolicy" : "frame-src 'self'", + "minimumQuickLoginWaitSeconds" : "60", + "waitIncrementSeconds" : "60" + } +} diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/dockerClientTest/keycloak-docker-compose-yaml/certs/docker-realm-public-key.pem b/testsuite/integration-arquillian/tests/base/src/test/resources/dockerClientTest/keycloak-docker-compose-yaml/certs/docker-realm-public-key.pem new file mode 100644 index 0000000000..a7493f1f52 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/dockerClientTest/keycloak-docker-compose-yaml/certs/docker-realm-public-key.pem @@ -0,0 +1,17 @@ +-----BEGIN CERTIFICATE----- +MIICsTCCAZkCBgFbaSTAdjANBgkqhkiG9w0BAQsFADAcMRowGAYDVQQDDBFkb2Nr +ZXItdGVzdC1yZWFsbTAeFw0xNzA0MTMyMTA2MDdaFw0yNzA0MTMyMTA3NDdaMBwx +GjAYBgNVBAMMEWRvY2tlci10ZXN0LXJlYWxtMIIBIjANBgkqhkiG9w0BAQEFAAOC +AQ8AMIIBCgKCAQEAk2ZfvP3znNH5EbBd6ckiT7Eq7loqBCa5o6fdOajD2X8cjT7r +oLG4GANhu075SUrCxfcx2A+P1kBnSsyPCc3dxMmCT7BUJsYScCF88q52GIskQc7E ++eBkuIjeVmPMECLq3xhY7YONqIl47n17dEYYmVo1uRqbrVSFdSX9EDqn9vRn/7uJ +FLafdK9766Na2JMSZVKgnNsXRTtxxCjnU3LyMnNw5JdbnsfSPj1pgnOi+pTDPqlw +fcAIaG72lmhWMXaStmwO1DYsBoUd4yEnv6/dtXQkAaDr6TthX7ITliaxXPrh+YMD +AxnhV7X/PtbiFUpTaNBpSy3k87onYBiWrL44IQIDAQABMA0GCSqGSIb3DQEBCwUA +A4IBAQB2u9hP3S1bP4+FBwOLPwI3p7WrWBlt2CgwTiyuXvV7u9GLiXqCDUWZd3dS +ks9vU4Y4NdVyToY4q9YFJ3oAQXlfRw2Yi6e/0nSPpU25o52TWwREnRY98fjVy1eC +5K2GRwSu79HZKeqA0Tg/ONvGOrlYO1KPbWZGg9NcwAGeILkNdfI82w0KZTpTy+f5 +ATtV30pFkDNT0gfayFmDQvw3EgcD/x0/vI3PlnHLLGprV/ZlBmFWo0vk8iUBwP1Y +bTA0XqKasITFXJaPeZWzNMCjR1NxDqlIq095uX04E5XGS6XGJKS9PanvGXidk5xM +gI7xwKE6jaxD9pspYPRgv66528Dc +-----END CERTIFICATE----- \ No newline at end of file diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/dockerClientTest/keycloak-docker-compose-yaml/certs/localhost.crt b/testsuite/integration-arquillian/tests/base/src/test/resources/dockerClientTest/keycloak-docker-compose-yaml/certs/localhost.crt new file mode 100644 index 0000000000..6b50a0459c --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/dockerClientTest/keycloak-docker-compose-yaml/certs/localhost.crt @@ -0,0 +1,35 @@ +-----BEGIN CERTIFICATE----- +MIIGBTCCA+2gAwIBAgIJALfo8UyCLlnkMA0GCSqGSIb3DQEBCwUAMIGYMQswCQYD +VQQGEwJVUzEXMBUGA1UECAwOTm9ydGggQ2Fyb2xpbmExEDAOBgNVBAcMB1JhbGVp +Z2gxFjAUBgNVBAoMDVJlZCBIYXQsIEluYy4xJzAlBgNVBAsMHklkZW50aXR5IGFu +ZCBBY2Nlc3MgTWFuYWdlbWVudDEdMBsGA1UEAwwUcmVnaXN0cnkubG9jYWxkb21h +aW4wHhcNMTcwNDIwMDMwNzMwWhcNMjAwMTE0MDMwNzMwWjCBmDELMAkGA1UEBhMC +VVMxFzAVBgNVBAgMDk5vcnRoIENhcm9saW5hMRAwDgYDVQQHDAdSYWxlaWdoMRYw +FAYDVQQKDA1SZWQgSGF0LCBJbmMuMScwJQYDVQQLDB5JZGVudGl0eSBhbmQgQWNj +ZXNzIE1hbmFnZW1lbnQxHTAbBgNVBAMMFHJlZ2lzdHJ5LmxvY2FsZG9tYWluMIIC +IjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAyIKYO7gYA9T8PpqTf2Lad81X +cHzhiRYvvzUDgR4UD1NummWPnl2sPjdlQayM/TZ7p6gserdLjms336tvU/6GOIjv +v10uvDsFVxafuASY1tQSlrFLwF2NwavVOWlPhdlYLvOUnT/zk7fWKRFy7WXp6hD5 +RAkI4+ywuhS6eiZy3wIv/04VjFGYAB1x3NfHVwSuo+cjz/UvI3sU1i0LR+aOSRoP +9GM8OBpaTxRu/vEHd3k0A2FLP3sJYzkSD6A0p+nqbMfrPKRuZEjDYvBad4KemAl2 +5GRxNeZkJUk0CX2QK2cqr6xOa7598Nr+3ejv99Iiga5r2VlSSdsbV3U9j3RoZY48 +J0RvSgsVeeYqE93SUsVKhSoN4UIdhiVoDCvLtuIeqfQjehowent03OwDUiYw0TeV +GqmcN54Ki6v+EWSNqY2h01wcbMuQw6PDQ/mn1pz7f/ZAt9T0fop6ml4Mg4nud9S9 +b/Y9+XfuJlPKwZIgQEtrpSfLveOBmWYRu9/rSX9YtHx+pyzbWDtwrF0O9Z/pO+T4 +qOMmfc2ltjzRMFKK6JZFhFVHQP0AKsxLChQrzoHr5k7Rmcn+iGtmqD4tWtzgEQvA +umhNsm4nrR92hB97yxw3WC9gGvJlBIi/swrCxiKCJDklxCZtVCmqwMFx/bzXu3pH +sKwYv3poURR9NZb7kDcCAwEAAaNQME4wHQYDVR0OBBYEFNhH71tQSivnjfCHd7pt +3Qo50DCZMB8GA1UdIwQYMBaAFNhH71tQSivnjfCHd7pt3Qo50DCZMAwGA1UdEwQF +MAMBAf8wDQYJKoZIhvcNAQELBQADggIBAGSCDF/l/ExabQ1DfoKoRCmVoslnK+M1 +0TuDtfss2zqF89BPLBNBKdfp7r1OV4fp465HMpd2ovUkuijLjrIf78+I4AFEv60s +Z7NKMYEULpvBZ3RY7INr9CoNcWGvnfC/h782axjyI6ZW6I2v717FcciI6su0Eg+k +kF6+c+cVLmhKLi7hnC9mlN0JMUcOt3cBuZ8NvCHwW6VFmv8hsxt8Z18JcY6aPZE8 +32XzdgcU/U9OAhv1iMEuoGAqQatCHAmA3FOpfI9LjVOxW0LZgHWKX7OEyDEZ+7Ed +DbEpD73bmTp89lvFcT0UEAcWkRpD+VSozgYEzSeNmzKks2ngl37SlG2YQ23UzgYS +alGcUEJFBmWr9pJUN+tDPzbtmlrEw9pA6xYZMTDgAQSRHGQK/5lISuzEIMR0nh3q +Hyhmamlg+zkF415gYKUwh96NgalIc+Y9B4vnSpOv7b+ZFXoubBD2Wk5oi0Ziyog0 +J8YcbLQ8ZhINRvDyNv0iWHNachIzO1/N5G5H8hjibLkH+tpFBSs3uCiwTi+L/MlD +Pqc0A6Slyi8TnJJDFCDaa3xU321dkvyhGmPeqiyIK+dpJO1FI3OU0rZeGGcyc+K6 +SnDRByp0HQt9W/8Aw+kXjUoI8LOYeR/7Ctd+Tqf11TDxmw9w9LSIEhiYeEJQCxTc +Dk72PkeTi1zO +-----END CERTIFICATE----- diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/dockerClientTest/keycloak-docker-compose-yaml/certs/localhost.key b/testsuite/integration-arquillian/tests/base/src/test/resources/dockerClientTest/keycloak-docker-compose-yaml/certs/localhost.key new file mode 100644 index 0000000000..22a39863af --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/dockerClientTest/keycloak-docker-compose-yaml/certs/localhost.key @@ -0,0 +1,51 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIJKQIBAAKCAgEAyIKYO7gYA9T8PpqTf2Lad81XcHzhiRYvvzUDgR4UD1NummWP +nl2sPjdlQayM/TZ7p6gserdLjms336tvU/6GOIjvv10uvDsFVxafuASY1tQSlrFL +wF2NwavVOWlPhdlYLvOUnT/zk7fWKRFy7WXp6hD5RAkI4+ywuhS6eiZy3wIv/04V +jFGYAB1x3NfHVwSuo+cjz/UvI3sU1i0LR+aOSRoP9GM8OBpaTxRu/vEHd3k0A2FL +P3sJYzkSD6A0p+nqbMfrPKRuZEjDYvBad4KemAl25GRxNeZkJUk0CX2QK2cqr6xO +a7598Nr+3ejv99Iiga5r2VlSSdsbV3U9j3RoZY48J0RvSgsVeeYqE93SUsVKhSoN +4UIdhiVoDCvLtuIeqfQjehowent03OwDUiYw0TeVGqmcN54Ki6v+EWSNqY2h01wc +bMuQw6PDQ/mn1pz7f/ZAt9T0fop6ml4Mg4nud9S9b/Y9+XfuJlPKwZIgQEtrpSfL +veOBmWYRu9/rSX9YtHx+pyzbWDtwrF0O9Z/pO+T4qOMmfc2ltjzRMFKK6JZFhFVH +QP0AKsxLChQrzoHr5k7Rmcn+iGtmqD4tWtzgEQvAumhNsm4nrR92hB97yxw3WC9g +GvJlBIi/swrCxiKCJDklxCZtVCmqwMFx/bzXu3pHsKwYv3poURR9NZb7kDcCAwEA +AQKCAgEAsPuM0dGZ6O/7QmsAXEVuHqbyUkj4bh9WP8jUcgiRnkF/c+rHTPrTyQru +Znye6fZISWFI+XyGxYvgAp54osQbxxUfwWLHmL/j484FZtEv8xe33Klb+szZDiTV +DVrmJXgFvVOlTvOe1TlEYHWVYvQ89yzKSIJNBZnrGCSpwJ3lcPCmWwyaOoPezeMv +mMYhnq50VBn2Y13AoOnIJ5AUz/8yglXt1UIuajrgkcKwgnlPpOYnwgAEAmFglONQ +DNjVAY2YLTJ9ccaV5hDP3anXwHtb70kTV19NCk11AfBObT4Wniju5acKhVHcKley +9T7haXZinOLPMUcFOkmbJaRHlTMj3UgnF4k2iJJ7NyY3lAAIedlZ3EFNwpa68Roo +WClNAJIV6KYRExOZfqeRyR09loTnynPgxkMR4N4oLJHCiTtReXW5Y1HAYbT+iVHC +Ox1ob/INuZ1VoumDfn6bRqFdK8LldjBwVqRecSad/dg84BtjTB/po81aUpSRENEV +aZP+jOT9kZbybACh8FdF8u7mxgL+x7Xidng3SKRJi5whQJNmQ62QkzTFMPVXCqlO +ABsz2a/Zw7swyetg9uApoTTCeK1P0V/MrcEVTIGmcABfBYAVMBj1S2SH1xgAr20P +IR3SOpPtiNYhIIOnfyQQ3qVudsaSOAJH26I7QLnMyBqOId0Js9ECggEBAOSrGSfT +bm7OhGu1ZcTmlS17kjsUUYn1Uy30vV5e7uhpQGmr4rKVWYkNeZa5qtJossY3z+4H +9fZAqJWH2Cr/4pqnfz4GqK+qE56fFdbyHzHKLZOXZGdp9fQzlLsEi9JVYgv+nAPR +MHS7WeMTUlFc+P3pP6Btyhk/x7YfZnnlatFYlsNJVzUVdblrG6wSVZGpmxcNIeM2 +UeGG78aDBZQdKUO+xuh6MFW20lU165QC1JfGE+NRawqvgSD09F3MGkEwJuD8XEBg +/rOwNUg8/ayQhd1EgRGQOiDgqfXSpsF101HPUSX/HDC41KG3gTKTc/Vw+ac5ID1r +b3PKExEXCicDgCkCggEBAOB55eVsRZHBHeBjhqemH8SxWUfSCbx17cGbs7sw95Rs +3wYci7ABC8wbvG5UDNPd3BI2IV5bJWYOlbVv+Y1FjNHamQjiSXgB3g6RzvaM0bVP +1Rvn7EvQF87XIKEdo3uHtvpSVBDHYq/DtDyE9wwaNctxBgJwThVXVYINsp+leGsD +uGVMAsUP01vMNdHJBk/ANPvYxUkDOCtlDDV8cyaFVJAq4/A1h4crv39S/6ZY/RWo +LQpYnA47pfKZzxvtDQsnVTmolQ8x4yAX5bQrpKAt/hIJhzKdeCglgVr9cq/7sNOO +kDLZzPLlFPRX1gOHTpDlucNxxlIjPh2h+3CCCPUzGV8CggEAYGmDgbczqKSKUJ96 ++Tn/S93+GcrHVlOJbqbx8Qg10ugNsIA4ZPNzfMWhrls6GtzqA4kkskfI/LrmWaWd +DwQ0luBoVc6Y8PfUrdyFaMtNO8Dy1nfObYvPl9bnrrKMAXLelBAV18YrmAwmKgfL +fWKl2OivWwTvYRXzLmau3lZMY1fmuRADJO6XZEY0tKhGS9Qm/+EZmKMeguhR0HEN +uRVSgK2/T+W0227p3+OMICvRVuy9FesOJsM4vpyJK8MSjsmums3MV5iNy1VQIdUV +X9zPlCt9/9m/qH0RLARVKtxy7Ntsa4jUafaEMGseniRtj97CZC9B2KOjqj5ZK6t7 +LFfdgQKCAQEAtu6gC3dQupdGYba55aXb/c8Jkx34ET2JpF3e+o3NNYgDuFdK/wPb +OVrhFIgqa/5BehXi26IruB/qoRG/rQEg4WPjkvnWJZZgAD+TChl4TOniIfu+9Yl/ +3XAzhxlAQUs4MoclOwdBxTsXhrpVGefCLyjMXPBosbuaU4IWL0QJ/ivp+aMYHr/m +3shsk6nfGt7oTtU48WdOPw76BByHOr0tTM+nMfptmBpu1LQu4sFifmOvUN8lTfQO +KMZvobJtDsnfCj34O4nMLjtLVqi6YE8a3lgldXoekZj+8cfZztCuKbnkiYw1GTzW +9skd/4Ik5LBR0pTFqepOlJeM8QMHics6wQKCAQA+6RvPk2/b8OJArrFHkhNbfqpf +Sa/BvRam8azo2MGgOZWVm/yAGHvoVgOaq2H1DrrDh6qBlzZULpwFD+XeuuzYrLs2 +mYr2LFZdeQtd95V7oASdM0OlFatzKPOoLrHwNc4ztwNz0sMrjTYxDG07mp/3Ixz7 +koUPinV636wZUmvwHiUTlD4E2db+fslDhBUc+HV/4MXihvMSA3D8Mum9SttMABYJ +L0lBzexfVL8oyYvft/tGwV9LwrlFpzndnX6ZZvgJUqzBPx/+exuZjnTwD3N70SN+ +T0TwL0tsVE5clxVdv5xlm5WIW4kQKglRoJnVB1TnpFddRRu/QD8S+e/S6G4w +-----END RSA PRIVATE KEY----- diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/dockerClientTest/keycloak-docker-compose-yaml/docker-compose.yaml b/testsuite/integration-arquillian/tests/base/src/test/resources/dockerClientTest/keycloak-docker-compose-yaml/docker-compose.yaml new file mode 100644 index 0000000000..53702a6fd5 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/dockerClientTest/keycloak-docker-compose-yaml/docker-compose.yaml @@ -0,0 +1,15 @@ +registry: + image: registry:2 + ports: + - 127.0.0.1:5000:5000 + environment: + REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY: /data + REGISTRY_HTTP_TLS_CERTIFICATE: /opt/certs/localhost.crt + REGISTRY_HTTP_TLS_KEY: /opt/certs/localhost.key + REGISTRY_AUTH_TOKEN_REALM: http://localhost:8080/auth/realms/docker-test-realm/protocol/docker-v2/auth + REGISTRY_AUTH_TOKEN_SERVICE: docker-test-client + REGISTRY_AUTH_TOKEN_ISSUER: http://localhost:8080/auth/realms/docker-test-realm + REGISTRY_AUTH_TOKEN_ROOTCERTBUNDLE: /opt/certs/localhost_trust_chain.pem + volumes: + - ./data:/data:z + - ./certs:/opt/certs:z \ No newline at end of file diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/dockerClientTest/keycloak-docker-compose-yaml/sysconfig_docker b/testsuite/integration-arquillian/tests/base/src/test/resources/dockerClientTest/keycloak-docker-compose-yaml/sysconfig_docker new file mode 100644 index 0000000000..433cbc58dd --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/dockerClientTest/keycloak-docker-compose-yaml/sysconfig_docker @@ -0,0 +1,45 @@ +# /etc/sysconfig/docker + +# Modify these options if you want to change the way the docker daemon runs +OPTIONS='--selinux-enabled --log-driver=journald --signature-verification=false' +if [ -z "${DOCKER_CERT_PATH}" ]; then + DOCKER_CERT_PATH=/etc/docker +fi + +# If you want to add your own registry to be used for docker search and docker +# pull use the ADD_REGISTRY option to list a set of registries, each prepended +# with --add-registry flag. The first registry added will be the first registry +# searched. +# ADD_REGISTRY='--add-registry registry.access.redhat.com' + +# If you want to block registries from being used, uncomment the BLOCK_REGISTRY +# option and give it a set of registries, each prepended with --block-registry +# flag. For example adding docker.io will stop users from downloading images +# from docker.io +# BLOCK_REGISTRY='--block-registry' + +# If you have a registry secured with https but do not have proper certs +# distributed, you can tell docker to not look for full authorization by +# adding the registry to the INSECURE_REGISTRY line and uncommenting it. +INSECURE_REGISTRY='--insecure-registry registry.localdomain:5000' + +# On an SELinux system, if you remove the --selinux-enabled option, you +# also need to turn on the docker_transition_unconfined boolean. +# setsebool -P docker_transition_unconfined 1 + +# Location used for temporary files, such as those created by +# docker load and build operations. Default is /var/lib/docker/tmp +# Can be overriden by setting the following environment variable. +# DOCKER_TMPDIR=/var/tmp + +# Controls the /etc/cron.daily/docker-logrotate cron job status. +# To disable, uncomment the line below. +# LOGROTATE=false +# + +# docker-latest daemon can be used by starting the docker-latest unitfile. +# To use docker-latest client, uncomment below lines +#DOCKERBINARY=/usr/bin/docker-latest +#DOCKERDBINARY=/usr/bin/dockerd-latest +#DOCKER_CONTAINERD_BINARY=/usr/bin/docker-containerd-latest +#DOCKER_CONTAINERD_SHIM_BINARY=/usr/bin/docker-containerd-shim-latest diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/dockerClientTest/registry-config.txt b/testsuite/integration-arquillian/tests/base/src/test/resources/dockerClientTest/registry-config.txt new file mode 100644 index 0000000000..fe1af6104a --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/dockerClientTest/registry-config.txt @@ -0,0 +1,6 @@ +auth: + token: + realm: http://localhost:8080/auth/auth/realms/docker-test-realm/protocol/docker-v2/auth + service: docker-test-client + issuer: http://localhost:8080/auth/auth/realms/docker-test-realm + diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/dockerClientTest/variable-override.txt b/testsuite/integration-arquillian/tests/base/src/test/resources/dockerClientTest/variable-override.txt new file mode 100644 index 0000000000..7fd8485cce --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/dockerClientTest/variable-override.txt @@ -0,0 +1,4 @@ +-e REGISTRY_AUTH_TOKEN_REALM=http://localhost:8080/auth/auth/realms/docker-test-realm/protocol/docker-v2/auth \ +-e REGISTRY_AUTH_TOKEN_SERVICE: docker-test-client \ +-e REGISTRY_AUTH_TOKEN_ISSUER: http://localhost:8080/auth/auth/realms/docker-test-realm \ + diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/log4j.properties b/testsuite/integration-arquillian/tests/base/src/test/resources/log4j.properties index 167c611c8f..8f743734d1 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/resources/log4j.properties +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/log4j.properties @@ -18,24 +18,22 @@ log4j.rootLogger=info, keycloak log4j.appender.keycloak=org.apache.log4j.ConsoleAppender -log4j.appender.keycloak.layout=org.apache.log4j.PatternLayout -log4j.appender.keycloak.layout.ConversionPattern=%d{HH:mm:ss,SSS} %-5p [%c] %m%n - -log4j.appender.testsuite=org.apache.log4j.ConsoleAppender -log4j.appender.testsuite.layout=org.apache.log4j.PatternLayout -log4j.appender.testsuite.layout.ConversionPattern=%d{HH:mm:ss,SSS} %-5p [%C{1}] %m%n +log4j.appender.keycloak.layout=org.apache.log4j.EnhancedPatternLayout +keycloak.testsuite.logging.pattern=%d{HH:mm:ss,SSS} %-5p [%c] %m%n +log4j.appender.keycloak.layout.ConversionPattern=${keycloak.testsuite.logging.pattern} # Logging with "info" when running test from IDE, but disabled when running test with "mvn" . Both cases can be overriden by use system property "keycloak.logging.level" (eg. -Dkeycloak.logging.level=debug ) -keycloak.logging.level=info -log4j.logger.org.keycloak=${keycloak.logging.level} +log4j.logger.org.keycloak=${keycloak.logging.level:info} log4j.logger.org.jboss.resteasy.resteasy_jaxrs.i18n=off #log4j.logger.org.keycloak.keys.DefaultKeyManager=trace #log4j.logger.org.keycloak.services.managers.AuthenticationManager=trace -log4j.logger.org.keycloak.testsuite=debug, testsuite -log4j.additivity.org.keycloak.testsuite=false +keycloak.testsuite.logging.level=debug +log4j.logger.org.keycloak.testsuite=${keycloak.testsuite.logging.level} + +log4j.logger.org.keycloak.testsuite.arquillian.undertow.lb.SimpleUndertowLoadBalancer=info # Enable to view events # log4j.logger.org.keycloak.events=debug @@ -54,6 +52,13 @@ log4j.logger.org.keycloak.connections.jpa.DefaultJpaConnectionProviderFactory=de # log4j.logger.org.keycloak.connections.jpa.DefaultJpaConnectionProviderFactory=debug # log4j.logger.org.keycloak.migration.MigrationModelManager=debug +keycloak.infinispan.logging.level=info +log4j.logger.org.keycloak.cluster.infinispan=${keycloak.infinispan.logging.level} +log4j.logger.org.keycloak.connections.infinispan=${keycloak.infinispan.logging.level} +log4j.logger.org.keycloak.keys.infinispan=${keycloak.infinispan.logging.level} +log4j.logger.org.keycloak.models.cache.infinispan=${keycloak.infinispan.logging.level} +log4j.logger.org.keycloak.models.sessions.infinispan=${keycloak.infinispan.logging.level} + # Enable to view kerberos/spnego logging # log4j.logger.org.keycloak.broker.kerberos=trace diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/testrealm.json b/testsuite/integration-arquillian/tests/base/src/test/resources/testrealm.json index f4b118e54f..2ce6b3916e 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/resources/testrealm.json +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/testrealm.json @@ -147,6 +147,17 @@ "adminUrl": "http://localhost:8180/auth/realms/master/app/admin", "secret": "password" }, + { + "clientId": "root-url-client", + "enabled": true, + "rootUrl": "http://localhost:8180/foo/bar", + "adminUrl": "http://localhost:8180/foo/bar", + "baseUrl": "/baz", + "redirectUris": [ + "http://localhost:8180/foo/bar/*" + ], + "secret": "password" + }, { "clientId" : "test-app-scope", "enabled": true, diff --git a/testsuite/integration-arquillian/tests/other/adapters/jboss/as7/pom.xml b/testsuite/integration-arquillian/tests/other/adapters/jboss/as7/pom.xml index 6d9c2344b7..ebb0293957 100644 --- a/testsuite/integration-arquillian/tests/other/adapters/jboss/as7/pom.xml +++ b/testsuite/integration-arquillian/tests/other/adapters/jboss/as7/pom.xml @@ -24,7 +24,7 @@ org.keycloak.testsuite integration-arquillian-tests-adapters-jboss - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT integration-arquillian-tests-adapters-as7 diff --git a/testsuite/integration-arquillian/tests/other/adapters/jboss/eap/pom.xml b/testsuite/integration-arquillian/tests/other/adapters/jboss/eap/pom.xml index 201c085db0..6e06b837f4 100644 --- a/testsuite/integration-arquillian/tests/other/adapters/jboss/eap/pom.xml +++ b/testsuite/integration-arquillian/tests/other/adapters/jboss/eap/pom.xml @@ -24,7 +24,7 @@ org.keycloak.testsuite integration-arquillian-tests-adapters-jboss - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT integration-arquillian-tests-adapters-eap diff --git a/testsuite/integration-arquillian/tests/other/adapters/jboss/eap6-fuse/pom.xml b/testsuite/integration-arquillian/tests/other/adapters/jboss/eap6-fuse/pom.xml index 82f387b1b3..9f5a988b8f 100644 --- a/testsuite/integration-arquillian/tests/other/adapters/jboss/eap6-fuse/pom.xml +++ b/testsuite/integration-arquillian/tests/other/adapters/jboss/eap6-fuse/pom.xml @@ -24,7 +24,7 @@ org.keycloak.testsuite integration-arquillian-tests-adapters-jboss - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT integration-arquillian-tests-adapters-eap6-fuse diff --git a/testsuite/integration-arquillian/tests/other/adapters/jboss/eap6/pom.xml b/testsuite/integration-arquillian/tests/other/adapters/jboss/eap6/pom.xml index 9faeaf9847..16ae5fd3a2 100644 --- a/testsuite/integration-arquillian/tests/other/adapters/jboss/eap6/pom.xml +++ b/testsuite/integration-arquillian/tests/other/adapters/jboss/eap6/pom.xml @@ -24,7 +24,7 @@ org.keycloak.testsuite integration-arquillian-tests-adapters-jboss - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT integration-arquillian-tests-adapters-eap6 diff --git a/testsuite/integration-arquillian/tests/other/adapters/jboss/pom.xml b/testsuite/integration-arquillian/tests/other/adapters/jboss/pom.xml index 091aba26ae..45d6cd14c5 100644 --- a/testsuite/integration-arquillian/tests/other/adapters/jboss/pom.xml +++ b/testsuite/integration-arquillian/tests/other/adapters/jboss/pom.xml @@ -24,7 +24,7 @@ org.keycloak.testsuite integration-arquillian-tests-adapters - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT integration-arquillian-tests-adapters-jboss diff --git a/testsuite/integration-arquillian/tests/other/adapters/jboss/relative/eap/pom.xml b/testsuite/integration-arquillian/tests/other/adapters/jboss/relative/eap/pom.xml index 7c0fa8a5ff..7ef888ebc2 100644 --- a/testsuite/integration-arquillian/tests/other/adapters/jboss/relative/eap/pom.xml +++ b/testsuite/integration-arquillian/tests/other/adapters/jboss/relative/eap/pom.xml @@ -24,7 +24,7 @@ org.keycloak.testsuite integration-arquillian-tests-adapters-jboss-relative - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT integration-arquillian-tests-adapters-relative-eap diff --git a/testsuite/integration-arquillian/tests/other/adapters/jboss/relative/pom.xml b/testsuite/integration-arquillian/tests/other/adapters/jboss/relative/pom.xml index 0333a30435..306ee67b7f 100644 --- a/testsuite/integration-arquillian/tests/other/adapters/jboss/relative/pom.xml +++ b/testsuite/integration-arquillian/tests/other/adapters/jboss/relative/pom.xml @@ -24,7 +24,7 @@ org.keycloak.testsuite integration-arquillian-tests-adapters-jboss - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT pom diff --git a/testsuite/integration-arquillian/tests/other/adapters/jboss/relative/wildfly/pom.xml b/testsuite/integration-arquillian/tests/other/adapters/jboss/relative/wildfly/pom.xml index f943f8ba03..e838764dc5 100644 --- a/testsuite/integration-arquillian/tests/other/adapters/jboss/relative/wildfly/pom.xml +++ b/testsuite/integration-arquillian/tests/other/adapters/jboss/relative/wildfly/pom.xml @@ -24,7 +24,7 @@ org.keycloak.testsuite integration-arquillian-tests-adapters-jboss-relative - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT integration-arquillian-tests-adapters-relative-wildfly diff --git a/testsuite/integration-arquillian/tests/other/adapters/jboss/remote/pom.xml b/testsuite/integration-arquillian/tests/other/adapters/jboss/remote/pom.xml index 7e44ddd5c2..922bb867e5 100644 --- a/testsuite/integration-arquillian/tests/other/adapters/jboss/remote/pom.xml +++ b/testsuite/integration-arquillian/tests/other/adapters/jboss/remote/pom.xml @@ -24,7 +24,7 @@ org.keycloak.testsuite integration-arquillian-tests-adapters-jboss - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT integration-arquillian-tests-adapters-remote diff --git a/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly/pom.xml b/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly/pom.xml index afa7e8b9d2..3a6e545cda 100644 --- a/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly/pom.xml +++ b/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly/pom.xml @@ -24,7 +24,7 @@ org.keycloak.testsuite integration-arquillian-tests-adapters-jboss - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT integration-arquillian-tests-adapters-wildfly diff --git a/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly10/pom.xml b/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly10/pom.xml index 486da62ab3..62c011cf01 100644 --- a/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly10/pom.xml +++ b/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly10/pom.xml @@ -24,10 +24,10 @@ org.keycloak.testsuite integration-arquillian-tests-adapters-jboss - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT - integration-arquillian-tests-adapters-wildfly + integration-arquillian-tests-adapters-wildfly10 Adapter Tests - JBoss - Wildfly 10 diff --git a/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly8/pom.xml b/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly8/pom.xml index 299b01516c..ea362d4bbe 100644 --- a/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly8/pom.xml +++ b/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly8/pom.xml @@ -24,7 +24,7 @@ org.keycloak.testsuite integration-arquillian-tests-adapters-jboss - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT integration-arquillian-tests-adapters-wildfly8 diff --git a/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly9/pom.xml b/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly9/pom.xml index 454e364172..af851f2839 100644 --- a/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly9/pom.xml +++ b/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly9/pom.xml @@ -24,7 +24,7 @@ org.keycloak.testsuite integration-arquillian-tests-adapters-jboss - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT integration-arquillian-tests-adapters-wildfly9 diff --git a/testsuite/integration-arquillian/tests/other/adapters/karaf/fuse61/pom.xml b/testsuite/integration-arquillian/tests/other/adapters/karaf/fuse61/pom.xml index f332d235e9..a206f014ce 100644 --- a/testsuite/integration-arquillian/tests/other/adapters/karaf/fuse61/pom.xml +++ b/testsuite/integration-arquillian/tests/other/adapters/karaf/fuse61/pom.xml @@ -24,7 +24,7 @@ org.keycloak.testsuite integration-arquillian-tests-adapters-karaf - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT integration-arquillian-tests-adapters-fuse61 diff --git a/testsuite/integration-arquillian/tests/other/adapters/karaf/fuse62/pom.xml b/testsuite/integration-arquillian/tests/other/adapters/karaf/fuse62/pom.xml index 561eb4c0db..62e8607ffc 100644 --- a/testsuite/integration-arquillian/tests/other/adapters/karaf/fuse62/pom.xml +++ b/testsuite/integration-arquillian/tests/other/adapters/karaf/fuse62/pom.xml @@ -24,7 +24,7 @@ org.keycloak.testsuite integration-arquillian-tests-adapters-karaf - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT integration-arquillian-tests-adapters-fuse62 diff --git a/testsuite/integration-arquillian/tests/other/adapters/karaf/fuse63/pom.xml b/testsuite/integration-arquillian/tests/other/adapters/karaf/fuse63/pom.xml index 0a438fe377..bd0e11efb0 100644 --- a/testsuite/integration-arquillian/tests/other/adapters/karaf/fuse63/pom.xml +++ b/testsuite/integration-arquillian/tests/other/adapters/karaf/fuse63/pom.xml @@ -24,7 +24,7 @@ org.keycloak.testsuite integration-arquillian-tests-adapters-karaf - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT integration-arquillian-tests-adapters-fuse63 diff --git a/testsuite/integration-arquillian/tests/other/adapters/karaf/karaf3/pom.xml b/testsuite/integration-arquillian/tests/other/adapters/karaf/karaf3/pom.xml index 3af5010950..f86b72ca10 100644 --- a/testsuite/integration-arquillian/tests/other/adapters/karaf/karaf3/pom.xml +++ b/testsuite/integration-arquillian/tests/other/adapters/karaf/karaf3/pom.xml @@ -24,7 +24,7 @@ org.keycloak.testsuite integration-arquillian-tests-adapters-karaf - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT integration-arquillian-tests-adapters-karaf3 diff --git a/testsuite/integration-arquillian/tests/other/adapters/karaf/pom.xml b/testsuite/integration-arquillian/tests/other/adapters/karaf/pom.xml index fdd9523ea6..433e64212c 100644 --- a/testsuite/integration-arquillian/tests/other/adapters/karaf/pom.xml +++ b/testsuite/integration-arquillian/tests/other/adapters/karaf/pom.xml @@ -24,7 +24,7 @@ org.keycloak.testsuite integration-arquillian-tests-adapters - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT integration-arquillian-tests-adapters-karaf diff --git a/testsuite/integration-arquillian/tests/other/adapters/pom.xml b/testsuite/integration-arquillian/tests/other/adapters/pom.xml index 70533a902e..6cfc621f24 100644 --- a/testsuite/integration-arquillian/tests/other/adapters/pom.xml +++ b/testsuite/integration-arquillian/tests/other/adapters/pom.xml @@ -24,7 +24,7 @@ org.keycloak.testsuite integration-arquillian-tests-other - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT integration-arquillian-tests-adapters @@ -98,6 +98,8 @@ jboss karaf tomcat + was + wls diff --git a/testsuite/integration-arquillian/tests/other/adapters/tomcat/pom.xml b/testsuite/integration-arquillian/tests/other/adapters/tomcat/pom.xml index fd0b3a2cf1..d5fd8c875e 100644 --- a/testsuite/integration-arquillian/tests/other/adapters/tomcat/pom.xml +++ b/testsuite/integration-arquillian/tests/other/adapters/tomcat/pom.xml @@ -24,7 +24,7 @@ org.keycloak.testsuite integration-arquillian-tests-adapters - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT integration-arquillian-tests-adapters-tomcat diff --git a/testsuite/integration-arquillian/tests/other/adapters/tomcat/tomcat7/pom.xml b/testsuite/integration-arquillian/tests/other/adapters/tomcat/tomcat7/pom.xml index a8e55b8f80..502985408e 100644 --- a/testsuite/integration-arquillian/tests/other/adapters/tomcat/tomcat7/pom.xml +++ b/testsuite/integration-arquillian/tests/other/adapters/tomcat/tomcat7/pom.xml @@ -24,7 +24,7 @@ org.keycloak.testsuite integration-arquillian-tests-adapters-tomcat - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT integration-arquillian-tests-adapters-tomcat7 diff --git a/testsuite/integration-arquillian/tests/other/adapters/tomcat/tomcat8/pom.xml b/testsuite/integration-arquillian/tests/other/adapters/tomcat/tomcat8/pom.xml index 6ef1449175..32574f8e33 100644 --- a/testsuite/integration-arquillian/tests/other/adapters/tomcat/tomcat8/pom.xml +++ b/testsuite/integration-arquillian/tests/other/adapters/tomcat/tomcat8/pom.xml @@ -24,7 +24,7 @@ org.keycloak.testsuite integration-arquillian-tests-adapters-tomcat - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT integration-arquillian-tests-adapters-tomcat8 diff --git a/testsuite/integration-arquillian/tests/other/adapters/tomcat/tomcat9/pom.xml b/testsuite/integration-arquillian/tests/other/adapters/tomcat/tomcat9/pom.xml index 5268feb9db..c48763444c 100644 --- a/testsuite/integration-arquillian/tests/other/adapters/tomcat/tomcat9/pom.xml +++ b/testsuite/integration-arquillian/tests/other/adapters/tomcat/tomcat9/pom.xml @@ -24,7 +24,7 @@ org.keycloak.testsuite integration-arquillian-tests-adapters-tomcat - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT integration-arquillian-tests-adapters-tomcat9 diff --git a/testsuite/integration-arquillian/tests/other/adapters/was/README.md b/testsuite/integration-arquillian/tests/other/adapters/was/README.md new file mode 100644 index 0000000000..ae7afce903 --- /dev/null +++ b/testsuite/integration-arquillian/tests/other/adapters/was/README.md @@ -0,0 +1,19 @@ +# Keycloak Arquillian WebSphere AS Integration Testsuite + +- arquillian-was-remote-8.5-custom container is used for deploying artifacts to running WebSphere server +- arquillian-was-remote-8.5-custom is based on arquillian-was-remote-8.5 and solves some ibm dependency issues +- arquillian-was-remote-8.5-custom can be downloaded from this [repo](https://repository.jboss.org/nexus/content/repositories/jboss_releases_staging_profile-11801) +- more info about arquillian-was-remote-8.5-custom: + - There is the [artifact](https://github.com/vramik/arquillian-container-was/blob/custom/was-remote-8.5/pom.xml#L17) + - This is a [profile](https://github.com/vramik/arquillian-container-was/blob/custom/pom.xml#L108-L114) to activate + - To build `ws-dependencies` module it is required to specify `lib_location` property where directory `lib` is located. The `lib` has to contain `com.ibm.ws.admin.client_8.5.0.jar` and `com.ibm.ws.orb_8.5.0.jar` which are part of WebSphere AS installation + - see [pom.xml](https://github.com/vramik/arquillian-container-was/blob/custom/ws-dependencies/pom.xml) for more details + - note: to solve classpath conflicts the package javax/ws from within `com.ibm.ws.admin.client_8.5.0.jar` has to be removed + +## How to run tests + +1. start IBM WebSphere container with ibmjdk8 (tests expects that app-server runs on port 8280) +2. add the [repository](https://repository.jboss.org/nexus/content/repositories/jboss_releases_staging_profile-11801) to settings.xml +3. mvn -f keycloak/pom.xml -Pdistribution -DskipTests clean install +4. mvn -f keycloak/testsuite/integration-arquillian/pom.xml -Pauth-server-wildfly -DskipTests clean install +5. mvn -f keycloak/testsuite/integration-arquillian/tests/other/adapters/was/pom.xml -Pauth-server-wildfly,app-server-was clean install \ No newline at end of file diff --git a/testsuite/integration-arquillian/tests/other/adapters/was/common/xslt/arquillian.xsl b/testsuite/integration-arquillian/tests/other/adapters/was/common/xslt/arquillian.xsl new file mode 100644 index 0000000000..420a0fbfdb --- /dev/null +++ b/testsuite/integration-arquillian/tests/other/adapters/was/common/xslt/arquillian.xsl @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + true + localhost + 8880 + false + admin + org.jboss.arquillian.container.was.remote_8_5.WebSphereRemoteContainer + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/testsuite/integration-arquillian/tests/other/adapters/was/pom.xml b/testsuite/integration-arquillian/tests/other/adapters/was/pom.xml new file mode 100644 index 0000000000..c5b96a215c --- /dev/null +++ b/testsuite/integration-arquillian/tests/other/adapters/was/pom.xml @@ -0,0 +1,45 @@ + + + + + + 4.0.0 + + + org.keycloak.testsuite + integration-arquillian-tests-adapters + 3.3.0.CR1-SNAPSHOT + + + integration-arquillian-tests-adapters-was + + pom + + Adapter Tests - WAS + + + + app-server-was + + was8 + + + + + \ No newline at end of file diff --git a/testsuite/integration-arquillian/tests/other/adapters/was/was8/pom.xml b/testsuite/integration-arquillian/tests/other/adapters/was/was8/pom.xml new file mode 100644 index 0000000000..ad138d389d --- /dev/null +++ b/testsuite/integration-arquillian/tests/other/adapters/was/was8/pom.xml @@ -0,0 +1,50 @@ + + + + + + 4.0.0 + + + org.keycloak.testsuite + integration-arquillian-tests-adapters-was + 3.3.0.CR1-SNAPSHOT + + + integration-arquillian-tests-adapters-was8 + + Adapter Tests - WAS8 + + + ${project.parent.basedir}/common + was + remote + true + + + + + + org.jboss.arquillian.container + arquillian-was-remote-8.5-custom + 1.0.0.Final + + + + \ No newline at end of file diff --git a/testsuite/integration-arquillian/tests/other/adapters/was/was8/src/test/java/org/keycloak/testsuite/adapter/WASSAMLFilterAdapterTest.java b/testsuite/integration-arquillian/tests/other/adapters/was/was8/src/test/java/org/keycloak/testsuite/adapter/WASSAMLFilterAdapterTest.java new file mode 100644 index 0000000000..3c1fb197da --- /dev/null +++ b/testsuite/integration-arquillian/tests/other/adapters/was/was8/src/test/java/org/keycloak/testsuite/adapter/WASSAMLFilterAdapterTest.java @@ -0,0 +1,9 @@ +package org.keycloak.testsuite.adapter; + +import org.keycloak.testsuite.adapter.servlet.AbstractSAMLFilterServletAdapterTest; +import org.keycloak.testsuite.arquillian.annotation.AppServerContainer; + +@AppServerContainer("app-server-was") +public class WASSAMLFilterAdapterTest extends AbstractSAMLFilterServletAdapterTest { + +} diff --git a/testsuite/integration-arquillian/tests/other/adapters/wls/common/xslt/arquillian.xsl b/testsuite/integration-arquillian/tests/other/adapters/wls/common/xslt/arquillian.xsl new file mode 100644 index 0000000000..d34cc0c9e3 --- /dev/null +++ b/testsuite/integration-arquillian/tests/other/adapters/wls/common/xslt/arquillian.xsl @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + true + org.jboss.arquillian.container.wls.remote_12_1_2.WebLogicContainer + t3://localhost:8280/ + weblogic + weblogic1 + AdminServer + /home/jenkins/Oracle/Middleware/Oracle_Home/wlserver + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/testsuite/integration-arquillian/tests/other/adapters/wls/pom.xml b/testsuite/integration-arquillian/tests/other/adapters/wls/pom.xml new file mode 100644 index 0000000000..85e33bce93 --- /dev/null +++ b/testsuite/integration-arquillian/tests/other/adapters/wls/pom.xml @@ -0,0 +1,45 @@ + + + + + + 4.0.0 + + + org.keycloak.testsuite + integration-arquillian-tests-adapters + 3.3.0.CR1-SNAPSHOT + + + integration-arquillian-tests-adapters-wls + + pom + + Adapter Tests - WLS + + + + app-server-wls + + wls12 + + + + + \ No newline at end of file diff --git a/testsuite/integration-arquillian/tests/other/adapters/wls/wls12/pom.xml b/testsuite/integration-arquillian/tests/other/adapters/wls/wls12/pom.xml new file mode 100644 index 0000000000..afade15a9a --- /dev/null +++ b/testsuite/integration-arquillian/tests/other/adapters/wls/wls12/pom.xml @@ -0,0 +1,48 @@ + + + + + + 4.0.0 + + + org.keycloak.testsuite + integration-arquillian-tests-adapters-wls + 3.3.0.CR1-SNAPSHOT + + + integration-arquillian-tests-adapters-wls12 + + Adapter Tests - WLS12 + + + ${project.parent.basedir}/common + wls + remote + true + + + + + org.jboss.arquillian.container + arquillian-wls-remote-12.1.x + + + + \ No newline at end of file diff --git a/testsuite/integration-arquillian/tests/other/adapters/wls/wls12/src/test/java/org/keycloak/testsuite/adapter/WLSSAMLFilterAdapterTest.java b/testsuite/integration-arquillian/tests/other/adapters/wls/wls12/src/test/java/org/keycloak/testsuite/adapter/WLSSAMLFilterAdapterTest.java new file mode 100644 index 0000000000..ad2a92ff2c --- /dev/null +++ b/testsuite/integration-arquillian/tests/other/adapters/wls/wls12/src/test/java/org/keycloak/testsuite/adapter/WLSSAMLFilterAdapterTest.java @@ -0,0 +1,9 @@ +package org.keycloak.testsuite.adapter; + +import org.keycloak.testsuite.adapter.servlet.AbstractSAMLFilterServletAdapterTest; +import org.keycloak.testsuite.arquillian.annotation.AppServerContainer; + +@AppServerContainer("app-server-wls") +public class WLSSAMLFilterAdapterTest extends AbstractSAMLFilterServletAdapterTest { + +} diff --git a/testsuite/integration-arquillian/tests/other/clean-start/pom.xml b/testsuite/integration-arquillian/tests/other/clean-start/pom.xml index 23d2b618b5..72eee786cd 100644 --- a/testsuite/integration-arquillian/tests/other/clean-start/pom.xml +++ b/testsuite/integration-arquillian/tests/other/clean-start/pom.xml @@ -23,7 +23,7 @@ org.keycloak.testsuite integration-arquillian-tests-other - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT integration-arquillian-tests-smoke-clean-start diff --git a/testsuite/integration-arquillian/tests/other/console/pom.xml b/testsuite/integration-arquillian/tests/other/console/pom.xml index 482d482f65..003e24c939 100644 --- a/testsuite/integration-arquillian/tests/other/console/pom.xml +++ b/testsuite/integration-arquillian/tests/other/console/pom.xml @@ -24,7 +24,7 @@ org.keycloak.testsuite integration-arquillian-tests-other - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT integration-arquillian-tests-console diff --git a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/authentication/RequiredActions.java b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/authentication/RequiredActions.java index 7e4c29c9ff..09f5f1cb48 100644 --- a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/authentication/RequiredActions.java +++ b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/authentication/RequiredActions.java @@ -85,4 +85,60 @@ public class RequiredActions extends Authentication { public void setUpdateProfileDefaultAction(boolean value) { setRequiredActionDefaultValue(UPDATE_PROFILE, value); } + + private boolean getRequiredActionValue(String id) { + WaitUtils.waitUntilElement(requiredActionTable).is().present(); + + WebElement checkbox = requiredActionTable.findElement(By.id(id)); + + return checkbox.isSelected(); + } + + private boolean getRequiredActionEnabledValue(String id) { + return getRequiredActionValue(id + ENABLED); + } + + private boolean getRequiredActionDefaultValue(String id) { + return getRequiredActionValue(id + DEFAULT); + } + + public boolean getTermsAndConditionEnabled() { + return getRequiredActionEnabledValue(TERMS_AND_CONDITIONS); + } + + public boolean getTermsAndConditionDefaultAction() { + return getRequiredActionDefaultValue(TERMS_AND_CONDITIONS); + } + + public boolean getVerifyEmailEnabled() { + return getRequiredActionEnabledValue(VERIFY_EMAIL); + } + + public boolean getVerifyEmailDefaultAction() { + return getRequiredActionDefaultValue(VERIFY_EMAIL); + } + + public boolean getUpdatePasswordEnabled() { + return getRequiredActionEnabledValue(UPDATE_PASSWORD); + } + + public boolean getUpdatePasswordDefaultAction() { + return getRequiredActionDefaultValue(UPDATE_PASSWORD); + } + + public boolean getConfigureTotpEnabled() { + return getRequiredActionEnabledValue(CONFIGURE_TOTP); + } + + public boolean getConfigureTotpDefaultAction() { + return getRequiredActionDefaultValue(CONFIGURE_TOTP); + } + + public boolean getUpdateProfileEnabled() { + return getRequiredActionEnabledValue(UPDATE_PROFILE); + } + + public boolean getUpdateProfileDefaultAction() { + return getRequiredActionDefaultValue(UPDATE_PROFILE); + } } diff --git a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/permission/Permissions.java b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/permission/Permissions.java index a6c95276d8..de9c13c339 100644 --- a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/permission/Permissions.java +++ b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/permission/Permissions.java @@ -25,7 +25,9 @@ import org.keycloak.representations.idm.authorization.ResourcePermissionRepresen import org.keycloak.representations.idm.authorization.ScopePermissionRepresentation; import org.keycloak.testsuite.console.page.clients.authorization.policy.PolicyTypeUI; import org.keycloak.testsuite.page.Form; +import org.keycloak.testsuite.util.URLUtils; import org.keycloak.testsuite.util.WaitUtils; +import org.openqa.selenium.By; import org.openqa.selenium.WebElement; import org.openqa.selenium.support.FindBy; import org.openqa.selenium.support.ui.Select; @@ -58,11 +60,9 @@ public class Permissions extends Form { if ("resource".equals(type)) { resourcePermission.form().populate((ResourcePermissionRepresentation) expected); - resourcePermission.form().save(); return (P) resourcePermission; } else if ("scope".equals(type)) { scopePermission.form().populate((ScopePermissionRepresentation) expected); - scopePermission.form().save(); return (P) scopePermission; } @@ -73,7 +73,7 @@ public class Permissions extends Form { for (WebElement row : permissions().rows()) { PolicyRepresentation actual = permissions().toRepresentation(row); if (actual.getName().equalsIgnoreCase(name)) { - row.findElements(tagName("a")).get(0).click(); + URLUtils.navigateToUri(driver, row.findElements(tagName("a")).get(0).getAttribute("href"), true); WaitUtils.waitForPageToLoad(driver); String type = representation.getType(); @@ -92,7 +92,7 @@ public class Permissions extends Form { for (WebElement row : permissions().rows()) { PolicyRepresentation actual = permissions().toRepresentation(row); if (actual.getName().equalsIgnoreCase(name)) { - row.findElements(tagName("a")).get(0).click(); + URLUtils.navigateToUri(driver, row.findElements(tagName("a")).get(0).getAttribute("href"), true); WaitUtils.waitForPageToLoad(driver); String type = actual.getType(); if ("resource".equals(type)) { @@ -109,7 +109,7 @@ public class Permissions extends Form { for (WebElement row : permissions().rows()) { PolicyRepresentation actual = permissions().toRepresentation(row); if (actual.getName().equalsIgnoreCase(name)) { - row.findElements(tagName("a")).get(0).click(); + URLUtils.navigateToUri(driver, row.findElements(tagName("a")).get(0).getAttribute("href"), true); WaitUtils.waitForPageToLoad(driver); String type = actual.getType(); @@ -124,4 +124,15 @@ public class Permissions extends Form { } } } + + public void deleteFromList(String name) { + for (WebElement row : permissions().rows()) { + PolicyRepresentation actual = permissions().toRepresentation(row); + if (actual.getName().equalsIgnoreCase(name)) { + row.findElements(tagName("td")).get(4).click(); + driver.findElement(By.xpath(".//button[text()='Delete']")).click(); + return; + } + } + } } \ No newline at end of file diff --git a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/permission/ResourcePermissionForm.java b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/permission/ResourcePermissionForm.java index cf39523b21..e31ac2a43f 100644 --- a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/permission/ResourcePermissionForm.java +++ b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/permission/ResourcePermissionForm.java @@ -18,6 +18,7 @@ package org.keycloak.testsuite.console.page.clients.authorization.permission; import org.keycloak.representations.idm.authorization.DecisionStrategy; import org.keycloak.representations.idm.authorization.ResourcePermissionRepresentation; +import org.keycloak.testsuite.console.page.fragment.ModalDialog; import org.keycloak.testsuite.console.page.fragment.MultipleStringSelect2; import org.keycloak.testsuite.console.page.fragment.OnOffSwitch; import org.keycloak.testsuite.page.Form; @@ -48,8 +49,8 @@ public class ResourcePermissionForm extends Form { @FindBy(xpath = "//i[contains(@class,'pficon-delete')]") private WebElement deleteButton; - @FindBy(xpath = ACTIVE_DIV_XPATH + "/button[text()='Delete']") - private WebElement confirmDelete; + @FindBy(xpath = "//div[@class='modal-dialog']") + protected ModalDialog modalDialog; @FindBy(id = "s2id_policies") private MultipleStringSelect2 policySelect; @@ -78,7 +79,7 @@ public class ResourcePermissionForm extends Form { public void delete() { deleteButton.click(); - confirmDelete.click(); + modalDialog.confirmDeletion(); } public ResourcePermissionRepresentation toRepresentation() { diff --git a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/permission/ScopePermissionForm.java b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/permission/ScopePermissionForm.java index f16cd5c55f..deb7f0663b 100644 --- a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/permission/ScopePermissionForm.java +++ b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/permission/ScopePermissionForm.java @@ -21,6 +21,7 @@ import java.util.function.Function; import org.keycloak.representations.idm.authorization.DecisionStrategy; import org.keycloak.representations.idm.authorization.ScopePermissionRepresentation; +import org.keycloak.testsuite.console.page.fragment.ModalDialog; import org.keycloak.testsuite.console.page.fragment.MultipleStringSelect2; import org.keycloak.testsuite.console.page.fragment.SingleStringSelect2; import org.keycloak.testsuite.page.Form; @@ -45,8 +46,8 @@ public class ScopePermissionForm extends Form { @FindBy(xpath = "//i[contains(@class,'pficon-delete')]") private WebElement deleteButton; - @FindBy(xpath = ACTIVE_DIV_XPATH + "/button[text()='Delete']") - private WebElement confirmDelete; + @FindBy(xpath = "//div[@class='modal-dialog']") + protected ModalDialog modalDialog; @FindBy(id = "s2id_policies") private MultipleStringSelect2 policySelect; @@ -81,7 +82,7 @@ public class ScopePermissionForm extends Form { public void delete() { deleteButton.click(); - confirmDelete.click(); + modalDialog.confirmDeletion(); } public ScopePermissionRepresentation toRepresentation() { diff --git a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/AggregatePolicyForm.java b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/AggregatePolicyForm.java index 5e7170d6df..12c4289d59 100644 --- a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/AggregatePolicyForm.java +++ b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/AggregatePolicyForm.java @@ -20,6 +20,7 @@ import java.util.Set; import org.keycloak.representations.idm.authorization.AggregatePolicyRepresentation; import org.keycloak.representations.idm.authorization.Logic; +import org.keycloak.testsuite.console.page.fragment.ModalDialog; import org.keycloak.testsuite.console.page.fragment.MultipleStringSelect2; import org.keycloak.testsuite.page.Form; import org.openqa.selenium.WebElement; @@ -46,8 +47,8 @@ public class AggregatePolicyForm extends Form { @FindBy(id = "s2id_policies") private MultipleStringSelect2 policySelect; - @FindBy(xpath = ACTIVE_DIV_XPATH + "/button[text()='Delete']") - private WebElement confirmDelete; + @FindBy(xpath = "//div[@class='modal-dialog']") + protected ModalDialog modalDialog; public void populate(AggregatePolicyRepresentation expected) { setInputValue(name, expected.getName()); @@ -83,7 +84,7 @@ public class AggregatePolicyForm extends Form { public void delete() { deleteButton.click(); - confirmDelete.click(); + modalDialog.confirmDeletion(); } public AggregatePolicyRepresentation toRepresentation() { diff --git a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/ClientPolicyForm.java b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/ClientPolicyForm.java index 9095a3207d..cedaceeca8 100644 --- a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/ClientPolicyForm.java +++ b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/ClientPolicyForm.java @@ -25,6 +25,7 @@ import java.util.stream.Collectors; import org.keycloak.representations.idm.authorization.ClientPolicyRepresentation; import org.keycloak.representations.idm.authorization.Logic; +import org.keycloak.testsuite.console.page.fragment.ModalDialog; import org.keycloak.testsuite.console.page.fragment.MultipleStringSelect2; import org.keycloak.testsuite.page.Form; import org.openqa.selenium.By; @@ -52,8 +53,8 @@ public class ClientPolicyForm extends Form { @FindBy(id = "s2id_clients") private ClientSelect clientsInput; - @FindBy(xpath = ACTIVE_DIV_XPATH + "/button[text()='Delete']") - private WebElement confirmDelete; + @FindBy(xpath = "//div[@class='modal-dialog']") + protected ModalDialog modalDialog; public void populate(ClientPolicyRepresentation expected) { setInputValue(name, expected.getName()); @@ -67,7 +68,7 @@ public class ClientPolicyForm extends Form { public void delete() { deleteButton.click(); - confirmDelete.click(); + modalDialog.confirmDeletion(); } public ClientPolicyRepresentation toRepresentation() { diff --git a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/JSPolicyForm.java b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/JSPolicyForm.java index e83585b9bb..9c1c1eaa8f 100644 --- a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/JSPolicyForm.java +++ b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/JSPolicyForm.java @@ -18,6 +18,7 @@ package org.keycloak.testsuite.console.page.clients.authorization.policy; import org.keycloak.representations.idm.authorization.JSPolicyRepresentation; import org.keycloak.representations.idm.authorization.Logic; +import org.keycloak.testsuite.console.page.fragment.ModalDialog; import org.keycloak.testsuite.page.Form; import org.openqa.selenium.JavascriptExecutor; import org.openqa.selenium.WebElement; @@ -41,8 +42,8 @@ public class JSPolicyForm extends Form { @FindBy(xpath = "//i[contains(@class,'pficon-delete')]") private WebElement deleteButton; - @FindBy(xpath = ACTIVE_DIV_XPATH + "/button[text()='Delete']") - private WebElement confirmDelete; + @FindBy(xpath = "//div[@class='modal-dialog']") + protected ModalDialog modalDialog; public void populate(JSPolicyRepresentation expected) { setInputValue(name, expected.getName()); @@ -58,7 +59,7 @@ public class JSPolicyForm extends Form { public void delete() { deleteButton.click(); - confirmDelete.click(); + modalDialog.confirmDeletion(); } public JSPolicyRepresentation toRepresentation() { diff --git a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/Policies.java b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/Policies.java index 7be563e114..a42e12e07e 100644 --- a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/Policies.java +++ b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/Policies.java @@ -30,7 +30,9 @@ import org.keycloak.representations.idm.authorization.RulePolicyRepresentation; import org.keycloak.representations.idm.authorization.TimePolicyRepresentation; import org.keycloak.representations.idm.authorization.UserPolicyRepresentation; import org.keycloak.testsuite.page.Form; +import org.keycloak.testsuite.util.URLUtils; import org.keycloak.testsuite.util.WaitUtils; +import org.openqa.selenium.By; import org.openqa.selenium.WebElement; import org.openqa.selenium.support.FindBy; import org.openqa.selenium.support.ui.Select; @@ -81,31 +83,24 @@ public class Policies extends Form { if ("role".equals(type)) { rolePolicy.form().populate((RolePolicyRepresentation) expected); - rolePolicy.form().save(); return (P) rolePolicy; } else if ("user".equals(type)) { userPolicy.form().populate((UserPolicyRepresentation) expected); - userPolicy.form().save(); return (P) userPolicy; } else if ("aggregate".equals(type)) { aggregatePolicy.form().populate((AggregatePolicyRepresentation) expected); - aggregatePolicy.form().save(); return (P) aggregatePolicy; } else if ("js".equals(type)) { jsPolicy.form().populate((JSPolicyRepresentation) expected); - jsPolicy.form().save(); return (P) jsPolicy; } else if ("time".equals(type)) { timePolicy.form().populate((TimePolicyRepresentation) expected); - timePolicy.form().save(); return (P) timePolicy; } else if ("rules".equals(type)) { rulePolicy.form().populate((RulePolicyRepresentation) expected); - rulePolicy.form().save(); return (P) rulePolicy; } else if ("client".equals(type)) { clientPolicy.form().populate((ClientPolicyRepresentation) expected); - clientPolicy.form().save(); return (P) clientPolicy; } else if ("group".equals(type)) { groupPolicy.form().populate((GroupPolicyRepresentation) expected); @@ -120,7 +115,7 @@ public class Policies extends Form { for (WebElement row : policies().rows()) { PolicyRepresentation actual = policies().toRepresentation(row); if (actual.getName().equalsIgnoreCase(name)) { - row.findElements(tagName("a")).get(0).click(); + URLUtils.navigateToUri(driver, row.findElements(tagName("a")).get(0).getAttribute("href"), true); WaitUtils.waitForPageToLoad(driver); String type = representation.getType(); @@ -151,8 +146,7 @@ public class Policies extends Form { for (WebElement row : policies().rows()) { PolicyRepresentation actual = policies().toRepresentation(row); if (actual.getName().equalsIgnoreCase(name)) { - row.findElements(tagName("a")).get(0).click(); - WaitUtils.waitForPageToLoad(driver); + URLUtils.navigateToUri(driver, row.findElements(tagName("a")).get(0).getAttribute("href"), true); String type = actual.getType(); if ("role".equals(type)) { return (P) rolePolicy; @@ -180,8 +174,7 @@ public class Policies extends Form { for (WebElement row : policies().rows()) { PolicyRepresentation actual = policies().toRepresentation(row); if (actual.getName().equalsIgnoreCase(name)) { - row.findElements(tagName("a")).get(0).click(); - WaitUtils.waitForPageToLoad(driver); + URLUtils.navigateToUri(driver, row.findElements(tagName("a")).get(0).getAttribute("href"), true); String type = actual.getType(); @@ -207,4 +200,15 @@ public class Policies extends Form { } } } + + public void deleteFromList(String name) { + for (WebElement row : policies().rows()) { + PolicyRepresentation actual = policies().toRepresentation(row); + if (actual.getName().equalsIgnoreCase(name)) { + row.findElements(tagName("td")).get(4).click(); + driver.findElement(By.xpath(".//button[text()='Delete']")).click(); + return; + } + } + } } \ No newline at end of file diff --git a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/RolePolicyForm.java b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/RolePolicyForm.java index 8b6f114765..f917678b28 100644 --- a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/RolePolicyForm.java +++ b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/RolePolicyForm.java @@ -28,6 +28,7 @@ import java.util.stream.Collectors; import org.keycloak.representations.idm.authorization.Logic; import org.keycloak.representations.idm.authorization.RolePolicyRepresentation; import org.keycloak.testsuite.console.page.fragment.AbstractMultipleSelect2; +import org.keycloak.testsuite.console.page.fragment.ModalDialog; import org.keycloak.testsuite.page.Form; import org.openqa.selenium.By; import org.openqa.selenium.WebElement; @@ -60,8 +61,8 @@ public class RolePolicyForm extends Form { @FindBy(id = "s2id_clientRoles") private ClientRoleSelect clientRoleSelect; - @FindBy(xpath = ACTIVE_DIV_XPATH + "/button[text()='Delete']") - private WebElement confirmDelete; + @FindBy(xpath = "//div[@class='modal-dialog']") + protected ModalDialog modalDialog; public void populate(RolePolicyRepresentation expected) { setInputValue(name, expected.getName()); @@ -115,7 +116,7 @@ public class RolePolicyForm extends Form { public void delete() { deleteButton.click(); - confirmDelete.click(); + modalDialog.confirmDeletion(); } public RolePolicyRepresentation toRepresentation() { diff --git a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/RulePolicyForm.java b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/RulePolicyForm.java index 17b4d464cb..0ba43f14d6 100644 --- a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/RulePolicyForm.java +++ b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/RulePolicyForm.java @@ -18,6 +18,7 @@ package org.keycloak.testsuite.console.page.clients.authorization.policy; import org.keycloak.representations.idm.authorization.Logic; import org.keycloak.representations.idm.authorization.RulePolicyRepresentation; +import org.keycloak.testsuite.console.page.fragment.ModalDialog; import org.keycloak.testsuite.page.Form; import org.keycloak.testsuite.util.WaitUtils; import org.openqa.selenium.WebElement; @@ -62,8 +63,8 @@ public class RulePolicyForm extends Form { @FindBy(xpath = "//i[contains(@class,'pficon-delete')]") private WebElement deleteButton; - @FindBy(xpath = ACTIVE_DIV_XPATH + "/button[text()='Delete']") - private WebElement confirmDelete; + @FindBy(xpath = "//div[@class='modal-dialog']") + protected ModalDialog modalDialog; @FindBy(id = "resolveModule") private WebElement resolveModuleButton; @@ -92,7 +93,7 @@ public class RulePolicyForm extends Form { public void delete() { deleteButton.click(); - confirmDelete.click(); + modalDialog.confirmDeletion(); } public RulePolicyRepresentation toRepresentation() { diff --git a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/TimePolicyForm.java b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/TimePolicyForm.java index 5c31f3373f..47be24d8f6 100644 --- a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/TimePolicyForm.java +++ b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/TimePolicyForm.java @@ -18,6 +18,7 @@ package org.keycloak.testsuite.console.page.clients.authorization.policy; import org.keycloak.representations.idm.authorization.TimePolicyRepresentation; import org.keycloak.representations.idm.authorization.Logic; +import org.keycloak.testsuite.console.page.fragment.ModalDialog; import org.keycloak.testsuite.page.Form; import org.openqa.selenium.WebElement; import org.openqa.selenium.support.FindBy; @@ -77,8 +78,8 @@ public class TimePolicyForm extends Form { @FindBy(xpath = "//i[contains(@class,'pficon-delete')]") private WebElement deleteButton; - @FindBy(xpath = ACTIVE_DIV_XPATH + "/button[text()='Delete']") - private WebElement confirmDelete; + @FindBy(xpath = "//div[@class='modal-dialog']") + protected ModalDialog modalDialog; public void populate(TimePolicyRepresentation expected) { setInputValue(name, expected.getName()); @@ -102,7 +103,7 @@ public class TimePolicyForm extends Form { public void delete() { deleteButton.click(); - confirmDelete.click(); + modalDialog.confirmDeletion(); } public TimePolicyRepresentation toRepresentation() { diff --git a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/UserPolicyForm.java b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/UserPolicyForm.java index e403d1b8b2..ec24ace72a 100644 --- a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/UserPolicyForm.java +++ b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/UserPolicyForm.java @@ -25,6 +25,7 @@ import java.util.stream.Collectors; import org.keycloak.representations.idm.authorization.Logic; import org.keycloak.representations.idm.authorization.UserPolicyRepresentation; +import org.keycloak.testsuite.console.page.fragment.ModalDialog; import org.keycloak.testsuite.console.page.fragment.MultipleStringSelect2; import org.keycloak.testsuite.page.Form; import org.openqa.selenium.By; @@ -52,8 +53,8 @@ public class UserPolicyForm extends Form { @FindBy(id = "s2id_users") private UserSelect usersInput; - @FindBy(xpath = ACTIVE_DIV_XPATH + "/button[text()='Delete']") - private WebElement confirmDelete; + @FindBy(xpath = "//div[@class='modal-dialog']") + protected ModalDialog modalDialog; public void populate(UserPolicyRepresentation expected) { setInputValue(name, expected.getName()); @@ -67,7 +68,7 @@ public class UserPolicyForm extends Form { public void delete() { deleteButton.click(); - confirmDelete.click(); + modalDialog.confirmDeletion(); } public UserPolicyRepresentation toRepresentation() { diff --git a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/resource/ResourceForm.java b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/resource/ResourceForm.java index 8f4a66f9fb..c4d2b2b9db 100644 --- a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/resource/ResourceForm.java +++ b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/resource/ResourceForm.java @@ -23,6 +23,7 @@ import java.util.Set; import org.jboss.arquillian.graphene.fragment.Root; import org.keycloak.representations.idm.authorization.ResourceRepresentation; import org.keycloak.representations.idm.authorization.ScopeRepresentation; +import org.keycloak.testsuite.console.page.fragment.ModalDialog; import org.keycloak.testsuite.page.Form; import org.keycloak.testsuite.util.WaitUtils; import org.openqa.selenium.By; @@ -54,8 +55,8 @@ public class ResourceForm extends Form { @FindBy(xpath = "//i[contains(@class,'pficon-delete')]") private WebElement deleteButton; - @FindBy(xpath = ACTIVE_DIV_XPATH + "/button[text()='Delete']") - private WebElement confirmDelete; + @FindBy(xpath = "//div[@class='modal-dialog']") + protected ModalDialog modalDialog; @FindBy(id = "s2id_scopes") private ScopesInput scopesInput; @@ -94,7 +95,7 @@ public class ResourceForm extends Form { public void delete() { deleteButton.click(); - confirmDelete.click(); + modalDialog.confirmDeletion(); } public ResourceRepresentation toRepresentation() { diff --git a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/resource/Resources.java b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/resource/Resources.java index 280af3f2df..199be95091 100644 --- a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/resource/Resources.java +++ b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/resource/Resources.java @@ -21,7 +21,9 @@ import static org.openqa.selenium.By.tagName; import org.jboss.arquillian.graphene.page.Page; import org.keycloak.representations.idm.authorization.ResourceRepresentation; import org.keycloak.testsuite.page.Form; +import org.keycloak.testsuite.util.URLUtils; import org.keycloak.testsuite.util.WaitUtils; +import org.openqa.selenium.By; import org.openqa.selenium.WebElement; import org.openqa.selenium.support.FindBy; @@ -52,7 +54,7 @@ public class Resources extends Form { for (WebElement row : resources().rows()) { ResourceRepresentation actual = resources().toRepresentation(row); if (actual.getName().equalsIgnoreCase(name)) { - row.findElements(tagName("a")).get(0).click(); + URLUtils.navigateToUri(driver, row.findElements(tagName("a")).get(0).getAttribute("href"), true); WaitUtils.waitForPageToLoad(driver); resource.form().populate(representation); return; @@ -64,7 +66,7 @@ public class Resources extends Form { for (WebElement row : resources().rows()) { ResourceRepresentation actual = resources().toRepresentation(row); if (actual.getName().equalsIgnoreCase(name)) { - row.findElements(tagName("a")).get(0).click(); + URLUtils.navigateToUri(driver, row.findElements(tagName("a")).get(0).getAttribute("href"), true); WaitUtils.waitForPageToLoad(driver); resource.form().delete(); return; @@ -72,11 +74,22 @@ public class Resources extends Form { } } + public void deleteFromList(String name) { + for (WebElement row : resources().rows()) { + ResourceRepresentation actual = resources().toRepresentation(row); + if (actual.getName().equalsIgnoreCase(name)) { + row.findElements(tagName("td")).get(6).click(); + driver.findElement(By.xpath(".//button[text()='Delete']")).click(); + return; + } + } + } + public Resource name(String name) { for (WebElement row : resources().rows()) { ResourceRepresentation actual = resources().toRepresentation(row); if (actual.getName().equalsIgnoreCase(name)) { - row.findElements(tagName("a")).get(0).click(); + URLUtils.navigateToUri(driver, row.findElements(tagName("a")).get(0).getAttribute("href"), true); WaitUtils.waitForPageToLoad(driver); return resource; } diff --git a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/scope/ScopeForm.java b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/scope/ScopeForm.java index ed01a2b453..29ec514fc2 100644 --- a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/scope/ScopeForm.java +++ b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/scope/ScopeForm.java @@ -17,6 +17,7 @@ package org.keycloak.testsuite.console.page.clients.authorization.scope; import org.keycloak.representations.idm.authorization.ScopeRepresentation; +import org.keycloak.testsuite.console.page.fragment.ModalDialog; import org.keycloak.testsuite.page.Form; import org.openqa.selenium.WebElement; import org.openqa.selenium.support.FindBy; @@ -35,8 +36,8 @@ public class ScopeForm extends Form { @FindBy(xpath = "//i[contains(@class,'pficon-delete')]") private WebElement deleteButton; - @FindBy(xpath = ACTIVE_DIV_XPATH + "/button[text()='Delete']") - private WebElement confirmDelete; + @FindBy(xpath = "//div[@class='modal-dialog']") + protected ModalDialog modalDialog; public void populate(ScopeRepresentation expected) { setInputValue(name, expected.getName()); @@ -46,6 +47,6 @@ public class ScopeForm extends Form { public void delete() { deleteButton.click(); - confirmDelete.click(); + modalDialog.confirmDeletion(); } } \ No newline at end of file diff --git a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/scope/Scopes.java b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/scope/Scopes.java index a59869c7e9..3974e35fae 100644 --- a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/scope/Scopes.java +++ b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/scope/Scopes.java @@ -21,6 +21,8 @@ import static org.openqa.selenium.By.tagName; import org.jboss.arquillian.graphene.page.Page; import org.keycloak.representations.idm.authorization.ScopeRepresentation; import org.keycloak.testsuite.page.Form; +import org.keycloak.testsuite.util.URLUtils; +import org.openqa.selenium.By; import org.openqa.selenium.WebElement; import org.openqa.selenium.support.FindBy; @@ -51,7 +53,7 @@ public class Scopes extends Form { for (WebElement row : scopes().rows()) { ScopeRepresentation actual = scopes().toRepresentation(row); if (actual.getName().equalsIgnoreCase(name)) { - row.findElements(tagName("a")).get(0).click(); + URLUtils.navigateToUri(driver, row.findElements(tagName("a")).get(0).getAttribute("href"), true); scope.form().populate(representation); } } @@ -61,9 +63,19 @@ public class Scopes extends Form { for (WebElement row : scopes().rows()) { ScopeRepresentation actual = scopes().toRepresentation(row); if (actual.getName().equalsIgnoreCase(name)) { - row.findElements(tagName("a")).get(0).click(); + URLUtils.navigateToUri(driver, row.findElements(tagName("a")).get(0).getAttribute("href"), true); scope.form().delete(); } } } + + public void deleteFromList(String name) { + for (WebElement row : scopes().rows()) { + ScopeRepresentation actual = scopes().toRepresentation(row); + if (actual.getName().equalsIgnoreCase(name)) { + row.findElements(tagName("td")).get(3).click(); + driver.findElement(By.xpath(".//button[text()='Delete']")).click(); + } + } + } } \ No newline at end of file diff --git a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/users/Users.java b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/users/Users.java index 7e4f417ba8..4232289415 100644 --- a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/users/Users.java +++ b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/users/Users.java @@ -53,7 +53,7 @@ public class Users extends AdminConsoleRealm { public static final String IMPERSONATE = "Impersonate"; public static final String DELETE = "Delete"; - @FindBy(xpath = "//div[./h1[text()='Users']]/table") + @FindBy(id = "user-table") private UsersTable table; public UsersTable table() { diff --git a/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authentication/RequiredActionsTest.java b/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authentication/RequiredActionsTest.java index b8217f8499..b879e18322 100644 --- a/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authentication/RequiredActionsTest.java +++ b/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authentication/RequiredActionsTest.java @@ -21,8 +21,11 @@ import org.jboss.arquillian.graphene.page.Page; import org.junit.Before; import org.junit.Test; import org.keycloak.representations.idm.UserRepresentation; +import org.keycloak.testsuite.Assert; +import org.keycloak.testsuite.auth.page.AuthRealm; import org.keycloak.testsuite.auth.page.login.Registration; import org.keycloak.testsuite.console.AbstractConsoleTest; +import org.keycloak.testsuite.console.page.AdminConsoleRealm; import org.keycloak.testsuite.console.page.authentication.RequiredActions; import org.keycloak.testsuite.console.page.realm.LoginSettings; import org.openqa.selenium.By; @@ -71,6 +74,52 @@ public class RequiredActionsTest extends AbstractConsoleTest { driver.findElement(By.xpath("//div[@id='kc-header-wrapper' and text()[contains(.,'Terms and Conditions')]]")); } + @Test + public void defaultCheckboxUncheckableWhenEnabledIsFalse() { + requiredActionsPage.setTermsAndConditionEnabled(false); + Assert.assertFalse(requiredActionsPage.getTermsAndConditionEnabled()); + requiredActionsPage.setTermsAndConditionDefaultAction(true); + Assert.assertFalse(requiredActionsPage.getTermsAndConditionDefaultAction()); + } + + @Test + public void defaultCheckboxUncheckedWhenEnabledBecomesFalse() { + requiredActionsPage.setTermsAndConditionEnabled(true); + Assert.assertTrue(requiredActionsPage.getTermsAndConditionEnabled()); + requiredActionsPage.setTermsAndConditionDefaultAction(true); + Assert.assertTrue(requiredActionsPage.getTermsAndConditionDefaultAction()); + requiredActionsPage.setTermsAndConditionEnabled(false); + Assert.assertFalse(requiredActionsPage.getTermsAndConditionEnabled()); + Assert.assertFalse(requiredActionsPage.getTermsAndConditionDefaultAction()); + assertAlertSuccess(); + } + + @Test + public void defaultCheckboxKeepsValueWhenEnabledIsToggled() { + requiredActionsPage.setTermsAndConditionEnabled(true); + requiredActionsPage.setTermsAndConditionDefaultAction(false); + Assert.assertTrue(requiredActionsPage.getTermsAndConditionEnabled()); + Assert.assertFalse(requiredActionsPage.getTermsAndConditionDefaultAction()); + requiredActionsPage.setTermsAndConditionEnabled(false); + Assert.assertFalse(requiredActionsPage.getTermsAndConditionEnabled()); + Assert.assertFalse(requiredActionsPage.getTermsAndConditionDefaultAction()); + requiredActionsPage.setTermsAndConditionEnabled(true); + Assert.assertTrue(requiredActionsPage.getTermsAndConditionEnabled()); + Assert.assertFalse(requiredActionsPage.getTermsAndConditionDefaultAction()); + + requiredActionsPage.setTermsAndConditionDefaultAction(true); + Assert.assertTrue(requiredActionsPage.getTermsAndConditionEnabled()); + Assert.assertTrue(requiredActionsPage.getTermsAndConditionDefaultAction()); + requiredActionsPage.setTermsAndConditionEnabled(false); + Assert.assertFalse(requiredActionsPage.getTermsAndConditionEnabled()); + Assert.assertFalse(requiredActionsPage.getTermsAndConditionDefaultAction()); + requiredActionsPage.setTermsAndConditionEnabled(true); + Assert.assertTrue(requiredActionsPage.getTermsAndConditionEnabled()); + Assert.assertTrue(requiredActionsPage.getTermsAndConditionDefaultAction()); + + assertAlertSuccess(); + } + @Test public void configureTotpDefaultActionTest() { requiredActionsPage.setConfigureTotpDefaultAction(true); diff --git a/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authorization/AggregatePolicyManagementTest.java b/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authorization/AggregatePolicyManagementTest.java index f1bba0d6eb..be2a984c25 100644 --- a/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authorization/AggregatePolicyManagementTest.java +++ b/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authorization/AggregatePolicyManagementTest.java @@ -122,6 +122,22 @@ public class AggregatePolicyManagementTest extends AbstractAuthorizationSettings assertNull(authorizationPage.authorizationTabs().policies().policies().findByName(expected.getName())); } + @Test + public void testDeleteFromList() throws InterruptedException { + authorizationPage.navigateTo(); + AggregatePolicyRepresentation expected = new AggregatePolicyRepresentation(); + + expected.setName("Test Delete Aggregate Policy"); + expected.setDescription("description"); + expected.addPolicy("Policy C"); + + expected = createPolicy(expected); + authorizationPage.navigateTo(); + authorizationPage.authorizationTabs().policies().deleteFromList(expected.getName()); + authorizationPage.navigateTo(); + assertNull(authorizationPage.authorizationTabs().policies().policies().findByName(expected.getName())); + } + private AggregatePolicyRepresentation createPolicy(AggregatePolicyRepresentation expected) { AggregatePolicy policy = authorizationPage.authorizationTabs().policies().create(expected); assertAlertSuccess(); diff --git a/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authorization/ClientPolicyManagementTest.java b/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authorization/ClientPolicyManagementTest.java index 2c95b83896..04e982606d 100644 --- a/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authorization/ClientPolicyManagementTest.java +++ b/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authorization/ClientPolicyManagementTest.java @@ -76,7 +76,7 @@ public class ClientPolicyManagementTest extends AbstractAuthorizationSettingsTes } @Test - public void testDeletePolicy() throws InterruptedException { + public void testDelete() throws InterruptedException { authorizationPage.navigateTo(); ClientPolicyRepresentation expected = new ClientPolicyRepresentation(); @@ -92,6 +92,22 @@ public class ClientPolicyManagementTest extends AbstractAuthorizationSettingsTes assertNull(authorizationPage.authorizationTabs().policies().policies().findByName(expected.getName())); } + @Test + public void testDeleteFromList() throws InterruptedException { + authorizationPage.navigateTo(); + ClientPolicyRepresentation expected = new ClientPolicyRepresentation(); + + expected.setName("Test Client Policy"); + expected.setDescription("description"); + expected.addClient("client c"); + + expected = createPolicy(expected); + authorizationPage.navigateTo(); + authorizationPage.authorizationTabs().policies().deleteFromList(expected.getName()); + authorizationPage.navigateTo(); + assertNull(authorizationPage.authorizationTabs().policies().policies().findByName(expected.getName())); + } + private ClientPolicyRepresentation createPolicy(ClientPolicyRepresentation expected) { ClientPolicy policy = authorizationPage.authorizationTabs().policies().create(expected); assertAlertSuccess(); diff --git a/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authorization/GroupPolicyManagementTest.java b/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authorization/GroupPolicyManagementTest.java index e8b05bf45b..91c86f96d6 100644 --- a/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authorization/GroupPolicyManagementTest.java +++ b/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authorization/GroupPolicyManagementTest.java @@ -135,6 +135,25 @@ public class GroupPolicyManagementTest extends AbstractAuthorizationSettingsTest assertNull(authorizationPage.authorizationTabs().policies().policies().findByName(expected.getName())); } + @Test + public void testDeleteFromList() throws InterruptedException { + authorizationPage.navigateTo(); + GroupPolicyRepresentation expected = new GroupPolicyRepresentation(); + + expected.setName("Test Delete Group Policy"); + expected.setDescription("description"); + expected.setGroupsClaim("groups"); + expected.addGroupPath("/Group A", true); + expected.addGroupPath("/Group A/Group B/Group D"); + expected.addGroupPath("Group F"); + + expected = createPolicy(expected); + authorizationPage.navigateTo(); + authorizationPage.authorizationTabs().policies().deleteFromList(expected.getName()); + authorizationPage.navigateTo(); + assertNull(authorizationPage.authorizationTabs().policies().policies().findByName(expected.getName())); + } + private GroupPolicyRepresentation createPolicy(GroupPolicyRepresentation expected) { GroupPolicy policy = authorizationPage.authorizationTabs().policies().create(expected); assertAlertSuccess(); diff --git a/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authorization/JSPolicyManagementTest.java b/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authorization/JSPolicyManagementTest.java index 0b9113c9b3..6da809c072 100644 --- a/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authorization/JSPolicyManagementTest.java +++ b/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authorization/JSPolicyManagementTest.java @@ -74,6 +74,22 @@ public class JSPolicyManagementTest extends AbstractAuthorizationSettingsTest { assertNull(authorizationPage.authorizationTabs().policies().policies().findByName(expected.getName())); } + @Test + public void testDeleteFromList() throws InterruptedException { + authorizationPage.navigateTo(); + JSPolicyRepresentation expected = new JSPolicyRepresentation(); + + expected.setName("Test JS Policy"); + expected.setDescription("description"); + expected.setCode("$evaluation.deny();"); + + expected = createPolicy(expected); + authorizationPage.navigateTo(); + authorizationPage.authorizationTabs().policies().deleteFromList(expected.getName()); + authorizationPage.navigateTo(); + assertNull(authorizationPage.authorizationTabs().policies().policies().findByName(expected.getName())); + } + private JSPolicyRepresentation createPolicy(JSPolicyRepresentation expected) { JSPolicy policy = authorizationPage.authorizationTabs().policies().create(expected); assertAlertSuccess(); diff --git a/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authorization/ResourceManagementTest.java b/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authorization/ResourceManagementTest.java index 75a479ad7b..3d29c03f24 100644 --- a/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authorization/ResourceManagementTest.java +++ b/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authorization/ResourceManagementTest.java @@ -72,7 +72,7 @@ public class ResourceManagementTest extends AbstractAuthorizationSettingsTest { } @Test - public void testDelete() { + public void testDeleteFromDetails() { ResourceRepresentation expected = createResource(); authorizationPage.navigateTo(); authorizationPage.authorizationTabs().resources().delete(expected.getName()); @@ -80,6 +80,15 @@ public class ResourceManagementTest extends AbstractAuthorizationSettingsTest { assertNull(authorizationPage.authorizationTabs().resources().resources().findByName(expected.getName())); } + @Test + public void testDeleteFromList() { + ResourceRepresentation expected = createResource(); + authorizationPage.navigateTo(); + authorizationPage.authorizationTabs().resources().deleteFromList(expected.getName()); + authorizationPage.navigateTo(); + assertNull(authorizationPage.authorizationTabs().resources().resources().findByName(expected.getName())); + } + private ResourceRepresentation createResource() { ResourceRepresentation expected = new ResourceRepresentation(); diff --git a/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authorization/ResourcePermissionManagementTest.java b/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authorization/ResourcePermissionManagementTest.java index 4ff011a6d0..f6a967e320 100644 --- a/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authorization/ResourcePermissionManagementTest.java +++ b/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authorization/ResourcePermissionManagementTest.java @@ -165,6 +165,23 @@ public class ResourcePermissionManagementTest extends AbstractAuthorizationSetti assertNull(authorizationPage.authorizationTabs().permissions().permissions().findByName(expected.getName())); } + @Test + public void testDeleteFromList() throws InterruptedException { + authorizationPage.navigateTo(); + ResourcePermissionRepresentation expected = new ResourcePermissionRepresentation(); + + expected.setName("Test Delete Resource Permission"); + expected.setDescription("description"); + expected.addResource("Resource B"); + expected.addPolicy("Policy C"); + + expected = createPermission(expected); + authorizationPage.navigateTo(); + authorizationPage.authorizationTabs().permissions().deleteFromList(expected.getName()); + authorizationPage.navigateTo(); + assertNull(authorizationPage.authorizationTabs().permissions().permissions().findByName(expected.getName())); + } + private ResourcePermissionRepresentation createPermission(ResourcePermissionRepresentation expected) { ResourcePermission policy = authorizationPage.authorizationTabs().permissions().create(expected); assertAlertSuccess(); diff --git a/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authorization/RolePolicyManagementTest.java b/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authorization/RolePolicyManagementTest.java index 44e4f70919..e8794ccee9 100644 --- a/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authorization/RolePolicyManagementTest.java +++ b/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authorization/RolePolicyManagementTest.java @@ -208,6 +208,24 @@ public class RolePolicyManagementTest extends AbstractAuthorizationSettingsTest assertNull(authorizationPage.authorizationTabs().policies().policies().findByName(expected.getName())); } + @Test + public void testDeleteFromList() throws InterruptedException { + authorizationPage.navigateTo(); + RolePolicyRepresentation expected = new RolePolicyRepresentation(); + + expected.setName("Test Delete Role Policy"); + expected.setDescription("description"); + expected.addRole("Realm Role A"); + expected.addRole("Realm Role B"); + expected.addRole("Realm Role C"); + + expected = createPolicy(expected); + authorizationPage.navigateTo(); + authorizationPage.authorizationTabs().policies().deleteFromList(expected.getName()); + authorizationPage.navigateTo(); + assertNull(authorizationPage.authorizationTabs().policies().policies().findByName(expected.getName())); + } + private RolePolicyRepresentation createPolicy(RolePolicyRepresentation expected) { RolePolicy policy = authorizationPage.authorizationTabs().policies().create(expected); assertAlertSuccess(); diff --git a/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authorization/RulePolicyManagementTest.java b/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authorization/RulePolicyManagementTest.java index 09fb47a85c..a1fbb60b6e 100644 --- a/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authorization/RulePolicyManagementTest.java +++ b/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authorization/RulePolicyManagementTest.java @@ -71,6 +71,18 @@ public class RulePolicyManagementTest extends AbstractAuthorizationSettingsTest assertNull(authorizationPage.authorizationTabs().policies().policies().findByName(expected.getName())); } + @Test + public void testDeleteFromList() throws InterruptedException { + authorizationPage.navigateTo(); + RulePolicyRepresentation expected =createDefaultRepresentation("Delete Rule Policy"); + + expected = createPolicy(expected); + authorizationPage.navigateTo(); + authorizationPage.authorizationTabs().policies().deleteFromList(expected.getName()); + authorizationPage.navigateTo(); + assertNull(authorizationPage.authorizationTabs().policies().policies().findByName(expected.getName())); + } + private RulePolicyRepresentation createDefaultRepresentation(String name) { RulePolicyRepresentation expected = new RulePolicyRepresentation(); diff --git a/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authorization/ScopeManagementTest.java b/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authorization/ScopeManagementTest.java index 84a5c42766..9bd5738c25 100644 --- a/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authorization/ScopeManagementTest.java +++ b/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authorization/ScopeManagementTest.java @@ -49,6 +49,15 @@ public class ScopeManagementTest extends AbstractAuthorizationSettingsTest { assertNull(authorizationPage.authorizationTabs().scopes().scopes().findByName(expected.getName())); } + @Test + public void testDeleteFromList() { + ScopeRepresentation expected = createScope(); + authorizationPage.navigateTo(); + authorizationPage.authorizationTabs().scopes().deleteFromList(expected.getName()); + authorizationPage.navigateTo(); + assertNull(authorizationPage.authorizationTabs().scopes().scopes().findByName(expected.getName())); + } + private ScopeRepresentation createScope() { ScopeRepresentation expected = new ScopeRepresentation(); diff --git a/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authorization/ScopePermissionManagementTest.java b/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authorization/ScopePermissionManagementTest.java index 3dfd0c87ef..e755335071 100644 --- a/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authorization/ScopePermissionManagementTest.java +++ b/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authorization/ScopePermissionManagementTest.java @@ -166,6 +166,23 @@ public class ScopePermissionManagementTest extends AbstractAuthorizationSettings assertNull(authorizationPage.authorizationTabs().permissions().permissions().findByName(expected.getName())); } + @Test + public void testDeleteFromList() throws InterruptedException { + authorizationPage.navigateTo(); + ScopePermissionRepresentation expected = new ScopePermissionRepresentation(); + + expected.setName("Test Delete Scope Permission"); + expected.setDescription("description"); + expected.addScope("Scope C"); + expected.addPolicy("Policy C"); + + expected = createPermission(expected); + authorizationPage.navigateTo(); + authorizationPage.authorizationTabs().permissions().deleteFromList(expected.getName()); + authorizationPage.navigateTo(); + assertNull(authorizationPage.authorizationTabs().permissions().permissions().findByName(expected.getName())); + } + private ScopePermissionRepresentation createPermission(ScopePermissionRepresentation expected) { ScopePermission policy = authorizationPage.authorizationTabs().permissions().create(expected); assertAlertSuccess(); diff --git a/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authorization/TimePolicyManagementTest.java b/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authorization/TimePolicyManagementTest.java index 6242c77be6..ed0165d72f 100644 --- a/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authorization/TimePolicyManagementTest.java +++ b/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authorization/TimePolicyManagementTest.java @@ -109,6 +109,33 @@ public class TimePolicyManagementTest extends AbstractAuthorizationSettingsTest assertNull(authorizationPage.authorizationTabs().policies().policies().findByName(expected.getName())); } + @Test + public void testDeleteFromList() throws InterruptedException { + authorizationPage.navigateTo(); + TimePolicyRepresentation expected = new TimePolicyRepresentation(); + + expected.setName("Test Time Policy"); + expected.setDescription("description"); + expected.setNotBefore("2017-01-01 00:00:00"); + expected.setNotBefore("2018-01-01 00:00:00"); + expected.setDayMonth("1"); + expected.setDayMonthEnd("2"); + expected.setMonth("3"); + expected.setMonthEnd("4"); + expected.setYear("5"); + expected.setYearEnd("6"); + expected.setHour("7"); + expected.setHourEnd("8"); + expected.setMinute("9"); + expected.setMinuteEnd("10"); + + expected = createPolicy(expected); + authorizationPage.navigateTo(); + authorizationPage.authorizationTabs().policies().deleteFromList(expected.getName()); + authorizationPage.navigateTo(); + assertNull(authorizationPage.authorizationTabs().policies().policies().findByName(expected.getName())); + } + private TimePolicyRepresentation createPolicy(TimePolicyRepresentation expected) { TimePolicy policy = authorizationPage.authorizationTabs().policies().create(expected); assertAlertSuccess(); diff --git a/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authorization/UserPolicyManagementTest.java b/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authorization/UserPolicyManagementTest.java index ed19bc5556..7e8c4837a9 100644 --- a/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authorization/UserPolicyManagementTest.java +++ b/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authorization/UserPolicyManagementTest.java @@ -76,7 +76,7 @@ public class UserPolicyManagementTest extends AbstractAuthorizationSettingsTest } @Test - public void testDeletePolicy() throws InterruptedException { + public void testDelete() throws InterruptedException { authorizationPage.navigateTo(); UserPolicyRepresentation expected = new UserPolicyRepresentation(); @@ -92,6 +92,22 @@ public class UserPolicyManagementTest extends AbstractAuthorizationSettingsTest assertNull(authorizationPage.authorizationTabs().policies().policies().findByName(expected.getName())); } + @Test + public void testDeleteFromList() throws InterruptedException { + authorizationPage.navigateTo(); + UserPolicyRepresentation expected = new UserPolicyRepresentation(); + + expected.setName("Test User Policy"); + expected.setDescription("description"); + expected.addUser("user c"); + + expected = createPolicy(expected); + authorizationPage.navigateTo(); + authorizationPage.authorizationTabs().policies().deleteFromList(expected.getName()); + authorizationPage.navigateTo(); + assertNull(authorizationPage.authorizationTabs().policies().policies().findByName(expected.getName())); + } + private UserPolicyRepresentation createPolicy(UserPolicyRepresentation expected) { UserPolicy policy = authorizationPage.authorizationTabs().policies().create(expected); assertAlertSuccess(); diff --git a/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/clients/ClientMappersOIDCTest.java b/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/clients/ClientMappersOIDCTest.java index 0d8e6b2cd8..eaa45dedad 100644 --- a/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/clients/ClientMappersOIDCTest.java +++ b/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/clients/ClientMappersOIDCTest.java @@ -23,6 +23,7 @@ package org.keycloak.testsuite.console.clients; import org.jboss.arquillian.graphene.page.Page; import org.junit.Before; +import org.junit.Ignore; import org.junit.Test; import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.ProtocolMapperRepresentation; @@ -92,7 +93,6 @@ public class ClientMappersOIDCTest extends AbstractClientTest { assertEquals("oidc-hardcoded-role-mapper", found.getProtocolMapper()); Map config = found.getConfig(); - assertEquals(1, config.size()); assertEquals("offline_access", config.get("role")); //edit @@ -164,8 +164,6 @@ public class ClientMappersOIDCTest extends AbstractClientTest { assertEquals("oidc-usersessionmodel-note-mapper", found.getProtocolMapper()); Map config = found.getConfig(); - assertNull(config.get("id.token.claim")); - assertNull(config.get("access.token.claim")); assertEquals("claim name", config.get("claim.name")); assertEquals("session note", config.get("user.session.note")); assertEquals("int", config.get("jsonType.label")); diff --git a/testsuite/integration-arquillian/tests/other/jpa-performance/pom.xml b/testsuite/integration-arquillian/tests/other/jpa-performance/pom.xml index 83fda9b493..75e4bc7a1c 100644 --- a/testsuite/integration-arquillian/tests/other/jpa-performance/pom.xml +++ b/testsuite/integration-arquillian/tests/other/jpa-performance/pom.xml @@ -24,7 +24,7 @@ org.keycloak.testsuite integration-arquillian-tests-other - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT integration-arquillian-tests-jpa-performance diff --git a/testsuite/integration-arquillian/tests/other/mod_auth_mellon/pom.xml b/testsuite/integration-arquillian/tests/other/mod_auth_mellon/pom.xml index 933b6537e0..16e5394c24 100644 --- a/testsuite/integration-arquillian/tests/other/mod_auth_mellon/pom.xml +++ b/testsuite/integration-arquillian/tests/other/mod_auth_mellon/pom.xml @@ -24,7 +24,7 @@ org.keycloak.testsuite integration-arquillian-tests-other - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT integration-arquillian-tests-other-mod_auth_mellon diff --git a/testsuite/integration-arquillian/tests/other/nodejs_adapter/pom.xml b/testsuite/integration-arquillian/tests/other/nodejs_adapter/pom.xml index 9270e51904..828006c55b 100644 --- a/testsuite/integration-arquillian/tests/other/nodejs_adapter/pom.xml +++ b/testsuite/integration-arquillian/tests/other/nodejs_adapter/pom.xml @@ -24,7 +24,7 @@ org.keycloak.testsuite integration-arquillian-tests-other - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT integration-arquillian-tests-nodejs-adapter diff --git a/testsuite/integration-arquillian/tests/other/pom.xml b/testsuite/integration-arquillian/tests/other/pom.xml index dce7878f4c..f406421e96 100644 --- a/testsuite/integration-arquillian/tests/other/pom.xml +++ b/testsuite/integration-arquillian/tests/other/pom.xml @@ -24,7 +24,7 @@ org.keycloak.testsuite integration-arquillian-tests - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT integration-arquillian-tests-other @@ -39,6 +39,7 @@ adapters sssd + springboot-tests diff --git a/testsuite/integration-arquillian/tests/other/server-config-migration/pom.xml b/testsuite/integration-arquillian/tests/other/server-config-migration/pom.xml index 4e578444f1..428d254f14 100644 --- a/testsuite/integration-arquillian/tests/other/server-config-migration/pom.xml +++ b/testsuite/integration-arquillian/tests/other/server-config-migration/pom.xml @@ -24,7 +24,7 @@ org.keycloak.testsuite integration-arquillian-tests-other - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../pom.xml diff --git a/testsuite/integration-arquillian/tests/other/springboot-tests/pom.xml b/testsuite/integration-arquillian/tests/other/springboot-tests/pom.xml new file mode 100644 index 0000000000..90a997f344 --- /dev/null +++ b/testsuite/integration-arquillian/tests/other/springboot-tests/pom.xml @@ -0,0 +1,89 @@ + + + + integration-arquillian-tests-other + org.keycloak.testsuite + 3.3.0.CR1-SNAPSHOT + + 4.0.0 + + integration-arquillian-tests-springboot + + + **/springboot/**/*Test.java + + tomcat + + + + + org.keycloak + keycloak-test-helper + ${project.version} + + + + + + + + maven-surefire-plugin + + + ${exclude.springboot} + + + + + + + + + test-springboot + + - + + + + + + com.bazaarvoice.maven.plugins + process-exec-maven-plugin + 0.7 + + + spring-boot-application-process + generate-test-resources + + start + + + springboot + ../../../../test-apps/spring-boot-adapter + + mvn + spring-boot:run + -Dkeycloak.version=${project.version} + -Pspring-boot-adapter-${adapter.container} + + + + + + kill-processes + post-integration-test + + stop-all + + + + + + + + + + + \ No newline at end of file diff --git a/testsuite/integration-arquillian/tests/other/springboot-tests/src/main/java/org/keycloak/testsuite/springboot/SpringAdminPage.java b/testsuite/integration-arquillian/tests/other/springboot-tests/src/main/java/org/keycloak/testsuite/springboot/SpringAdminPage.java new file mode 100644 index 0000000000..8ce5e75688 --- /dev/null +++ b/testsuite/integration-arquillian/tests/other/springboot-tests/src/main/java/org/keycloak/testsuite/springboot/SpringAdminPage.java @@ -0,0 +1,22 @@ +package org.keycloak.testsuite.springboot; + +import org.keycloak.testsuite.pages.AbstractPage; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.support.FindBy; + +public class SpringAdminPage extends AbstractPage { + + @FindBy(className = "test") + private WebElement testDiv; + + + @Override + public boolean isCurrent() { + return driver.getTitle().equalsIgnoreCase("springboot admin page"); + } + + @Override + public void open() throws Exception { + + } +} diff --git a/testsuite/integration-arquillian/tests/other/springboot-tests/src/main/java/org/keycloak/testsuite/springboot/SpringApplicationPage.java b/testsuite/integration-arquillian/tests/other/springboot-tests/src/main/java/org/keycloak/testsuite/springboot/SpringApplicationPage.java new file mode 100644 index 0000000000..9442cd3769 --- /dev/null +++ b/testsuite/integration-arquillian/tests/other/springboot-tests/src/main/java/org/keycloak/testsuite/springboot/SpringApplicationPage.java @@ -0,0 +1,40 @@ +package org.keycloak.testsuite.springboot; + +import org.keycloak.testsuite.pages.AbstractPage; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.support.FindBy; + +public class SpringApplicationPage extends AbstractPage { + + @FindBy(className = "test") + private WebElement testDiv; + + @FindBy(className = "adminlink") + private WebElement adminLink; + + private String title; + + public SpringApplicationPage() { + super(); + + title = "springboot test page"; + } + + public String getTitle() { + return title; + } + + @Override + public boolean isCurrent() { + return driver.getTitle().equalsIgnoreCase(title); + } + + @Override + public void open() throws Exception { + + } + + public void goAdmin() { + adminLink.click(); + } +} diff --git a/testsuite/integration-arquillian/tests/other/springboot-tests/src/main/java/org/keycloak/testsuite/springboot/TokenPage.java b/testsuite/integration-arquillian/tests/other/springboot-tests/src/main/java/org/keycloak/testsuite/springboot/TokenPage.java new file mode 100644 index 0000000000..7fc79618a7 --- /dev/null +++ b/testsuite/integration-arquillian/tests/other/springboot-tests/src/main/java/org/keycloak/testsuite/springboot/TokenPage.java @@ -0,0 +1,19 @@ +package org.keycloak.testsuite.springboot; + +import java.net.URL; + +import org.jboss.arquillian.test.api.ArquillianResource; +import org.keycloak.testsuite.adapter.page.AbstractShowTokensPage; + +public class TokenPage extends AbstractShowTokensPage { + + @Override + public boolean isCurrent() { + return driver.getTitle().equalsIgnoreCase("tokens from spring boot"); + } + + @Override + public URL getInjectedUrl() { + return null; + } +} diff --git a/testsuite/integration-arquillian/tests/other/springboot-tests/src/test/java/org/keycloak/testsuite/springboot/AbstractSpringBootTest.java b/testsuite/integration-arquillian/tests/other/springboot-tests/src/test/java/org/keycloak/testsuite/springboot/AbstractSpringBootTest.java new file mode 100644 index 0000000000..5b15077719 --- /dev/null +++ b/testsuite/integration-arquillian/tests/other/springboot-tests/src/test/java/org/keycloak/testsuite/springboot/AbstractSpringBootTest.java @@ -0,0 +1,217 @@ +package org.keycloak.testsuite.springboot; + +import static org.keycloak.testsuite.admin.ApiUtil.assignRealmRoles; +import static org.keycloak.testsuite.admin.ApiUtil.createUserWithAdminClient; +import static org.keycloak.testsuite.admin.ApiUtil.resetUserPassword; +import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlStartsWith; + +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import javax.ws.rs.core.UriBuilder; + +import org.jboss.arquillian.graphene.page.Page; +import org.jboss.logging.Logger; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.keycloak.OAuth2Constants; +import org.keycloak.admin.client.resource.RealmResource; +import org.keycloak.admin.client.resource.RoleResource; +import org.keycloak.representations.idm.ClientRepresentation; +import org.keycloak.representations.idm.RealmRepresentation; +import org.keycloak.representations.idm.RoleRepresentation; +import org.keycloak.representations.idm.UserRepresentation; +import org.keycloak.testsuite.AbstractKeycloakTest; +import org.keycloak.testsuite.admin.ApiUtil; +import org.keycloak.testsuite.arquillian.SuiteContext; +import org.keycloak.testsuite.pages.LoginPage; +import org.keycloak.testsuite.util.WaitUtils; +import org.keycloak.util.TokenUtil; +import org.openqa.selenium.By; + +public abstract class AbstractSpringBootTest extends AbstractKeycloakTest { + + protected static final String REALM_ID = "cd8ee421-5100-41ba-95dd-b27c8e5cf042"; + + protected static final String REALM_NAME = "test"; + + protected static final String CLIENT_ID = "spring-boot-app"; + protected static final String SECRET = "e3789ac5-bde6-4957-a7b0-612823dac101"; + + protected static final String APPLICATION_URL = "http://localhost:8280"; + protected static final String BASE_URL = APPLICATION_URL + "/admin"; + + protected static final String USER_LOGIN = "testuser"; + protected static final String USER_EMAIL = "user@email.test"; + protected static final String USER_PASSWORD = "user-password"; + + protected static final String USER_LOGIN_2 = "testuser2"; + protected static final String USER_EMAIL_2 = "user2@email.test"; + protected static final String USER_PASSWORD_2 = "user2-password"; + + protected static final String CORRECT_ROLE = "admin"; + protected static final String INCORRECT_ROLE = "wrong-admin"; + + protected static final String REALM_PUBLIC_KEY = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5" + + "mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi7" + + "9NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB"; + + protected static final String REALM_PRIVATE_KEY = "MIICXAIBAAKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3Bj" + + "LGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vj" + + "O2NjsSAVcWEQMVhJ31LwIDAQABAoGAfmO8gVhyBxdqlxmIuglbz8bcjQbhXJLR2EoS8ngTXmN1bo2L90M0mUKSdc7qF10LgETBzqL8jY" + + "lQIbt+e6TH8fcEpKCjUlyq0Mf/vVbfZSNaVycY13nTzo27iPyWQHK5NLuJzn1xvxxrUeXI6A2WFpGEBLbHjwpx5WQG9A+2scECQQDvdn" + + "9NE75HPTVPxBqsEd2z10TKkl9CZxu10Qby3iQQmWLEJ9LNmy3acvKrE3gMiYNWb6xHPKiIqOR1as7L24aTAkEAtyvQOlCvr5kAjVqrEK" + + "Xalj0Tzewjweuxc0pskvArTI2Oo070h65GpoIKLc9jf+UA69cRtquwP93aZKtW06U8dQJAF2Y44ks/mK5+eyDqik3koCI08qaC8HYq2w" + + "Vl7G2QkJ6sbAaILtcvD92ToOvyGyeE0flvmDZxMYlvaZnaQ0lcSQJBAKZU6umJi3/xeEbkJqMfeLclD27XGEFoPeNrmdx0q10Azp4NfJ" + + "AY+Z8KRyQCR2BEG+oNitBOZ+YXF9KCpH3cdmECQHEigJhYg+ykOvr1aiZUMFT72HU0jnmQe2FVekuG+LJUt2Tm7GtMjTFoGpf0JwrVuZ" + + "N39fOYAlo+nTixgeW7X8Y="; + + @Page + protected LoginPage loginPage; + + @Page + protected SpringApplicationPage applicationPage; + + @Page + protected SpringAdminPage adminPage; + + @Page + protected TokenPage tokenPage; + + @Override + public void addTestRealms(List testRealms) { + RealmRepresentation realm = new RealmRepresentation(); + + realm.setId(REALM_ID); + realm.setRealm(REALM_NAME); + realm.setEnabled(true); + + realm.setPublicKey(REALM_PUBLIC_KEY); + realm.setPrivateKey(REALM_PRIVATE_KEY); + + realm.setClients(Collections.singletonList(createClient())); + + List eventListeners = new ArrayList<>(); + eventListeners.add("jboss-logging"); + eventListeners.add("event-queue"); + realm.setEventsListeners(eventListeners); + + testRealms.add(realm); + } + + private ClientRepresentation createClient() { + ClientRepresentation clientRepresentation = new ClientRepresentation(); + + clientRepresentation.setId(CLIENT_ID); + clientRepresentation.setSecret(SECRET); + + clientRepresentation.setBaseUrl(BASE_URL); + clientRepresentation.setRedirectUris(Collections.singletonList(BASE_URL + "/*")); + clientRepresentation.setAdminUrl(BASE_URL); + + return clientRepresentation; + } + + private void addUser(String login, String email, String password, String... roles) { + UserRepresentation userRepresentation = new UserRepresentation(); + + userRepresentation.setUsername(login); + userRepresentation.setEmail(email); + userRepresentation.setEmailVerified(true); + userRepresentation.setEnabled(true); + + RealmResource realmResource = adminClient.realm(REALM_NAME); + String userId = createUserWithAdminClient(realmResource, userRepresentation); + + resetUserPassword(realmResource.users().get(userId), password, false); + + for (String role : roles) + assignRealmRoles(realmResource, userId, role); + } + + private String getAuthRoot(SuiteContext suiteContext) { + return suiteContext.getAuthServerInfo().getContextRoot().toString(); + } + + private String encodeUrl(String url) { + String result; + try { + result = URLEncoder.encode(url, "UTF-8"); + } catch (UnsupportedEncodingException e) { + result = url; + } + + return result; + } + + protected String logoutPage(String redirectUrl) { + return getAuthRoot(suiteContext) + + "/auth/realms/" + REALM_NAME + + "/protocol/" + "openid-connect" + + "/logout?redirect_uri=" + encodeUrl(redirectUrl); + } + + protected void setAdapterAndServerTimeOffset(int timeOffset, String url) { + setTimeOffset(timeOffset); + + String timeOffsetUri = UriBuilder.fromUri(url) + .queryParam("timeOffset", timeOffset) + .build().toString(); + + driver.navigate().to(timeOffsetUri); + WaitUtils.waitUntilElement(By.tagName("body")).is().visible(); + } + + protected String getCorrectUserId() { + return adminClient.realms().realm(REALM_NAME).users().search(USER_LOGIN) + .get(0).getId(); + } + + @Before + public void createRoles() { + RealmResource realm = realmsResouce().realm(REALM_NAME); + + RoleRepresentation correct = new RoleRepresentation(CORRECT_ROLE, CORRECT_ROLE, false); + realm.roles().create(correct); + + RoleRepresentation incorrect = new RoleRepresentation(INCORRECT_ROLE, INCORRECT_ROLE, false); + realm.roles().create(incorrect); + } + + @Before + public void addUsers() { + addUser(USER_LOGIN, USER_EMAIL, USER_PASSWORD, CORRECT_ROLE); + addUser(USER_LOGIN_2, USER_EMAIL_2, USER_PASSWORD_2, INCORRECT_ROLE); + } + + @After + public void cleanupUsers() { + RealmResource providerRealm = adminClient.realm(REALM_NAME); + UserRepresentation userRep = ApiUtil.findUserByUsername(providerRealm, USER_LOGIN); + if (userRep != null) { + providerRealm.users().get(userRep.getId()).remove(); + } + + RealmResource childRealm = adminClient.realm(REALM_NAME); + userRep = ApiUtil.findUserByUsername(childRealm, USER_LOGIN_2); + if (userRep != null) { + childRealm.users().get(userRep.getId()).remove(); + } + } + + @After + public void cleanupRoles() { + RealmResource realm = realmsResouce().realm(REALM_NAME); + + RoleResource correctRole = realm.roles().get(CORRECT_ROLE); + correctRole.remove(); + + RoleResource incorrectRole = realm.roles().get(INCORRECT_ROLE); + incorrectRole.remove(); + } +} diff --git a/testsuite/integration-arquillian/tests/other/springboot-tests/src/test/java/org/keycloak/testsuite/springboot/BasicSpringBootTest.java b/testsuite/integration-arquillian/tests/other/springboot-tests/src/test/java/org/keycloak/testsuite/springboot/BasicSpringBootTest.java new file mode 100644 index 0000000000..6aea719f18 --- /dev/null +++ b/testsuite/integration-arquillian/tests/other/springboot-tests/src/test/java/org/keycloak/testsuite/springboot/BasicSpringBootTest.java @@ -0,0 +1,61 @@ +package org.keycloak.testsuite.springboot; + +import org.junit.Assert; +import org.junit.Test; + +public class BasicSpringBootTest extends AbstractSpringBootTest { + @Test + public void testCorrectUser() { + driver.navigate().to(APPLICATION_URL + "/index.html"); + + Assert.assertTrue("Must be on application page", applicationPage.isCurrent()); + + applicationPage.goAdmin(); + + Assert.assertTrue("Must be on login page", loginPage.isCurrent()); + + loginPage.login(USER_LOGIN, USER_PASSWORD); + + Assert.assertTrue("Must be on admin page", adminPage.isCurrent()); + Assert.assertTrue("Admin page must contain correct div", + driver.getPageSource().contains("You are now admin")); + + driver.navigate().to(logoutPage(BASE_URL)); + + Assert.assertTrue("Must be on login page", loginPage.isCurrent()); + + } + + @Test + public void testIncorrectUser() { + driver.navigate().to(APPLICATION_URL + "/index.html"); + + Assert.assertTrue("Must be on application page", applicationPage.isCurrent()); + + applicationPage.goAdmin(); + + Assert.assertTrue("Must be on login page", loginPage.isCurrent()); + + loginPage.login(USER_LOGIN_2, USER_PASSWORD_2); + + Assert.assertTrue("Must return 403 because of incorrect role", + driver.getPageSource().contains("There was an unexpected error (type=Forbidden, status=403)") + || driver.getPageSource().contains("\"status\":403,\"error\":\"Forbidden\"")); + } + + @Test + public void testIncorrectCredentials() { + driver.navigate().to(APPLICATION_URL + "/index.html"); + + Assert.assertTrue("Must be on application page", applicationPage.isCurrent()); + + applicationPage.goAdmin(); + + Assert.assertTrue("Must be on login page", loginPage.isCurrent()); + + loginPage.login(USER_LOGIN, USER_PASSWORD_2); + + Assert.assertEquals("Error message about password", + "Invalid username or password.", loginPage.getError()); + } +} diff --git a/testsuite/integration-arquillian/tests/other/springboot-tests/src/test/java/org/keycloak/testsuite/springboot/OfflineTokenSpringBootTest.java b/testsuite/integration-arquillian/tests/other/springboot-tests/src/test/java/org/keycloak/testsuite/springboot/OfflineTokenSpringBootTest.java new file mode 100644 index 0000000000..5ac950f767 --- /dev/null +++ b/testsuite/integration-arquillian/tests/other/springboot-tests/src/test/java/org/keycloak/testsuite/springboot/OfflineTokenSpringBootTest.java @@ -0,0 +1,154 @@ +package org.keycloak.testsuite.springboot; + +import org.jboss.arquillian.graphene.page.Page; +import org.junit.Assert; +import org.junit.Rule; +import org.junit.Test; +import org.keycloak.OAuth2Constants; +import org.keycloak.events.Details; +import org.keycloak.events.EventType; +import org.keycloak.services.Urls; +import org.keycloak.testsuite.AssertEvents; +import org.keycloak.testsuite.pages.AccountApplicationsPage; +import org.keycloak.testsuite.pages.OAuthGrantPage; +import org.keycloak.testsuite.util.ClientManager; +import org.keycloak.testsuite.util.WaitUtils; +import org.keycloak.util.TokenUtil; +import org.openqa.selenium.By; + +import javax.ws.rs.core.UriBuilder; +import java.util.List; + +import static org.keycloak.testsuite.util.WaitUtils.pause; + +public class OfflineTokenSpringBootTest extends AbstractSpringBootTest { + private static final String SERVLET_URI = APPLICATION_URL + "/admin/TokenServlet"; + + @Rule + public AssertEvents events = new AssertEvents(this); + + @Page + private AccountApplicationsPage accountAppPage; + + @Page + private OAuthGrantPage oauthGrantPage; + + @Test + public void testTokens() { + String servletUri = UriBuilder.fromUri(SERVLET_URI) + .queryParam(OAuth2Constants.SCOPE, OAuth2Constants.OFFLINE_ACCESS) + .build().toString(); + driver.navigate().to(servletUri); + + Assert.assertTrue("Must be on login page", loginPage.isCurrent()); + loginPage.login(USER_LOGIN, USER_PASSWORD); + + WaitUtils.waitUntilElement(By.tagName("body")).is().visible(); + + Assert.assertTrue(tokenPage.isCurrent()); + + Assert.assertEquals(tokenPage.getRefreshToken().getType(), TokenUtil.TOKEN_TYPE_OFFLINE); + Assert.assertEquals(tokenPage.getRefreshToken().getExpiration(), 0); + + String accessTokenId = tokenPage.getAccessToken().getId(); + String refreshTokenId = tokenPage.getRefreshToken().getId(); + + setAdapterAndServerTimeOffset(9999, SERVLET_URI); + + driver.navigate().to(SERVLET_URI); + Assert.assertTrue("Must be on tokens page", tokenPage.isCurrent()); + Assert.assertNotEquals(tokenPage.getRefreshToken().getId(), refreshTokenId); + Assert.assertNotEquals(tokenPage.getAccessToken().getId(), accessTokenId); + + setAdapterAndServerTimeOffset(0, SERVLET_URI); + + driver.navigate().to(logoutPage(SERVLET_URI)); + Assert.assertTrue("Must be on login page", loginPage.isCurrent()); + } + + @Test + public void testRevoke() { + // Login to servlet first with offline token + String servletUri = UriBuilder.fromUri(SERVLET_URI) + .queryParam(OAuth2Constants.SCOPE, OAuth2Constants.OFFLINE_ACCESS) + .build().toString(); + driver.navigate().to(servletUri); + WaitUtils.waitUntilElement(By.tagName("body")).is().visible(); + + loginPage.login(USER_LOGIN, USER_PASSWORD); + Assert.assertTrue("Must be on token page", tokenPage.isCurrent()); + + Assert.assertEquals(tokenPage.getRefreshToken().getType(), TokenUtil.TOKEN_TYPE_OFFLINE); + + // Assert refresh works with increased time + setAdapterAndServerTimeOffset(9999, SERVLET_URI); + driver.navigate().to(SERVLET_URI); + Assert.assertTrue("Must be on token page", tokenPage.isCurrent()); + setAdapterAndServerTimeOffset(0, SERVLET_URI); + + events.clear(); + + // Go to account service and revoke grant + accountAppPage.open(); + + List additionalGrants = accountAppPage.getApplications().get(CLIENT_ID).getAdditionalGrants(); + Assert.assertEquals(additionalGrants.size(), 1); + Assert.assertEquals(additionalGrants.get(0), "Offline Token"); + accountAppPage.revokeGrant(CLIENT_ID); + pause(500); + Assert.assertEquals(accountAppPage.getApplications().get(CLIENT_ID).getAdditionalGrants().size(), 0); + + events.expect(EventType.REVOKE_GRANT).realm(REALM_ID).user(getCorrectUserId()) + .client("account").detail(Details.REVOKED_CLIENT, CLIENT_ID).assertEvent(); + + // Assert refresh doesn't work now (increase time one more time) + setAdapterAndServerTimeOffset(9999, SERVLET_URI); + driver.navigate().to(SERVLET_URI); + loginPage.assertCurrent(); + setAdapterAndServerTimeOffset(0, SERVLET_URI); + } + + @Test + public void testConsent() { + ClientManager.realm(adminClient.realm(REALM_NAME)).clientId(CLIENT_ID).consentRequired(true); + + // Assert grant page doesn't have 'Offline Access' role when offline token is not requested + driver.navigate().to(SERVLET_URI); + loginPage.login(USER_LOGIN, USER_PASSWORD); + oauthGrantPage.assertCurrent(); + WaitUtils.waitUntilElement(By.xpath("//body")).text().not().contains("Offline access"); + oauthGrantPage.cancel(); + + // Assert grant page has 'Offline Access' role now + String servletUri = UriBuilder.fromUri(SERVLET_URI) + .queryParam(OAuth2Constants.SCOPE, OAuth2Constants.OFFLINE_ACCESS) + .build().toString(); + driver.navigate().to(servletUri); + WaitUtils.waitUntilElement(By.tagName("body")).is().visible(); + + loginPage.login(USER_LOGIN, USER_PASSWORD); + oauthGrantPage.assertCurrent(); + WaitUtils.waitUntilElement(By.xpath("//body")).text().contains("Offline access"); + + oauthGrantPage.accept(); + + Assert.assertTrue("Must be on token page", tokenPage.isCurrent()); + Assert.assertEquals(tokenPage.getRefreshToken().getType(), TokenUtil.TOKEN_TYPE_OFFLINE); + + String accountAppPageUrl = + Urls.accountApplicationsPage(getAuthServerRoot(), REALM_NAME).toString(); + driver.navigate().to(accountAppPageUrl); + AccountApplicationsPage.AppEntry offlineClient = accountAppPage.getApplications().get(CLIENT_ID); + Assert.assertTrue(offlineClient.getRolesGranted().contains("Offline access")); + Assert.assertTrue(offlineClient.getAdditionalGrants().contains("Offline Token")); + + //This was necessary to be introduced, otherwise other testcases will fail + driver.navigate().to(logoutPage(SERVLET_URI)); + loginPage.assertCurrent(); + + events.clear(); + + // Revert change + ClientManager.realm(adminClient.realm(REALM_NAME)).clientId(CLIENT_ID).consentRequired(false); + } +} diff --git a/testsuite/integration-arquillian/tests/other/sssd/pom.xml b/testsuite/integration-arquillian/tests/other/sssd/pom.xml index 93c7339215..e3f0b789e6 100644 --- a/testsuite/integration-arquillian/tests/other/sssd/pom.xml +++ b/testsuite/integration-arquillian/tests/other/sssd/pom.xml @@ -5,7 +5,7 @@ integration-arquillian-tests-other org.keycloak.testsuite - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT 4.0.0 diff --git a/testsuite/integration-arquillian/tests/pom.xml b/testsuite/integration-arquillian/tests/pom.xml index 24e67c9823..324f7265e8 100755 --- a/testsuite/integration-arquillian/tests/pom.xml +++ b/testsuite/integration-arquillian/tests/pom.xml @@ -24,7 +24,7 @@ org.keycloak.testsuite integration-arquillian - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT pom @@ -43,6 +43,7 @@ true false false + false auth-server-${auth.server} ${containers.home}/${auth.server.container} @@ -85,6 +86,7 @@ 12232 13232 jdbc:h2:mem:test-dc-shared + %d{HH:mm:ss,SSS} %-5p [%c] %m%n @@ -271,6 +273,7 @@ ${auth.server.crossdc} ${auth.server.undertow.crossdc} + ${cache.server.lifecycle.skip} ${cache.server} ${cache.server.port.offset} @@ -284,13 +287,14 @@ ${keycloak.connectionsInfinispan.remoteStorePort} ${keycloak.connectionsInfinispan.remoteStorePort.2} ${keycloak.connectionsInfinispan.remoteStoreServer} + ${keycloak.testsuite.logging.pattern} ${keycloak.connectionsJpa.url.crossdc} listener - org.keycloak.testsuite.util.TestEventsLogger,org.keycloak.testsuite.util.junit.AggregateResultsReporter,org.keycloak.testsuite.util.NonIDERunListener + org.keycloak.testsuite.util.TestEventsLogger,org.keycloak.testsuite.util.NonIDERunListener @@ -386,6 +390,7 @@ true true ${cache.server.home}/standalone/configuration + %d{HH:mm:ss,SSS} [%t] %-5p [%c{1.}] %m%n @@ -460,6 +465,7 @@ true true ${cache.server.home}/standalone/configuration + %d{HH:mm:ss,SSS} [%t] %-5p [%c{1.}] %m%n @@ -540,7 +546,7 @@ auth-server-enable-disable-feature @@ -584,6 +590,8 @@ ${containers.home}/auth-server-${auth.server}-backend2 ${auth.server.backend1.home}/standalone/configuration + + %d{HH:mm:ss,SSS} [%t] %-5p [%c{1.}] %m%n @@ -944,13 +952,6 @@ - - - org.keycloak.testsuite - integration-arquillian-test-utils - ${project.version} - - junit diff --git a/testsuite/integration/pom.xml b/testsuite/integration/pom.xml index cda3bbbfa8..25b8036ba8 100755 --- a/testsuite/integration/pom.xml +++ b/testsuite/integration/pom.xml @@ -21,7 +21,7 @@ keycloak-testsuite-pom org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT 4.0.0 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 8c38363db5..f981377064 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/KeycloakServer.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/KeycloakServer.java @@ -27,6 +27,7 @@ import org.jboss.logging.Logger; import org.jboss.resteasy.plugins.server.servlet.HttpServlet30Dispatcher; import org.jboss.resteasy.plugins.server.undertow.UndertowJaxrsServer; import org.jboss.resteasy.spi.ResteasyDeployment; +import org.keycloak.common.Version; import org.keycloak.connections.infinispan.InfinispanConnectionProvider; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; @@ -38,16 +39,18 @@ import org.keycloak.services.managers.RealmManager; import org.keycloak.services.resources.KeycloakApplication; import org.keycloak.testsuite.util.cli.TestsuiteCLI; import org.keycloak.util.JsonSerialization; -import org.mvel2.util.Make; import javax.servlet.DispatcherType; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; +import java.text.SimpleDateFormat; +import java.util.Date; import java.util.HashMap; import java.util.Map; import java.util.Properties; +import javax.net.ssl.SSLContext; /** * @author Stian Thorgersen @@ -61,6 +64,7 @@ public class KeycloakServer { public static class KeycloakServerConfig { private String host = "localhost"; private int port = 8081; + private int portHttps = -1; private int workerThreads = Math.max(Runtime.getRuntime().availableProcessors(), 2) * 8; private String resourcesHome; @@ -72,6 +76,10 @@ public class KeycloakServer { return port; } + public int getPortHttps() { + return portHttps; + } + public String getResourcesHome() { return resourcesHome; } @@ -84,6 +92,10 @@ public class KeycloakServer { this.port = port; } + public void setPortHttps(int portHttps) { + this.portHttps = portHttps; + } + public void setResourcesHome(String resourcesHome) { this.resourcesHome = resourcesHome; } @@ -106,6 +118,10 @@ public class KeycloakServer { } public static void main(String[] args) throws Throwable { + if (!System.getenv().containsKey("MAVEN_CMD_LINE_ARGS")) { + Version.BUILD_TIME = new SimpleDateFormat("yyyy-MM-dd HH:mm").format(new Date()); + } + bootstrapKeycloakServer(args); } @@ -133,6 +149,10 @@ public class KeycloakServer { config.setPort(Integer.valueOf(System.getProperty("keycloak.port"))); } + if (System.getProperty("keycloak.port.https") != null) { + config.setPortHttps(Integer.valueOf(System.getProperty("keycloak.port.https"))); + } + if (System.getProperty("keycloak.bind.address") != null) { config.setHost(System.getProperty("keycloak.bind.address")); } @@ -305,6 +325,10 @@ public class KeycloakServer { .setWorkerThreads(config.getWorkerThreads()) .setIoThreads(config.getWorkerThreads() / 8); + if (config.getPortHttps() != -1) { + builder = builder.addHttpsListener(config.getPortHttps(), config.getHost(), SSLContext.getDefault()); + } + server = new UndertowJaxrsServer(); try { server.start(builder); @@ -343,7 +367,9 @@ public class KeycloakServer { info("Loading resources from " + config.getResourcesHome()); } - info("Started Keycloak (http://" + config.getHost() + ":" + config.getPort() + "/auth) in " + info("Started Keycloak (http://" + config.getHost() + ":" + config.getPort() + "/auth" + + (config.getPortHttps() > 0 ? ", https://" + config.getHost() + ":" + config.getPortHttps()+ "/auth" : "") + + ") in " + (System.currentTimeMillis() - start) + " ms\n"); } catch (RuntimeException e) { server.stop(); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractFirstBrokerLoginTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractFirstBrokerLoginTest.java index acf775c313..b64cf03141 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractFirstBrokerLoginTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractFirstBrokerLoginTest.java @@ -37,6 +37,7 @@ import org.keycloak.testsuite.pages.InfoPage; import org.keycloak.testsuite.pages.LoginExpiredPage; import org.keycloak.testsuite.pages.LoginPasswordUpdatePage; import org.keycloak.testsuite.pages.LoginUpdateProfileEditUsernameAllowedPage; +import org.keycloak.testsuite.pages.ProceedPage; import org.keycloak.testsuite.rule.KeycloakRule; import org.keycloak.testsuite.rule.WebResource; import org.keycloak.testsuite.rule.WebRule; @@ -52,6 +53,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +import org.hamcrest.Matchers; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.startsWith; import static org.junit.Assert.assertEquals; @@ -345,6 +347,9 @@ public abstract class AbstractFirstBrokerLoginTest extends AbstractIdentityProvi // Go to the same link again driver.navigate().to(linkFromMail.trim()); + proceedPage.assertCurrent(); + Assert.assertThat(proceedPage.getInfo(), Matchers.containsString("Confirm linking the account")); + proceedPage.clickProceedLink(); infoPage.assertCurrent(); Assert.assertThat(infoPage.getInfo(), startsWith("You successfully verified your email. Please go back to your original browser and continue there with the login.")); } @@ -379,10 +384,14 @@ public abstract class AbstractFirstBrokerLoginTest extends AbstractIdentityProvi WebDriver driver2 = webRule2.getDriver(); InfoPage infoPage2 = webRule2.getPage(InfoPage.class); + ProceedPage proceedPage2 = webRule2.getPage(ProceedPage.class); driver2.navigate().to(linkFromMail.trim()); // authenticated, but not redirected to app. Just seeing info page. + proceedPage2.assertCurrent(); + Assert.assertThat(proceedPage2.getInfo(), Matchers.containsString("Confirm linking the account")); + proceedPage2.clickProceedLink(); infoPage2.assertCurrent(); Assert.assertThat(infoPage2.getInfo(), startsWith("You successfully verified your email. Please go back to your original browser and continue there with the login.")); } finally { @@ -540,16 +549,12 @@ public abstract class AbstractFirstBrokerLoginTest extends AbstractIdentityProvi driver.navigate().back(); Assert.assertTrue(driver.getPageSource().contains("You are already logged in.")); driver.navigate().forward(); - this.loginExpiredPage.assertCurrent(); - this.loginExpiredPage.clickLoginContinueLink(); this.idpConfirmLinkPage.assertCurrent(); // Click browser 'back' on review profile page this.idpConfirmLinkPage.clickReviewProfile(); this.updateProfilePage.assertCurrent(); driver.navigate().back(); - this.loginExpiredPage.assertCurrent(); - this.loginExpiredPage.clickLoginContinueLink(); this.updateProfilePage.assertCurrent(); this.updateProfilePage.update("Pedro", "Igor", "psilva@redhat.com"); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractIdentityProviderTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractIdentityProviderTest.java index 297d00a55b..c854e1e886 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractIdentityProviderTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractIdentityProviderTest.java @@ -110,6 +110,9 @@ public abstract class AbstractIdentityProviderTest { @WebResource protected InfoPage infoPage; + @WebResource + protected ProceedPage proceedPage; + protected KeycloakSession session; protected int logoutTimeOffset = 0; diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/OIDCBrokerUserPropertyTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/OIDCBrokerUserPropertyTest.java index c3e013549a..9fb585e55e 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/OIDCBrokerUserPropertyTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/OIDCBrokerUserPropertyTest.java @@ -91,9 +91,9 @@ public class OIDCBrokerUserPropertyTest extends AbstractKeycloakIdentityProvider @Override protected void doAssertTokenRetrieval(String pageSource) { try { - SAML2Request saml2Request = new SAML2Request(); - ResponseType responseType = (ResponseType) saml2Request - .getSAML2ObjectFromStream(PostBindingUtil.base64DecodeAsStream(pageSource)); + ResponseType responseType = (ResponseType) SAML2Request + .getSAML2ObjectFromStream(PostBindingUtil.base64DecodeAsStream(pageSource)) + .getSamlObject(); //.getSAML2ObjectFromStream(PostBindingUtil.base64DecodeAsStream(URLDecoder.decode(pageSource, "UTF-8"))); assertNotNull(responseType); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/SAMLBrokerUserPropertyTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/SAMLBrokerUserPropertyTest.java index bbbbc479d1..8fca98dc63 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/SAMLBrokerUserPropertyTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/SAMLBrokerUserPropertyTest.java @@ -90,9 +90,9 @@ public class SAMLBrokerUserPropertyTest extends AbstractKeycloakIdentityProvider @Override protected void doAssertTokenRetrieval(String pageSource) { try { - SAML2Request saml2Request = new SAML2Request(); - ResponseType responseType = (ResponseType) saml2Request - .getSAML2ObjectFromStream(PostBindingUtil.base64DecodeAsStream(pageSource)); + ResponseType responseType = (ResponseType) SAML2Request + .getSAML2ObjectFromStream(PostBindingUtil.base64DecodeAsStream(pageSource)) + .getSamlObject(); //.getSAML2ObjectFromStream(PostBindingUtil.base64DecodeAsStream(URLDecoder.decode(pageSource, "UTF-8"))); assertNotNull(responseType); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/SAMLKeyCloakServerBrokerBasicTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/SAMLKeyCloakServerBrokerBasicTest.java index 8afc49b692..5e177796df 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/SAMLKeyCloakServerBrokerBasicTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/SAMLKeyCloakServerBrokerBasicTest.java @@ -93,9 +93,9 @@ public class SAMLKeyCloakServerBrokerBasicTest extends AbstractKeycloakIdentityP @Override protected void doAssertTokenRetrieval(String pageSource) { try { - SAML2Request saml2Request = new SAML2Request(); - ResponseType responseType = (ResponseType) saml2Request - .getSAML2ObjectFromStream(PostBindingUtil.base64DecodeAsStream(pageSource)); + ResponseType responseType = (ResponseType) SAML2Request + .getSAML2ObjectFromStream(PostBindingUtil.base64DecodeAsStream(pageSource)) + .getSamlObject(); //.getSAML2ObjectFromStream(PostBindingUtil.base64DecodeAsStream(URLDecoder.decode(pageSource, "UTF-8"))); assertNotNull(responseType); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/SAMLKeyCloakServerBrokerWithSignatureTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/SAMLKeyCloakServerBrokerWithSignatureTest.java index 8a453a7fe6..a0ee823d9b 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/SAMLKeyCloakServerBrokerWithSignatureTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/SAMLKeyCloakServerBrokerWithSignatureTest.java @@ -98,9 +98,9 @@ public class SAMLKeyCloakServerBrokerWithSignatureTest extends AbstractKeycloakI @Override protected void doAssertTokenRetrieval(String pageSource) { try { - SAML2Request saml2Request = new SAML2Request(); - ResponseType responseType = (ResponseType) saml2Request - .getSAML2ObjectFromStream(PostBindingUtil.base64DecodeAsStream(pageSource)); + ResponseType responseType = (ResponseType) SAML2Request + .getSAML2ObjectFromStream(PostBindingUtil.base64DecodeAsStream(pageSource)) + .getSamlObject(); assertNotNull(responseType); assertFalse(responseType.getAssertions().isEmpty()); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ClusterInvalidationTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ClusterInvalidationTest.java index f71d0da25a..f11499e3c7 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ClusterInvalidationTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ClusterInvalidationTest.java @@ -67,7 +67,7 @@ import org.keycloak.testsuite.util.cli.TestCacheUtils; * * * - * Finally, add this system property when running the test: -Dkeycloak.connectionsInfinispan.remoteStoreEnabled=true + * Finally, add this system properties when running the test: -Dkeycloak.connectionsInfinispan.remoteStoreEnabled=true -Dkeycloak.connectionsInfinispan.siteName=dc-0 * * @author Marek Posolda */ diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionProviderTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionProviderTest.java index 7d6745e702..0312cbb753 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionProviderTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionProviderTest.java @@ -105,6 +105,13 @@ public class UserSessionProviderTest { assertEquals(1000, session.sessions().getUserSession(realm, sessions[0].getId()).getLastSessionRefresh()); } + @Test + public void testUpdateSessionInSameTransaction() { + UserSessionModel[] sessions = createSessions(); + session.sessions().getUserSession(realm, sessions[0].getId()).setLastSessionRefresh(1000); + assertEquals(1000, session.sessions().getUserSession(realm, sessions[0].getId()).getLastSessionRefresh()); + } + @Test public void testRestartSession() { int started = Time.currentTime(); @@ -156,7 +163,8 @@ public class UserSessionProviderTest { String userSessionId = sessions[0].getId(); String clientUUID = realm.getClientByClientId("test-app").getId(); - AuthenticatedClientSessionModel clientSession = sessions[0].getAuthenticatedClientSessions().get(clientUUID); + UserSessionModel userSession = session.sessions().getUserSession(realm, userSessionId); + AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessions().get(clientUUID); int time = clientSession.getTimestamp(); assertEquals(null, clientSession.getAction()); @@ -172,6 +180,24 @@ public class UserSessionProviderTest { assertEquals(time + 10, updated.getTimestamp()); } + @Test + public void testUpdateClientSessionInSameTransaction() { + UserSessionModel[] sessions = createSessions(); + + String userSessionId = sessions[0].getId(); + String clientUUID = realm.getClientByClientId("test-app").getId(); + + UserSessionModel userSession = session.sessions().getUserSession(realm, userSessionId); + AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessions().get(clientUUID); + + clientSession.setAction(AuthenticatedClientSessionModel.Action.CODE_TO_TOKEN.name()); + clientSession.setNote("foo", "bar"); + + AuthenticatedClientSessionModel updated = session.sessions().getUserSession(realm, userSessionId).getAuthenticatedClientSessions().get(clientUUID); + assertEquals(AuthenticatedClientSessionModel.Action.CODE_TO_TOKEN.name(), updated.getAction()); + assertEquals("bar", updated.getNote("foo")); + } + @Test public void testGetUserSessions() { UserSessionModel[] sessions = createSessions(); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/ProceedPage.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/ProceedPage.java new file mode 100644 index 0000000000..97d7c289e0 --- /dev/null +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/ProceedPage.java @@ -0,0 +1,51 @@ +/* + * Copyright 2017 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.pages; + +import org.openqa.selenium.WebElement; +import org.openqa.selenium.support.FindBy; + +/** + * + * @author hmlnarik + */ +public class ProceedPage extends AbstractPage { + + @FindBy(className = "instruction") + private WebElement infoMessage; + + @FindBy(linkText = "» Click here to proceed") + private WebElement proceedLink; + + public String getInfo() { + return infoMessage.getText(); + } + + public boolean isCurrent() { + return driver.getPageSource().contains("kc-info-message") && proceedLink.isDisplayed(); + } + + @Override + public void open() { + throw new UnsupportedOperationException(); + } + + public void clickProceedLink() { + proceedLink.click(); + } + +} diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/saml/ValidationTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/saml/ValidationTest.java index 6833b3415b..de905bfe5d 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/saml/ValidationTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/saml/ValidationTest.java @@ -78,7 +78,7 @@ public class ValidationTest { public void testBrokerExportDescriptor() throws Exception { URL schemaFile = getClass().getResource("/schema/saml/v2/saml-schema-metadata-2.0.xsd"); Source xmlFile = new StreamSource(new ByteArrayInputStream(SPMetadataDescriptor.getSPDescriptor( - "POST", "http://realm/assertion", "http://realm/logout", true, false, "test", SamlProtocol.SAML_DEFAULT_NAMEID_FORMAT, KeycloakModelUtils.generateKeyPairCertificate("test").getCertificate() + "POST", "http://realm/assertion", "http://realm/logout", true, false, false, "test", SamlProtocol.SAML_DEFAULT_NAMEID_FORMAT, KeycloakModelUtils.generateKeyPairCertificate("test").getCertificate(), "" ).getBytes()), "SP Descriptor"); SchemaFactory schemaFactory = SchemaFactory .newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/AbstractOfflineCacheCommand.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/AbstractSessionCacheCommand.java similarity index 56% rename from testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/AbstractOfflineCacheCommand.java rename to testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/AbstractSessionCacheCommand.java index 7aea03e5e3..f85a8e3cc5 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/AbstractOfflineCacheCommand.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/AbstractSessionCacheCommand.java @@ -23,18 +23,28 @@ import org.infinispan.context.Flag; import org.keycloak.common.util.Time; import org.keycloak.connections.infinispan.InfinispanConnectionProvider; import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserModel; import org.keycloak.models.sessions.infinispan.entities.SessionEntity; import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity; +import org.keycloak.models.utils.KeycloakModelUtils; /** * @author Marek Posolda */ -public abstract class AbstractOfflineCacheCommand extends AbstractCommand { +public abstract class AbstractSessionCacheCommand extends AbstractCommand { @Override protected void doRunCommand(KeycloakSession session) { InfinispanConnectionProvider provider = session.getProvider(InfinispanConnectionProvider.class); - Cache ispnCache = provider.getCache(InfinispanConnectionProvider.OFFLINE_SESSION_CACHE_NAME); + String cacheName = getArg(0); + if (!cacheName.equals(InfinispanConnectionProvider.SESSION_CACHE_NAME) && !cacheName.equals(InfinispanConnectionProvider.OFFLINE_SESSION_CACHE_NAME)) { + log.errorf("Invalid cache name: '%s', Only cache names '%s' or '%s' are supported", cacheName, InfinispanConnectionProvider.SESSION_CACHE_NAME, + InfinispanConnectionProvider.OFFLINE_SESSION_CACHE_NAME); + throw new HandledException(); + } + + Cache ispnCache = provider.getCache(cacheName); doRunCacheCommand(session, ispnCache); } @@ -52,12 +62,17 @@ public abstract class AbstractOfflineCacheCommand extends AbstractCommand { ", authenticatedClientSessions: " + clientSessionsSize; } + @Override + public String printUsage() { + return getName() + " "; + } + protected abstract void doRunCacheCommand(KeycloakSession session, Cache cache); // IMPLS - public static class PutCommand extends AbstractOfflineCacheCommand { + public static class PutCommand extends AbstractSessionCacheCommand { @Override public String getName() { @@ -67,10 +82,10 @@ public abstract class AbstractOfflineCacheCommand extends AbstractCommand { @Override protected void doRunCacheCommand(KeycloakSession session, Cache cache) { UserSessionEntity userSession = new UserSessionEntity(); - String id = getArg(0); + String id = getArg(1); userSession.setId(id); - userSession.setRealm(getArg(1)); + userSession.setRealm(getArg(2)); userSession.setLastSessionRefresh(Time.currentTime()); cache.put(id, userSession); @@ -78,12 +93,12 @@ public abstract class AbstractOfflineCacheCommand extends AbstractCommand { @Override public String printUsage() { - return getName() + " "; + return getName() + " "; } } - public static class GetCommand extends AbstractOfflineCacheCommand { + public static class GetCommand extends AbstractSessionCacheCommand { @Override public String getName() { @@ -92,19 +107,19 @@ public abstract class AbstractOfflineCacheCommand extends AbstractCommand { @Override protected void doRunCacheCommand(KeycloakSession session, Cache cache) { - String id = getArg(0); + String id = getArg(1); UserSessionEntity userSession = (UserSessionEntity) cache.get(id); printSession(id, userSession); } @Override public String printUsage() { - return getName() + " "; + return getName() + " "; } } // Just to check performance of multiple get calls. And comparing what's the change between the case when item is available locally or not. - public static class GetMultipleCommand extends AbstractOfflineCacheCommand { + public static class GetMultipleCommand extends AbstractSessionCacheCommand { @Override public String getName() { @@ -113,8 +128,8 @@ public abstract class AbstractOfflineCacheCommand extends AbstractCommand { @Override protected void doRunCacheCommand(KeycloakSession session, Cache cache) { - String id = getArg(0); - int count = getIntArg(1); + String id = getArg(1); + int count = getIntArg(2); long start = System.currentTimeMillis(); for (int i=0 ; i "; + return getName() + " "; } } - public static class RemoveCommand extends AbstractOfflineCacheCommand { + public static class RemoveCommand extends AbstractSessionCacheCommand { @Override public String getName() { @@ -141,18 +156,18 @@ public abstract class AbstractOfflineCacheCommand extends AbstractCommand { @Override protected void doRunCacheCommand(KeycloakSession session, Cache cache) { - String id = getArg(0); + String id = getArg(1); cache.remove(id); } @Override public String printUsage() { - return getName() + " "; + return getName() + " "; } } - public static class ClearCommand extends AbstractOfflineCacheCommand { + public static class ClearCommand extends AbstractSessionCacheCommand { @Override public String getName() { @@ -166,7 +181,7 @@ public abstract class AbstractOfflineCacheCommand extends AbstractCommand { } - public static class SizeCommand extends AbstractOfflineCacheCommand { + public static class SizeCommand extends AbstractSessionCacheCommand { @Override public String getName() { @@ -180,7 +195,7 @@ public abstract class AbstractOfflineCacheCommand extends AbstractCommand { } - public static class ListCommand extends AbstractOfflineCacheCommand { + public static class ListCommand extends AbstractSessionCacheCommand { @Override public String getName() { @@ -201,7 +216,7 @@ public abstract class AbstractOfflineCacheCommand extends AbstractCommand { } - public static class GetLocalCommand extends AbstractOfflineCacheCommand { + public static class GetLocalCommand extends AbstractSessionCacheCommand { @Override public String getName() { @@ -211,7 +226,7 @@ public abstract class AbstractOfflineCacheCommand extends AbstractCommand { @Override protected void doRunCacheCommand(KeycloakSession session, Cache cache) { - String id = getArg(0); + String id = getArg(1); cache = ((AdvancedCache) cache).withFlags(Flag.CACHE_MODE_LOCAL); UserSessionEntity userSession = (UserSessionEntity) cache.get(id); printSession(id, userSession); @@ -219,12 +234,12 @@ public abstract class AbstractOfflineCacheCommand extends AbstractCommand { @Override public String printUsage() { - return getName() + " "; + return getName() + " "; } } - public static class SizeLocalCommand extends AbstractOfflineCacheCommand { + public static class SizeLocalCommand extends AbstractSessionCacheCommand { @Override public String getName() { @@ -237,4 +252,80 @@ public abstract class AbstractOfflineCacheCommand extends AbstractCommand { } } + + public static class CreateManySessionsCommand extends AbstractSessionCacheCommand { + + @Override + public String getName() { + return "createManySessions"; + } + + @Override + protected void doRunCacheCommand(KeycloakSession session, Cache cache) { + String realmName = getArg(1); + int count = getIntArg(2); + int batchCount = getIntArg(3); + + BatchTaskRunner.runInBatches(0, count, batchCount, session.getKeycloakSessionFactory(), (KeycloakSession batchSession, int firstInIteration, int countInIteration) -> { + for (int i=0 ; i "; + } + + } + + + // This will propagate creating sessions to remoteCache too + public static class CreateManySessionsProviderCommand extends AbstractSessionCacheCommand { + + @Override + public String getName() { + return "createManySessionsProvider"; + } + + @Override + protected void doRunCacheCommand(KeycloakSession session, Cache cache) { + String realmName = getArg(1); + String username = getArg(2); + int count = getIntArg(3); + int batchCount = getIntArg(4); + + BatchTaskRunner.runInBatches(0, count, batchCount, session.getKeycloakSessionFactory(), (KeycloakSession batchSession, int firstInIteration, int countInIteration) -> { + RealmModel realm = batchSession.realms().getRealmByName(realmName); + UserModel user = batchSession.users().getUserByUsername(username, realm); + + for (int i=0 ; i "; + } + + } + } diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/BatchTaskRunner.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/BatchTaskRunner.java new file mode 100644 index 0000000000..e6f96cccc6 --- /dev/null +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/BatchTaskRunner.java @@ -0,0 +1,67 @@ +/* + * 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.util.cli; + +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.models.KeycloakSessionTask; +import org.keycloak.models.utils.KeycloakModelUtils; + +/** + * @author Marek Posolda + */ +class BatchTaskRunner { + + static void runInBatches(int first, int count, int batchCount, KeycloakSessionFactory sessionFactory, BatchTask batchTask) { + + final StateHolder state = new StateHolder(); + state.firstInThisBatch = first; + state.remaining = count; + state.countInThisBatch = Math.min(batchCount, state.remaining); + while (state.remaining > 0) { + KeycloakModelUtils.runJobInTransaction(sessionFactory, new KeycloakSessionTask() { + + @Override + public void run(KeycloakSession session) { + batchTask.run(session, state.firstInThisBatch, state.countInThisBatch); + } + }); + + // update state + state.firstInThisBatch = state.firstInThisBatch + state.countInThisBatch; + state.remaining = state.remaining - state.countInThisBatch; + state.countInThisBatch = Math.min(batchCount, state.remaining); + } + } + + + private static class StateHolder { + int firstInThisBatch; + int countInThisBatch; + int remaining; + }; + + + @FunctionalInterface + public interface BatchTask { + + void run(KeycloakSession session, int firstInThisIteration, int countInThisIteration); + + } + +} diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/CacheCommands.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/CacheCommands.java index 0c7eff0450..1f2e10db49 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/CacheCommands.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/CacheCommands.java @@ -72,7 +72,7 @@ public class CacheCommands { log.infof("Cache %s, size: %d", cache.getName(), size); if (size > 50) { - log.info("Skip printing cache recors due to big size"); + log.info("Skip printing cache records due to big size"); } else { for (Map.Entry entry : cache.entrySet()) { log.infof("%s=%s", entry.getKey(), entry.getValue()); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/ClusterProviderTaskCommand.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/ClusterProviderTaskCommand.java new file mode 100644 index 0000000000..f1e09677cf --- /dev/null +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/ClusterProviderTaskCommand.java @@ -0,0 +1,76 @@ +/* + * 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.util.cli; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; + +import org.keycloak.cluster.ClusterProvider; +import org.keycloak.common.util.MultivaluedHashMap; +import org.keycloak.models.KeycloakSession; +import org.keycloak.testsuite.federation.sync.SyncDummyUserFederationProviderFactory; + +/** + * @author Marek Posolda + */ +public class ClusterProviderTaskCommand extends AbstractCommand { + + private static final ExecutorService executors = Executors.newCachedThreadPool(); + + @Override + protected void doRunCommand(KeycloakSession session) { + String taskName = getArg(0); + int taskTimeout = getIntArg(1); + int sleepTime = getIntArg(2); + + ClusterProvider cluster = session.getProvider(ClusterProvider.class); + Future future = cluster.executeIfNotExecutedAsync(taskName, taskTimeout, () -> { + log.infof("Started sleeping for " + sleepTime + " seconds"); + Thread.sleep(sleepTime * 1000); + log.infof("Stopped sleeping"); + return null; + }); + + log.info("I've retrieved future successfully"); + + executors.execute(() -> { + try { + future.get(); + log.info("Successfully finished future!"); + } catch (Exception e) { + e.printStackTrace(); + } + }); + } + + private void updateConfig(MultivaluedHashMap cfg, int waitTime) { + cfg.putSingle(SyncDummyUserFederationProviderFactory.WAIT_TIME, String.valueOf(waitTime)); + } + + + @Override + public String getName() { + return "clusterProviderTask"; + } + + @Override + public String printUsage() { + return super.printUsage() + " "; + } +} diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/RoleCommands.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/RoleCommands.java index 2c71c72116..2ac3054994 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/RoleCommands.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/RoleCommands.java @@ -43,12 +43,6 @@ public class RoleCommands { return "createRoles"; } - private class StateHolder { - int firstInThisBatch; - int countInThisBatch; - int remaining; - }; - @Override protected void doRunCommand(KeycloakSession session) { rolePrefix = getArg(0); @@ -57,24 +51,9 @@ public class RoleCommands { int count = getIntArg(3); int batchCount = getIntArg(4); - final StateHolder state = new StateHolder(); - state.firstInThisBatch = first; - state.remaining = count; - state.countInThisBatch = Math.min(batchCount, state.remaining); - while (state.remaining > 0) { - KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), new KeycloakSessionTask() { - - @Override - public void run(KeycloakSession session) { - createRolesInBatch(session, roleContainer, rolePrefix, state.firstInThisBatch, state.countInThisBatch); - } - }); - - // update state - state.firstInThisBatch = state.firstInThisBatch + state.countInThisBatch; - state.remaining = state.remaining - state.countInThisBatch; - state.countInThisBatch = Math.min(batchCount, state.remaining); - } + BatchTaskRunner.runInBatches(first, count, batchCount, session.getKeycloakSessionFactory(), (KeycloakSession bathcSession, int firstInThisIteration, int countInThisIteration) -> { + createRolesInBatch(session, roleContainer, rolePrefix, firstInThisIteration, countInThisIteration); + }); log.infof("Command finished. All roles from %s to %s created", rolePrefix + first, rolePrefix + (first + count - 1)); } diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/TestsuiteCLI.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/TestsuiteCLI.java index b1ff087950..baedc8ab1b 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/TestsuiteCLI.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/TestsuiteCLI.java @@ -43,15 +43,17 @@ public class TestsuiteCLI { private static final Class[] BUILTIN_COMMANDS = { ExitCommand.class, HelpCommand.class, - AbstractOfflineCacheCommand.PutCommand.class, - AbstractOfflineCacheCommand.GetCommand.class, - AbstractOfflineCacheCommand.GetMultipleCommand.class, - AbstractOfflineCacheCommand.GetLocalCommand.class, - AbstractOfflineCacheCommand.SizeLocalCommand.class, - AbstractOfflineCacheCommand.RemoveCommand.class, - AbstractOfflineCacheCommand.SizeCommand.class, - AbstractOfflineCacheCommand.ListCommand.class, - AbstractOfflineCacheCommand.ClearCommand.class, + AbstractSessionCacheCommand.PutCommand.class, + AbstractSessionCacheCommand.GetCommand.class, + AbstractSessionCacheCommand.GetMultipleCommand.class, + AbstractSessionCacheCommand.GetLocalCommand.class, + AbstractSessionCacheCommand.SizeLocalCommand.class, + AbstractSessionCacheCommand.RemoveCommand.class, + AbstractSessionCacheCommand.SizeCommand.class, + AbstractSessionCacheCommand.ListCommand.class, + AbstractSessionCacheCommand.ClearCommand.class, + AbstractSessionCacheCommand.CreateManySessionsCommand.class, + AbstractSessionCacheCommand.CreateManySessionsProviderCommand.class, PersistSessionsCommand.class, LoadPersistentSessionsCommand.class, UserCommands.Create.class, @@ -62,7 +64,8 @@ public class TestsuiteCLI { RoleCommands.CreateRoles.class, CacheCommands.ListCachesCommand.class, CacheCommands.GetCacheCommand.class, - CacheCommands.CacheRealmObjectsCommand.class + CacheCommands.CacheRealmObjectsCommand.class, + ClusterProviderTaskCommand.class }; private final KeycloakSessionFactory sessionFactory; diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/UserCommands.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/UserCommands.java index 5fc0e86f6a..7360d7bb11 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/UserCommands.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/UserCommands.java @@ -48,12 +48,6 @@ public class UserCommands { return "createUsers"; } - private class StateHolder { - int firstInThisBatch; - int countInThisBatch; - int remaining; - }; - @Override protected void doRunCommand(KeycloakSession session) { usernamePrefix = getArg(0); @@ -64,24 +58,7 @@ public class UserCommands { int batchCount = getIntArg(5); roleNames = getArg(6); - final StateHolder state = new StateHolder(); - state.firstInThisBatch = first; - state.remaining = count; - state.countInThisBatch = Math.min(batchCount, state.remaining); - while (state.remaining > 0) { - KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), new KeycloakSessionTask() { - - @Override - public void run(KeycloakSession session) { - createUsersInBatch(session, state.firstInThisBatch, state.countInThisBatch); - } - }); - - // update state - state.firstInThisBatch = state.firstInThisBatch + state.countInThisBatch; - state.remaining = state.remaining - state.countInThisBatch; - state.countInThisBatch = Math.min(batchCount, state.remaining); - } + BatchTaskRunner.runInBatches(first, count, batchCount, session.getKeycloakSessionFactory(), this::createUsersInBatch); log.infof("Command finished. All users from %s to %s created", usernamePrefix + first, usernamePrefix + (first + count - 1)); } 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 fc695d420e..a8aa46dcdf 100755 --- a/testsuite/integration/src/test/resources/META-INF/keycloak-server.json +++ b/testsuite/integration/src/test/resources/META-INF/keycloak-server.json @@ -85,6 +85,9 @@ "connectionsInfinispan": { "default": { + "jgroupsUdpMcastAddr": "${keycloak.connectionsInfinispan.jgroupsUdpMcastAddr:234.56.78.90}", + "nodeName": "${keycloak.connectionsInfinispan.nodeName,jboss.node.name:}", + "siteName": "${keycloak.connectionsInfinispan.siteName,jboss.site.name:}", "clustered": "${keycloak.connectionsInfinispan.clustered:false}", "async": "${keycloak.connectionsInfinispan.async:false}", "sessionsOwners": "${keycloak.connectionsInfinispan.sessionsOwners:1}", diff --git a/testsuite/integration/src/test/resources/log4j.properties b/testsuite/integration/src/test/resources/log4j.properties index 5f0d60b153..20f1df698b 100755 --- a/testsuite/integration/src/test/resources/log4j.properties +++ b/testsuite/integration/src/test/resources/log4j.properties @@ -58,6 +58,13 @@ log4j.logger.org.keycloak.connections.jpa.DefaultJpaConnectionProviderFactory=${ # Enable to view hibernate statistics log4j.logger.org.keycloak.connections.jpa.HibernateStatsReporter=debug +keycloak.infinispan.logging.level=info +log4j.logger.org.keycloak.cluster.infinispan=${keycloak.infinispan.logging.level} +log4j.logger.org.keycloak.connections.infinispan=${keycloak.infinispan.logging.level} +log4j.logger.org.keycloak.keys.infinispan=${keycloak.infinispan.logging.level} +log4j.logger.org.keycloak.models.cache.infinispan=${keycloak.infinispan.logging.level} +log4j.logger.org.keycloak.models.sessions.infinispan=${keycloak.infinispan.logging.level} + # Enable to view ldap logging # log4j.logger.org.keycloak.storage.ldap=trace @@ -86,6 +93,8 @@ log4j.logger.org.apache.directory.server.ldap.LdapProtocolHandler=error #log4j.logger.org.keycloak.services.resources.IdentityBrokerService=trace #log4j.logger.org.keycloak.broker=trace +#log4j.logger.org.keycloak.cluster.infinispan.InfinispanNotificationsManager=trace + #log4j.logger.io.undertow=trace #log4j.logger.org.keycloak.protocol=debug diff --git a/testsuite/jetty/jetty81/pom.xml b/testsuite/jetty/jetty81/pom.xml index 21dd80932f..41d754930a 100755 --- a/testsuite/jetty/jetty81/pom.xml +++ b/testsuite/jetty/jetty81/pom.xml @@ -21,7 +21,7 @@ keycloak-testsuite-pom org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../../pom.xml 4.0.0 diff --git a/testsuite/jetty/jetty91/pom.xml b/testsuite/jetty/jetty91/pom.xml index 2c967593ed..6d4fe3e00f 100755 --- a/testsuite/jetty/jetty91/pom.xml +++ b/testsuite/jetty/jetty91/pom.xml @@ -21,7 +21,7 @@ keycloak-testsuite-pom org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../../pom.xml 4.0.0 diff --git a/testsuite/jetty/jetty92/pom.xml b/testsuite/jetty/jetty92/pom.xml index c6eb51c136..4886f7fbde 100755 --- a/testsuite/jetty/jetty92/pom.xml +++ b/testsuite/jetty/jetty92/pom.xml @@ -21,7 +21,7 @@ keycloak-testsuite-pom org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../../pom.xml 4.0.0 diff --git a/testsuite/jetty/jetty93/pom.xml b/testsuite/jetty/jetty93/pom.xml index 5412b51bb0..ca619eebbb 100644 --- a/testsuite/jetty/jetty93/pom.xml +++ b/testsuite/jetty/jetty93/pom.xml @@ -21,7 +21,7 @@ keycloak-testsuite-pom org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../../pom.xml 4.0.0 diff --git a/testsuite/jetty/jetty94/pom.xml b/testsuite/jetty/jetty94/pom.xml index 65dec4a9fa..87fbee5de7 100644 --- a/testsuite/jetty/jetty94/pom.xml +++ b/testsuite/jetty/jetty94/pom.xml @@ -21,7 +21,7 @@ keycloak-testsuite-pom org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../../pom.xml 4.0.0 diff --git a/testsuite/jetty/pom.xml b/testsuite/jetty/pom.xml index 75860cb8fb..ed23e79750 100755 --- a/testsuite/jetty/pom.xml +++ b/testsuite/jetty/pom.xml @@ -20,7 +20,7 @@ keycloak-testsuite-pom org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../pom.xml Keycloak SAML Jetty Testsuite Integration diff --git a/testsuite/pom.xml b/testsuite/pom.xml index a123732ce7..0adb305304 100755 --- a/testsuite/pom.xml +++ b/testsuite/pom.xml @@ -21,7 +21,7 @@ keycloak-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../pom.xml 4.0.0 @@ -53,6 +53,7 @@ integration tomcat8 integration-arquillian + utils diff --git a/testsuite/proxy/pom.xml b/testsuite/proxy/pom.xml index c120b111a0..0d0314f458 100755 --- a/testsuite/proxy/pom.xml +++ b/testsuite/proxy/pom.xml @@ -21,7 +21,7 @@ keycloak-testsuite-pom org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../pom.xml 4.0.0 diff --git a/testsuite/tomcat6/pom.xml b/testsuite/tomcat6/pom.xml index f2d3c7739b..7500803981 100755 --- a/testsuite/tomcat6/pom.xml +++ b/testsuite/tomcat6/pom.xml @@ -21,7 +21,7 @@ keycloak-testsuite-pom org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../pom.xml 4.0.0 diff --git a/testsuite/tomcat7/pom.xml b/testsuite/tomcat7/pom.xml index b984b03087..2109bac880 100755 --- a/testsuite/tomcat7/pom.xml +++ b/testsuite/tomcat7/pom.xml @@ -21,7 +21,7 @@ keycloak-testsuite-pom org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../pom.xml 4.0.0 diff --git a/testsuite/tomcat8/pom.xml b/testsuite/tomcat8/pom.xml index d4671955d0..700c4619b8 100755 --- a/testsuite/tomcat8/pom.xml +++ b/testsuite/tomcat8/pom.xml @@ -21,7 +21,7 @@ keycloak-testsuite-pom org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT ../pom.xml 4.0.0 diff --git a/testsuite/utils/pom.xml b/testsuite/utils/pom.xml new file mode 100755 index 0000000000..0bbf053514 --- /dev/null +++ b/testsuite/utils/pom.xml @@ -0,0 +1,36 @@ + + + + + + keycloak-testsuite-pom + org.keycloak + 3.3.0.CR1-SNAPSHOT + + 4.0.0 + + keycloak-testsuite-utils + Keycloak TestSuite Utils + + + + 1.8 + 1.8 + + diff --git a/testsuite/utils/src/main/java/org/keycloak/testsuite/LogTrimmer.java b/testsuite/utils/src/main/java/org/keycloak/testsuite/LogTrimmer.java new file mode 100644 index 0000000000..d53bf5b6ef --- /dev/null +++ b/testsuite/utils/src/main/java/org/keycloak/testsuite/LogTrimmer.java @@ -0,0 +1,44 @@ +package org.keycloak.testsuite; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; + +/** + * Created by st on 03/07/17. + */ +public class LogTrimmer { + + public static void main(String[] args) throws IOException { + BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); + String testRunning = null; + StringBuilder sb = new StringBuilder(); + for(String l = br.readLine(); l != null; l = br.readLine()) { + if (testRunning == null) { + if (l.startsWith("Running")) { + testRunning = l.split(" ")[1]; + System.out.println(l); + } else { + System.out.println("-- " + l); + } + } else { + if (l.contains("Tests run:")) { + if (!(l.contains("Failures: 0") && l.contains("Errors: 0"))) { + System.out.println("--------- " + testRunning + " output start ---------"); + System.out.println(sb.toString()); + System.out.println("--------- " + testRunning + " output end ---------"); + } + System.out.println(l); + + + testRunning = null; + sb = new StringBuilder(); + } else { + sb.append(testRunning.substring(testRunning.lastIndexOf('.') + 1) + " ++ " + l); + sb.append("\n"); + } + } + } + } + +} diff --git a/themes/pom.xml b/themes/pom.xml index e2a5de904a..42742626c3 100755 --- a/themes/pom.xml +++ b/themes/pom.xml @@ -4,7 +4,7 @@ keycloak-parent org.keycloak - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT 4.0.0 @@ -50,5 +50,48 @@ + + + + + maven-clean-plugin + + + + src/main/resources/theme/keycloak/common/resources/node_modules + + + + + + com.github.eirslett + frontend-maven-plugin + + + install node and yarn + + install-node-and-yarn + + generate-resources + + + yarn install + + yarn + + + install --production=false --frozen-lockfile + + + + + v6.11.1 + v0.27.5 + src/main/resources/theme/keycloak/common/resources + target + + + + diff --git a/themes/src/main/resources-community/theme/base/account/messages/messages_no.properties b/themes/src/main/resources-community/theme/base/account/messages/messages_no.properties index 0bc2c19434..948ff6c8a9 100644 --- a/themes/src/main/resources-community/theme/base/account/messages/messages_no.properties +++ b/themes/src/main/resources-community/theme/base/account/messages/messages_no.properties @@ -161,3 +161,4 @@ locale_ja=\u65E5\u672C\u8A9E locale_no=Norsk locale_pt-BR=Portugu\u00EAs (Brasil) locale_ru=\u0420\u0443\u0441\u0441\u043A\u0438\u0439 +locale_zh-CN=\u4e2d\u6587\u7b80\u4f53 diff --git a/themes/src/main/resources-community/theme/base/account/messages/messages_sv.properties b/themes/src/main/resources-community/theme/base/account/messages/messages_sv.properties index 8c83abbe2b..cc134cdebe 100755 --- a/themes/src/main/resources-community/theme/base/account/messages/messages_sv.properties +++ b/themes/src/main/resources-community/theme/base/account/messages/messages_sv.properties @@ -2,14 +2,14 @@ doSave=Spara doCancel=Avbryt doLogOutAllSessions=Logga ut från samtliga sessioner -doRemove=Ta Bort -doAdd=Lägg Till -doSignOut=Logga Ut +doRemove=Ta bort +doAdd=Lägg till +doSignOut=Logga ut -editAccountHtmlTitle=Redigera Konto +editAccountHtmlTitle=Redigera konto federatedIdentitiesHtmlTitle=Federerade identiteter -accountLogHtmlTitle=Kontoslogg -changePasswordHtmlTitle=Byt Lösenord +accountLogHtmlTitle=Kontologg +changePasswordHtmlTitle=Byt lösenord sessionsHtmlTitle=Sessioner accountManagementTitle=Kontohantering för Keycloak authenticatorTitle=Autentiserare @@ -21,40 +21,40 @@ firstName=Förnamn lastName=Efternamn password=Lösenord passwordConfirm=Bekräftelse -passwordNew=Nytt Lösenord +passwordNew=Nytt lösenord username=Användarnamn address=Adress street=Gata locality=Postort -region=Stat, Provins, eller Region +region=Stat, Provins eller Region postal_code=Postnummer country=Land emailVerified=E-post verifierad gssDelegationCredential=GSS Delegation Credential role_admin=Administratör -role_realm-admin=Realm Administratör +role_realm-admin=Realm-administratör role_create-realm=Skapa realm role_view-realm=Visa realm role_view-users=Visa användare role_view-applications=Visa applikationer role_view-clients=Visa klienter role_view-events=Visa event -role_view-identity-providers=Visa identity providers +role_view-identity-providers=Visa identitetsleverantörer role_manage-realm=Hantera realm role_manage-users=Hantera användare role_manage-applications=Hantera applikationer -role_manage-identity-providers=Hantera identity providers +role_manage-identity-providers=Hantera identitetsleverantörer role_manage-clients=Hantera klienter role_manage-events=Hantera event role_view-profile=Visa profil role_manage-account=Hantera konto role_read-token=Läs element -role_offline-access=Åtkomst Offline +role_offline-access=Åtkomst offline role_uma_authorization=Erhåll tillstånd client_account=Konto client_security-admin-console=Säkerhetsadministratörskonsol -client_admin-cli=Administratörs CLI +client_admin-cli=Administratörs-CLI client_realm-management=Realmhantering @@ -71,7 +71,7 @@ client=Klient clients=Klienter details=Detaljer started=Startade -lastAccess=Senast Åtkomst +lastAccess=Senast åtkomst expires=Upphör applications=Applikationer @@ -82,20 +82,20 @@ sessions=Sessioner log=Logg application=Applikation -availablePermissions=Tillgängliga Tillstånd -grantedPermissions=Beviljade Tillstånd -grantedPersonalInfo=Medgiven Personlig Information -additionalGrants=Ytterligare Medgivanden +availablePermissions=Tillgängliga rättigheter +grantedPermissions=Beviljade rättigheter +grantedPersonalInfo=Medgiven personlig information +additionalGrants=Ytterligare medgivanden action=Åtgärd -inResource=in -fullAccess=Fullständig Åtkomst -offlineToken=Offline Token -revoke=Upphäv Tillstånd +inResource=i +fullAccess=Fullständig åtkomst +offlineToken=Offline token +revoke=Upphäv rättighet -configureAuthenticators=Ändrade Autentiserare +configureAuthenticators=Konfigurerade autentiserare mobile=Mobil totpStep1=Installera FreeOTP eller Google Authenticator på din enhet. Båda applikationerna finns tillgängliga på Google Play och Apple App Store. -totpStep2=Öppna applikationen och skanna streckkoden eller skriv i nyckeLn. +totpStep2=Öppna applikationen och skanna streckkoden eller skriv i nyckeln. totpStep3=Fyll i engångskoden som tillhandahålls av applikationen och klicka på Spara för att avsluta inställningarna. missingUsernameMessage=Vänligen ange användarnamn. @@ -106,10 +106,10 @@ missingEmailMessage=Vänligen ange e-post. missingPasswordMessage=Vänligen ange lösenord. notMatchPasswordMessage=Lösenorden matchar inte. -missingTotpMessage=Vänligen ange autentiserarekoden. +missingTotpMessage=Vänligen ange autentiseringskoden. invalidPasswordExistingMessage=Det nuvarande lösenordet är ogiltigt. invalidPasswordConfirmMessage=Lösenordsbekräftelsen matchar inte. -invalidTotpMessage=Autentiserarekoden är ogiltig. +invalidTotpMessage=Autentiseringskoden är ogiltig. usernameExistsMessage=Användarnamnet finns redan. emailExistsMessage=E-posten finns redan. @@ -120,20 +120,20 @@ readOnlyPasswordMessage=Du kan inte uppdatera ditt lösenord eftersom ditt konto successTotpMessage=Mobilautentiseraren är inställd. successTotpRemovedMessage=Mobilautentiseraren är borttagen. -successGrantRevokedMessage=Upphävandet av tillståndet lyckades. +successGrantRevokedMessage=Upphävandet av rättigheten lyckades. accountUpdatedMessage=Ditt konto har uppdaterats. accountPasswordUpdatedMessage=Ditt lösenord har uppdaterats. -missingIdentityProviderMessage=Identity provider är inte angiven. +missingIdentityProviderMessage=Identitetsleverantör är inte angiven. invalidFederatedIdentityActionMessage=Åtgärden är ogiltig eller saknas. -identityProviderNotFoundMessage=Angiven identity provider hittas inte. +identityProviderNotFoundMessage=Angiven identitetsleverantör hittas inte. federatedIdentityLinkNotActiveMessage=Den här identiteten är inte längre aktiv. -federatedIdentityRemovingLastProviderMessage=Du kan inte ta bort senaste federerade identiteten eftersom du inte har lösenordet. -identityProviderRedirectErrorMessage=Misslyckades med att omdirigera till identity provider. -identityProviderRemovedMessage=Borttaginingen av Identity provider lyckades. +federatedIdentityRemovingLastProviderMessage=Du kan inte ta bort senaste federerade identiteten eftersom du inte har ett lösenord. +identityProviderRedirectErrorMessage=Misslyckades med att omdirigera till identitetsleverantör. +identityProviderRemovedMessage=Borttagningen av identitetsleverantören lyckades. identityProviderAlreadyLinkedMessage=Den federerade identiteten som returnerades av {0} är redan länkad till en annan användare. -staleCodeAccountMessage=Sidan har redan upphört. Vänligen försök igen. +staleCodeAccountMessage=Sidan har upphört att gälla. Vänligen försök igen. consentDenied=Samtycket förnekades. accountDisabledMessage=Kontot är inaktiverat, kontakta administratör. @@ -145,6 +145,6 @@ invalidPasswordMinDigitsMessage=Ogiltigt lösenord: måste innehålla minst {0} invalidPasswordMinUpperCaseCharsMessage=Ogiltigt lösenord: måste innehålla minst {0} stora bokstäver. invalidPasswordMinSpecialCharsMessage=Ogiltigt lösenord: måste innehålla minst {0} specialtecken. invalidPasswordNotUsernameMessage=Ogiltigt lösenord: Får inte vara samma som användarnamnet. -invalidPasswordRegexPatternMessage=Ogiltigt lösenord: matchar inte regex mönstret(en). +invalidPasswordRegexPatternMessage=Ogiltigt lösenord: matchar inte kravet för lösenordsmönster. invalidPasswordHistoryMessage=Ogiltigt lösenord: Får inte vara samma som de senaste {0} lösenorden. invalidPasswordGenericMessage=Ogiltigt lösenord: Det nya lösenordet stämmer inte med lösenordspolicyn. \ No newline at end of file diff --git a/themes/src/main/resources-community/theme/base/admin/messages/admin-messages_es.properties b/themes/src/main/resources-community/theme/base/admin/messages/admin-messages_es.properties index fb17ff6a24..f5cab83e9a 100755 --- a/themes/src/main/resources-community/theme/base/admin/messages/admin-messages_es.properties +++ b/themes/src/main/resources-community/theme/base/admin/messages/admin-messages_es.properties @@ -464,3 +464,4 @@ identity-provider-mappers=Asignadores de proveedores de identidad (IDP) create-identity-provider-mapper=Crear asignador de proveedor de identidad (IDP) add-identity-provider-mapper=A\u00F1adir asignador de proveedor de identidad client.description.tooltip=Indica la descripci\u00F3n del cliente. Por ejemplo ''My Client for TimeSheets''. Tambi\u00E9n soporta claves para valores localizados. Por ejemplo: ${my_client_description} +content-type-options= diff --git a/themes/src/main/resources-community/theme/base/admin/theme.properties b/themes/src/main/resources-community/theme/base/admin/theme.properties index 46cee85718..4bd8da4d73 100644 --- a/themes/src/main/resources-community/theme/base/admin/theme.properties +++ b/themes/src/main/resources-community/theme/base/admin/theme.properties @@ -1,2 +1,2 @@ import=common/keycloak -locales=ca,de,en,es,fr,it,ja,lt,no,pt-BR,ru \ No newline at end of file +locales=ca,en,es,fr,it,ja,lt,no,pt-BR,ru,zh-CN \ No newline at end of file diff --git a/themes/src/main/resources-community/theme/base/email/messages/messages_de.properties b/themes/src/main/resources-community/theme/base/email/messages/messages_de.properties index f8c7145d75..9068321b7b 100755 --- a/themes/src/main/resources-community/theme/base/email/messages/messages_de.properties +++ b/themes/src/main/resources-community/theme/base/email/messages/messages_de.properties @@ -1,7 +1,15 @@ emailVerificationSubject=E-Mail verifizieren -passwordResetSubject=Passwort zur\u00FCcksetzen emailVerificationBody=Jemand hat ein {2} Konto mit dieser E-Mail Adresse erstellt. Falls Sie das waren, dann klicken Sie auf den Link, um die E-Mail Adresse zu verifizieren.\n\n{0}\n\nDieser Link wird in {1} Minuten ablaufen.\n\nFalls Sie dieses Konto nicht erstellt haben, dann k\u00F6nnen sie diese Nachricht ignorieren. emailVerificationBodyHtml=

    Jemand hat ein {2} Konto mit dieser E-Mail Adresse erstellt. Falls das Sie waren, klicken Sie auf den Link, um die E-Mail Adresse zu verifizieren.

    {0}

    Dieser Link wird in {1} Minuten ablaufen.

    Falls Sie dieses Konto nicht erstellt haben, dann k\u00F6nnen sie diese Nachricht ignorieren.

    +identityProviderLinkSubject=Link {0} +identityProviderLinkBody=Es wurde beantragt Ihren Account {1} mit dem Account {0} von Benutzer {2} zu verlinken. Sollten Sie dies beantragt haben, klicken Sie auf den unten stehenden Link.\n\n{3}\n\n Die G\u00FCltigkeit des Links wird in {4} Minuten verfallen.\n\nSollten Sie Ihren Account nicht verlinken wollen, ignorieren Sie diese Nachricht. Wenn Sie die Accounts verlinken wird ein Login auf {1} \u00FCber {0} erm\u00F6glicht. +identityProviderLinkBodyHtml=

    Es wurde beantragt Ihren Account {1} mit dem Account {0} von Benutzer {2} zu verlinken. Sollten Sie dies beantragt haben, klicken Sie auf den unten stehenden Link.

    Link zur Best\u00E4tigung der Kontoverkn\u00FCpfung

    Die G\u00FCltigkeit des Links wird in {4} Minuten verfallen.

    Sollten Sie Ihren Account nicht verlinken wollen, ignorieren Sie diese Nachricht. Wenn Sie die Accounts verlinken wird ein Login auf {1} \u00FCber {0} erm\u00F6glicht.

    +passwordResetSubject=Passwort zur\u00FCcksetzen +passwordResetBody=Es wurde eine \u00C4nderung der Anmeldeinformationen f\u00FCr Ihren Account {2} angefordert. Wenn Sie diese \u00C4nderung beantragt haben, klicken Sie auf den unten stehenden Link.\n\n{0}\n\nDie G\u00FCltigkeit des Links wird in {1} Minuten verfallen.\n\nSollten Sie keine \u00C4nderung vollziehen wollen k\u00F6nnen Sie diese Nachricht ignorieren und an Ihrem Account wird nichts ge\u00E4ndert. +passwordResetBodyHtml=

    Es wurde eine \u00C4nderung der Anmeldeinformationen f\u00FCr Ihren Account {2} angefordert. Wenn Sie diese \u00C4nderung beantragt haben, klicken Sie auf den unten stehenden Link.

    Link zum Zur\u00FCcksetzen von Anmeldeinformationen

    Die G\u00FCltigkeit des Links wird in {1} Minuten verfallen.

    Sollten Sie keine \u00C4nderung vollziehen wollen k\u00F6nnen Sie diese Nachricht ignorieren und an Ihrem Account wird nichts ge\u00E4ndert.

    +executeActionsSubject=Aktualisieren Sie Ihr Konto +executeActionsBody=Ihr Administrator hat Sie aufgefordert Ihren Account {2} zu aktualisieren. Klicken Sie auf den unten stehenden Link um den Prozess zu starten.\n\n{0}\n\nDie G\u00FCltigkeit des Links wird in {1} Minuten verfallen.\n\nSollten Sie sich dieser Aufforderung nicht bewusst sein, ignorieren Sie diese Nachricht und Ihr Account bleibt unver\u00E4ndert. +executeActionsBodyHtml=

    Ihr Administrator hat Sie aufgefordert Ihren Account {2} zu aktualisieren. Klicken Sie auf den unten stehenden Link um den Prozess zu starten.

    Link zum Account-Update

    Die G\u00FCltigkeit des Links wird in {1} Minuten verfallen.

    Sollten Sie sich dieser Aufforderung nicht bewusst sein, ignorieren Sie diese Nachricht und Ihr Account bleibt unver\u00E4ndert.

    eventLoginErrorSubject=Fehlgeschlagene Anmeldung eventLoginErrorBody=Jemand hat um {0} von {1} versucht, sich mit ihrem Konto anzumelden. Falls das nicht Sie waren, dann kontaktieren Sie bitte Ihren Admin. eventLoginErrorBodyHtml=

    Jemand hat um {0} von {1} versucht, sich mit ihrem Konto anzumelden. Falls das nicht Sie waren, dann kontaktieren Sie bitte Ihren Admin.

    diff --git a/themes/src/main/resources-community/theme/base/email/messages/messages_ja.properties b/themes/src/main/resources-community/theme/base/email/messages/messages_ja.properties index 087170a10c..a60ffe3d2b 100644 --- a/themes/src/main/resources-community/theme/base/email/messages/messages_ja.properties +++ b/themes/src/main/resources-community/theme/base/email/messages/messages_ja.properties @@ -1,16 +1,16 @@ # encoding: utf-8 emailVerificationSubject=Eメールの確認 emailVerificationBody=このメールアドレスで {2} アカウントが作成されたました。以下のリンクをクリックしてメールアドレスの確認を完了してください。\n\n{0}\n\nこのリンクは {1} 分間だけ有効です。\n\nもしこのアカウントの作成に心当たりがない場合は、このメールを無視してください。 -emailVerificationBodyHtml=

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

    {0}

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

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

    +emailVerificationBodyHtml=

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

    メールアドレスの確認

    このリンクは {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} にログインすることができるようになります。

    +identityProviderLinkBodyHtml=

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

    アカウントリンクの確認

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

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

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

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

    {0}

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

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

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

    あなたの {2} アカウントのパスワードの変更が要求されています。以下のリンクをクリックしてパスワードのリセットを行ってください。

    パスワードのリセット

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

    もしパスワードのリセットを行わない場合は、このメッセージを無視してください。何も変更されません。

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

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

    {0}

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

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

    +executeActionsBodyHtml=

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

    アカウントの更新

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

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

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

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

    diff --git a/themes/src/main/resources-community/theme/base/email/messages/messages_sv.properties b/themes/src/main/resources-community/theme/base/email/messages/messages_sv.properties index 5b5ac6d582..a5ffbf43b6 100755 --- a/themes/src/main/resources-community/theme/base/email/messages/messages_sv.properties +++ b/themes/src/main/resources-community/theme/base/email/messages/messages_sv.properties @@ -6,9 +6,9 @@ identityProviderLinkSubject=Länk {0} identityProviderLinkBody=Någon vill länka ditt "{1}" konto med "{0}" kontot tillhörande användaren {2} . Om det var du, klicka då på länken nedan för att länka kontona\n\n{3}\n\nDen här länken kommer att upphöra inom {4} minuter.\n\nOm du inte vill länka kontot, ignorera i så fall det här meddelandet. Om du länkar kontona, så kan du logga in till {1} genom {0}. identityProviderLinkBodyHtml=

    Någon vill länka ditt {1} konto med {0} kontot tillhörande användaren {2} . Om det var du, klicka då på länken nedan för att länka kontona

    {3}

    Den här länken kommer att upphöra inom {4} minuter.

    Om du inte vill länka kontot, ignorera i så fall det här meddelandet. Om du länkar kontona, så kan du logga in till {1} genom {0}.

    passwordResetSubject=Återställ lösenord -passwordResetBody=Någon har precis bett om att ändra ditt {2} kontos användaruppgifter. Om det var du, klicka då på länken nedan för att återställa dem.\n\n{0}\n\nDen här länken och koden kommer att upphöra inom {1} minuter.\n\nOm du inte vill återställa dina kontouppgifter, ignorera i så fall det här meddelandet så kommer inget att ändras. -passwordResetBodyHtml=

    Någon har precis bett om att ändra ditt {2} kontos användaruppgifter. Om det var du, klicka då på länken nedan för att återställa dem.

    {0}

    Den här länken och koden kommer att upphöra inom {1} minuter.

    Om du inte vill återställa dina kontouppgifter, ignorera i så fall det här meddelandet så kommer inget att ändras.

    -executeActionsSubject=Uppdatera Ditt Konto +passwordResetBody=Någon har precis bett om att ändra användaruppgifter för ditt konto {2}. Om det var du, klicka då på länken nedan för att återställa dem.\n\n{0}\n\nDen här länken och koden kommer att upphöra inom {1} minuter.\n\nOm du inte vill återställa dina kontouppgifter, ignorera i så fall det här meddelandet så kommer inget att ändras. +passwordResetBodyHtml=

    Någon har precis bett om att ändra användaruppgifter för ditt konto {2}. Om det var du, klicka då på länken nedan för att återställa dem.

    {0}

    Den här länken och koden kommer att upphöra inom {1} minuter.

    Om du inte vill återställa dina kontouppgifter, ignorera i så fall det här meddelandet så kommer inget att ändras.

    +executeActionsSubject=Uppdatera ditt konto executeActionsBody=Din administratör har precis bett om att du skall uppdatera ditt {2} konto. Klicka på länken för att påbörja processen.\n\n{0}\n\nDen här länken kommer att upphöra inom {1} minuter.\n\nOm du är omedveten om att din administratör har bett om detta, ignorera i så fall det här meddelandet så kommer inget att ändras. executeActionsBodyHtml=

    Din administratör har precis bett om att du skall uppdatera ditt {2} konto. Klicka på länken för att påbörja processen.

    {0}

    Den här länken kommer att upphöra inom {1} minuter.

    Om du är omedveten om att din administratör har bett om detta, ignorera i så fall det här meddelandet så kommer inget att ändras.

    eventLoginErrorSubject=Inloggningsfel diff --git a/themes/src/main/resources-community/theme/base/login/messages/messages_no.properties b/themes/src/main/resources-community/theme/base/login/messages/messages_no.properties index efa23a354c..1f1a9d903e 100644 --- a/themes/src/main/resources-community/theme/base/login/messages/messages_no.properties +++ b/themes/src/main/resources-community/theme/base/login/messages/messages_no.properties @@ -218,6 +218,7 @@ locale_no=Norsk locale_pt_BR=Portugu\u00EAs (Brasil) locale_pt-BR=Portugu\u00EAs (Brasil) locale_ru=\u0420\u0443\u0441\u0441\u043A\u0438\u0439 +locale_zh-CN=\u4e2d\u6587\u7b80\u4f53 backToApplication=« Tilbake til applikasjonen missingParameterMessage=Manglende parameter\: {0} diff --git a/themes/src/main/resources-community/theme/base/login/messages/messages_sv.properties b/themes/src/main/resources-community/theme/base/login/messages/messages_sv.properties index c671ee33fe..c383703aa4 100755 --- a/themes/src/main/resources-community/theme/base/login/messages/messages_sv.properties +++ b/themes/src/main/resources-community/theme/base/login/messages/messages_sv.properties @@ -1,5 +1,5 @@ # encoding: utf-8 -doLogIn=Logga In +doLogIn=Logga in doRegister=Registrera doCancel=Avbryt doSubmit=Skicka @@ -8,30 +8,30 @@ doNo=Nej doContinue=Fortsätt doAccept=Acceptera doDecline=Avböj -doForgotPassword=Glömt Lösenord? +doForgotPassword=Glömt lösenord? doClickHere=Klicka här doImpersonate=Imitera -kerberosNotConfigured=Kerberos är Inte Konfigurerad -kerberosNotConfiguredTitle=Kerberos är Inte Konfigurerad -bypassKerberosDetail=Antingen så är du inte inloggad via Kerberos eller så är inte din webläsare inställd för Kerberosinloggning. Vänligen klicka på fortsätt för att logga in på annat sätt. +kerberosNotConfigured=Kerberos är inte konfigurerat +kerberosNotConfiguredTitle=Kerberos är inte konfigurerat +bypassKerberosDetail=Antingen så är du inte inloggad via Kerberos eller så är inte din webbläsare inställd för Kerberosinloggning. Vänligen klicka på fortsätt för att logga in på annat sätt. kerberosNotSetUp=Kerberos är inte inställt. Du kan inte logga in. registerWithTitle=Registrera med {0} registerWithTitleHtml={0} loginTitle=Logga in till {0} loginTitleHtml={0} -impersonateTitle={0} Imitera Användare -impersonateTitleHtml={0} Imitera Användare +impersonateTitle={0} Imitera användare +impersonateTitleHtml={0} Imitera användare realmChoice=Realm unknownUser=Okänd användare -loginTotpTitle=Inställning av Mobilautentiserare -loginProfileTitle=Uppdatera Kontoinformation -loginTimeout=Du tog för lång tid för att logga in. Inloggningsprocessen börjar om. -oauthGrantTitle=Bevilja Åtkomst +loginTotpTitle=Inställning av mobilautentiserare +loginProfileTitle=Uppdatera kontoinformation +loginTimeout=Det tog för lång tid att logga in. Inloggningsprocessen börjar om. +oauthGrantTitle=Bevilja åtkomst oauthGrantTitleHtml={0} errorTitle=Vi ber om ursäkt... errorTitleHtml=Vi ber om ursäkt ... -emailVerifyTitle=E-postsverifikation -emailForgotTitle=Glömt Ditt Lösenord? +emailVerifyTitle=E-postverifiering +emailForgotTitle=Glömt ditt lösenord? updatePasswordTitle=Uppdatera lösenord codeSuccessTitle=Rätt kod codeErrorTitle=Felkod\: {0} @@ -51,15 +51,15 @@ firstName=Förnamn lastName=Efternamn email=E-post password=Lösenord -passwordConfirm=Bekräfta Lösenord -passwordNew=Nytt Lösenord -passwordNewConfirm=Bekräftelse av Nytt Lösenord +passwordConfirm=Bekräfta lösenord +passwordNew=Nytt lösenord +passwordNewConfirm=Bekräftelse av nytt lösenord rememberMe=Kom ihåg mig authenticatorCode=Engångskod address=Adress street=Gata locality=Postort -region=Stat, Provins, eller Region +region=Stat, Provins eller Region postal_code=Postnummer country=Land emailVerified=E-post verifierad @@ -70,8 +70,8 @@ loginTotpStep2=Öppna applikationen och skanna streckkoden eller skriv i nyckeln loginTotpStep3=Fyll i engångskoden som tillhandahålls av applikationen och klicka på Spara för att avsluta inställningarna loginTotpOneTime=Engångskod -oauthGrantRequest=Godkänner du dessa åtkomstförmånen? -inResource=in +oauthGrantRequest=Godkänner du tillgång till de här rättigheterna? +inResource=i emailVerifyInstruction1=Ett e-postmeddelande med instruktioner om hur du verifierar din e-postadress har skickats till dig. emailVerifyInstruction2=Har du inte fått en verifikationskod i din e-post? @@ -88,9 +88,9 @@ emailInstruction=Fyll i ditt användarnamn eller din e-postadress, så kommer vi copyCodeInstruction=Vänligen kopiera den här koden och klistra in den i din applikation: -personalInfo=Personlig Information: +personalInfo=Personlig information: role_admin=Administratör -role_realm-admin=Realm Administratör +role_realm-admin=Realm-administratör role_create-realm=Skapa realm role_create-client=Skapa klient role_view-realm=Visa realm @@ -98,39 +98,39 @@ role_view-users=Visa användare role_view-applications=Visa applikationer role_view-clients=Visa klienter role_view-events=Visa event -role_view-identity-providers=Visa identity providers +role_view-identity-providers=Visa identitetsleverantörer role_manage-realm=Hantera realm role_manage-users=Hantera användare role_manage-applications=Hantera applikationer -role_manage-identity-providers=Hantera identity providers +role_manage-identity-providers=Hantera identitetsleverantörer role_manage-clients=Hantera klienter role_manage-events=Hantera event role_view-profile=Visa profil role_manage-account=Hantera konto role_read-token=Läs element -role_offline-access=Åtkomst Offline +role_offline-access=Åtkomst offline client_account=Konto client_security-admin-console=Säkerhetsadministratörskonsol -client_admin-cli=Administratörs CLI +client_admin-cli=Administratörs-CLI client_realm-management=Realmhantering invalidUserMessage=Ogiltigt användarnamn eller lösenord. invalidEmailMessage=Ogiltig e-postadress. accountDisabledMessage=Kontot är inaktiverat, kontakta administratör. accountTemporarilyDisabledMessage=Kontot är tillfälligt inaktiverat, kontakta administratör eller försök igen senare. -expiredCodeMessage=Inloggnings time-out. Vänligen försök igen. +expiredCodeMessage=Inloggningen nådde en maxtidsgräns. Vänligen försök igen. missingFirstNameMessage=Vänligen ange förnamn. missingLastNameMessage=Vänligen ange efternamn. missingEmailMessage=Vänligen ange e-post. missingUsernameMessage=Vänligen ange användarnamn. missingPasswordMessage=Vänligen ange lösenord. -missingTotpMessage=Vänligen ange autentiserarekod. +missingTotpMessage=Vänligen ange autentiseringskod. notMatchPasswordMessage=Lösenorden matchar inte. invalidPasswordExistingMessage=Det nuvarande lösenordet är ogiltigt. invalidPasswordConfirmMessage=Lösenordsbekräftelsen matchar inte. -invalidTotpMessage=Autentiserarekoden är ogiltig. +invalidTotpMessage=Autentiseringskoden är ogiltig. usernameExistsMessage=Användarnamnet finns redan. emailExistsMessage=E-postadressen finns redan. @@ -169,42 +169,42 @@ invalidPasswordGenericMessage=Ogiltigt lösenord: Det nya lösenordet stämmer i failedToProcessResponseMessage=Misslyckades med att behandla svaret httpsRequiredMessage=HTTPS krävs -realmNotEnabledMessage=Realm är inte aktiverat -invalidRequestMessage=Ogiltig Förfrågan +realmNotEnabledMessage=Realm är inte aktiverad +invalidRequestMessage=Ogiltig förfrågan failedLogout=Utloggning misslyckades unknownLoginRequesterMessage=Okänd inloggningsförfrågan loginRequesterNotEnabledMessage=Inloggningsförfrågaren är inte aktiverad -bearerOnlyMessage=Bearer-only applikationer tillåts inte att initiera inloggning genom webbläsare +bearerOnlyMessage=Bearer-only-applikationer tillåts inte att initiera inloggning genom webbläsare standardFlowDisabledMessage=Klienten tillåts inte att initiera inloggning genom webbläsare med det givna response_type. Standardflödet är inaktiverat för klienten. implicitFlowDisabledMessage=Klienten tillåts inte att initiera inloggning genom webbläsare med det givna response_type. Villkorslöst flöde är inaktiverat för klienten. -invalidRedirectUriMessage=Ogiltig omdirigerad uri +invalidRedirectUriMessage=Ogiltig omdirigeringsadress unsupportedNameIdFormatMessage=NameIDFormat stöds ej invalidRequesterMessage=Ogiltig förfrågare registrationNotAllowedMessage=Registrering tillåts ej resetCredentialNotAllowedMessage=Återställning av uppgifter tillåts ej -permissionNotApprovedMessage=Tillståndet ej godkänt. -noRelayStateInResponseMessage=Inget vidarebefordrat tillstånd i svaret från identity provider. +permissionNotApprovedMessage=Rättigheten ej godkänd. +noRelayStateInResponseMessage=Inget vidarebefordrat tillstånd i svaret från identitetsleverantör. insufficientPermissionMessage=Otillräckliga tillstånd för att länka identiteter. -couldNotProceedWithAuthenticationRequestMessage=Kunde inte fortsätta med autentiseringsförfrågan till identity provider. -couldNotObtainTokenMessage=Kunde inte motta element från identity provider. -unexpectedErrorRetrievingTokenMessage=Oväntat fel när element hämtas från identity provider. -unexpectedErrorHandlingResponseMessage=Oväntat fel under hantering av svar från från identity provider. -identityProviderAuthenticationFailedMessage=Autentiseringen misslyckades. Kunde inte autentisera med identity provider. +couldNotProceedWithAuthenticationRequestMessage=Kunde inte fortsätta med autentiseringsförfrågan till identitetsleverantör. +couldNotObtainTokenMessage=Kunde inte motta element från identitetsleverantör. +unexpectedErrorRetrievingTokenMessage=Oväntat fel när element hämtas från identitetsleverantör. +unexpectedErrorHandlingResponseMessage=Oväntat fel under hantering av svar från från identitetsleverantör. +identityProviderAuthenticationFailedMessage=Autentiseringen misslyckades. Kunde inte autentisera med identitetsleverantör. identityProviderDifferentUserMessage=Autentiserad som {0}, men väntades att vara autentiserad som {1} -couldNotSendAuthenticationRequestMessage=Kunde inte skicka autentiseringsförfrågan till identity provider. -unexpectedErrorHandlingRequestMessage=Oväntat fel under hantering av autentiseringsförfrågan till identity provider. +couldNotSendAuthenticationRequestMessage=Kunde inte skicka autentiseringsförfrågan till identitetsleverantör. +unexpectedErrorHandlingRequestMessage=Oväntat fel under hantering av autentiseringsförfrågan till identitetsleverantör. invalidAccessCodeMessage=Ogiltig tillträdeskod. sessionNotActiveMessage=Sessionen ej aktiv. invalidCodeMessage=Ett fel uppstod, vänligen logga in igen genom din applikation. -identityProviderUnexpectedErrorMessage=Oväntat fel under autentiseringen med identity provider -identityProviderNotFoundMessage=Kunde inte hitta en identity provider med identifikatorn. -identityProviderLinkSuccess=Ditt konto lyckades med att länka {0} med kontot {1} . +identityProviderUnexpectedErrorMessage=Oväntat fel under autentiseringen med identitetsleverantör +identityProviderNotFoundMessage=Kunde inte hitta en identitetsleverantör med identifikatorn. +identityProviderLinkSuccess=Ditt konto lyckades med att länka {0} med kontot {1}. staleCodeMessage=Den här sidan är inte längre giltig, vänligen gå tillbaka till din applikation och logga in igen -realmSupportsNoCredentialsMessage=Realm:et stödjer inga inloggningstyper. -identityProviderNotUniqueMessage=Realm:et stödjer flera identity providers. Kunde inte avgöra vilken identity provider som skall användas för autentisering. +realmSupportsNoCredentialsMessage=Realmen stödjer inga inloggningstyper. +identityProviderNotUniqueMessage=Realmen stödjer flera identitetsleverantör. Kunde inte avgöra vilken identitetsleverantör som skall användas för autentisering. emailVerifiedMessage=Din e-postadress har blivit verifierad. -staleEmailVerificationLink=Länken du klickade på är en gammal inaktuell länk som inte längre är giltig. Kanske har du redan verifierat din e-post? +staleEmailVerificationLink=Länken du klickade på är en gammal, inaktuell länk som inte längre är giltig. Kanske har du redan verifierat din e-post? backToApplication=« Tillbaka till applikationen missingParameterMessage=Parametrar som saknas\: {0} diff --git a/themes/src/main/resources-product/theme/rh-sso/admin/theme.properties b/themes/src/main/resources-product/theme/rh-sso/admin/theme.properties index 009f980712..5b489c1780 100755 --- a/themes/src/main/resources-product/theme/rh-sso/admin/theme.properties +++ b/themes/src/main/resources-product/theme/rh-sso/admin/theme.properties @@ -1,3 +1,3 @@ parent=keycloak import=common/rh-sso -styles=lib/rcue/css/rcue.min.css lib/rcue/css/rcue-additions.min.css lib/select2-3.4.1/select2.css css/styles.css lib/angular/treeview/css/angular.treeview.css +styles=lib/rcue/css/rcue.min.css lib/rcue/css/rcue-additions.min.css node_modules/select2/select2.css css/styles.css lib/angular/treeview/css/angular.treeview.css diff --git a/themes/src/main/resources/theme/base/account/applications.ftl b/themes/src/main/resources/theme/base/account/applications.ftl index bca5102368..45a253af07 100755 --- a/themes/src/main/resources/theme/base/account/applications.ftl +++ b/themes/src/main/resources/theme/base/account/applications.ftl @@ -27,9 +27,9 @@ <#list applications.applications as application> - <#if application.client.baseUrl??> + <#if application.effectiveUrl?has_content> <#if application.client.name??>${advancedMsg(application.client.name)}<#else>${application.client.clientId} - <#if application.client.baseUrl??> + <#if application.effectiveUrl?has_content> 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 b3f48291a6..47dbda14ba 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 @@ -163,4 +163,6 @@ locale_ja=\u65E5\u672C\u8A9E locale_no=Norsk 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 +locale_ru=\u0420\u0443\u0441\u0441\u043A\u0438\u0439 +locale_zh-CN=\u4e2d\u6587\u7b80\u4f53 +locale_sv=Svenska \ No newline at end of file diff --git a/themes/src/main/resources/theme/base/account/messages/messages_zh_CN.properties b/themes/src/main/resources/theme/base/account/messages/messages_zh_CN.properties new file mode 100644 index 0000000000..4691002972 --- /dev/null +++ b/themes/src/main/resources/theme/base/account/messages/messages_zh_CN.properties @@ -0,0 +1,164 @@ +doSave=保存 +doCancel=取消 +doLogOutAllSessions=登出所有会话 +doRemove=删除 +doAdd=添加 +doSignOut=登出 + +editAccountHtmlTitle=编辑账户 +federatedIdentitiesHtmlTitle=链接的身份 +accountLogHtmlTitle=账户日志 +changePasswordHtmlTitle=更改密码 +sessionsHtmlTitle=会话 +accountManagementTitle=Keycloak账户管理 +authenticatorTitle=认证者 +applicationsHtmlTitle=应用 + +authenticatorCode=一次性认证码 +email=电子邮件 +firstName=名 +givenName=姓 +fullName=全名 +lastName=姓 +familyName=姓 +password=密码 +passwordConfirm=确认 +passwordNew=新密码 +username=用户名 +address=地址 +street=街道 +locality=城市住所 +region=省,自治区,直辖市 +postal_code=邮政编码 +country=国家 +emailVerified=验证过的Email +gssDelegationCredential=GSS Delegation Credential + +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=读取 token +role_offline-access=离线访问 +role_uma_authorization=获取授权 +client_account=账户 +client_security-admin-console=安全管理终端 +client_admin-cli=管理命令行 +client_realm-management=域管理 +client_broker=代理 + + +requiredFields=必填项 +allFieldsRequired=所有项必填 + +backToApplication=« 回到应用 +backTo=回到 {0} + +date=日期 +event=事件 +ip=IP +client=客户端 +clients=客户端 +details=详情 +started=开始 +lastAccess=最后一次访问 +expires=过期时间 +applications=应用 + +account=账户 +federatedIdentity=关联身份 +authenticator=认证方 +sessions=会话 +log=日志 + +application=应用 +availablePermissions=可用权限 +grantedPermissions=授予权限 +grantedPersonalInfo=授权的个人信息 +additionalGrants=可授予的权限 +action=操作 +inResource=in +fullAccess=所有权限 +offlineToken=离线 token +revoke=收回授权 + +configureAuthenticators=配置的认证者 +mobile=手机 +totpStep1=在你的设备上安装 FreeOTP 或者 Google Authenticator.两个应用可以从 Google Play 和 Apple App Store下载。 +totpStep2=打开应用扫描二维码输入验证码 +totpStep3=输入应用提供的一次性验证码单击保存 + +missingUsernameMessage=请指定用户名 +missingFirstNameMessage=请指定名 +invalidEmailMessage=无效的电子邮箱地址 +missingLastNameMessage=请指定姓 +missingEmailMessage=请指定邮件地址 +missingPasswordMessage=请输入密码 +notMatchPasswordMessage=密码不匹配 + +missingTotpMessage=请指定认证者代码 +invalidPasswordExistingMessage=无效的旧密码 +invalidPasswordConfirmMessage=确认密码不相符 +invalidTotpMessage=无效的认证码 + +usernameExistsMessage=用户名已经存在 +emailExistsMessage=电子邮箱已经存在 + +readOnlyUserMessage=无法修改账户,因为它是只读的。 +readOnlyPasswordMessage=不可以更该账户因为它是只读的。 + +successTotpMessage=手机认证者配置完毕 +successTotpRemovedMessage=手机认证者已删除 + +successGrantRevokedMessage=授权成功回收 + +accountUpdatedMessage=您的账户已经更新 +accountPasswordUpdatedMessage=您的密码已经修改 + +missingIdentityProviderMessage=身份提供者未指定 +invalidFederatedIdentityActionMessage=无效或者缺少操作 +identityProviderNotFoundMessage=指定的身份提供者未找到 +federatedIdentityLinkNotActiveMessage=这个身份不再使用了。 +federatedIdentityRemovingLastProviderMessage=你不可以移除最后一个身份提供者因为你没有设置密码 +identityProviderRedirectErrorMessage=尝试重定向到身份提供商失败 +identityProviderRemovedMessage=身份提供商成功删除 +identityProviderAlreadyLinkedMessage=链接的身份 {0} 已经连接到已有用户。 +staleCodeAccountMessage=页面过期。请再试一次。 +consentDenied=不同意 + +accountDisabledMessage=账户已经关闭,请联系管理员 + +accountTemporarilyDisabledMessage=账户暂时关闭,请联系管理员或稍后再试。 +invalidPasswordMinLengthMessage=无效的密码:最短长度 {0}. +invalidPasswordMinLowerCaseCharsMessage=无效的密码: 至少包含 {0} 小写字母。 +invalidPasswordMinDigitsMessage=无效的密码: 至少包含 {0} 数字。 +invalidPasswordMinUpperCaseCharsMessage=无效的密码: 至少包含 {0} 大写字母 +invalidPasswordMinSpecialCharsMessage=无效的密码: 至少包含 {0} 个特殊字符 +invalidPasswordNotUsernameMessage=无效的密码: 不能与用户名相同 +invalidPasswordRegexPatternMessage=无效的密码: 无法与正则表达式匹配 +invalidPasswordHistoryMessage=无效的密码: 不能与之前的{0} 个旧密码相同 +locale_ca=Català +locale_de=Deutsch +locale_en=English +locale_es=Español +locale_fr=Français +locale_it=Italian +locale_ja=日本語 +locale_no=Norsk +locale_lt=Lietuvių +locale_pt-BR=Português (Brasil) +locale_ru=Русский +locale_zh-CN=中文简体 diff --git a/themes/src/main/resources/theme/base/account/template.ftl b/themes/src/main/resources/theme/base/account/template.ftl index c117cffda4..bc59407819 100644 --- a/themes/src/main/resources/theme/base/account/template.ftl +++ b/themes/src/main/resources/theme/base/account/template.ftl @@ -43,8 +43,8 @@
  • - <#if referrer?has_content && referrer.url?has_content>
  • ${msg("backTo",referrer.name)}
  • -
  • ${msg("doSignOut")}
  • + <#if referrer?has_content && referrer.url?has_content>
  • ${msg("backTo",referrer.name?html)}
  • +
  • ${msg("doSignOut")}
  • diff --git a/themes/src/main/resources/theme/base/account/theme.properties b/themes/src/main/resources/theme/base/account/theme.properties new file mode 100644 index 0000000000..b9c3990957 --- /dev/null +++ b/themes/src/main/resources/theme/base/account/theme.properties @@ -0,0 +1 @@ +locales=ca,de,en,es,fr,it,ja,lt,no,pt-BR,ru,zh-CN \ No newline at end of file diff --git a/themes/src/main/resources/theme/base/admin/index.ftl b/themes/src/main/resources/theme/base/admin/index.ftl index 771790465c..aebc488493 100755 --- a/themes/src/main/resources/theme/base/admin/index.ftl +++ b/themes/src/main/resources/theme/base/admin/index.ftl @@ -20,29 +20,46 @@ var consoleBaseUrl = '${consoleBaseUrl}'; var resourceUrl = '${resourceUrl}'; var masterRealm = '${masterRealm}'; + var resourceVersion = '${resourceVersion}'; - - + + + + + + + + + + + + + + + - - - - - - - - + - - + - - + diff --git a/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties b/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties index 28a985c516..f261105214 100644 --- a/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties +++ b/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties @@ -574,6 +574,17 @@ key=Key stackoverflow.key.tooltip=The Key obtained from Stack Overflow client registration. openshift.base-url=Base Url openshift.base-url.tooltip=Base Url to Openshift Online API +gitlab-application-id=Application Id +gitlab-application-secret=Application Secret +gitlab.application-id.tooltip=Application Id for the application you created in your GitLab Applications account menu +gitlab.application-secret.tooltip=Secret for the application that you created in your GitLab Applications account menu +gitlab.default-scopes.tooltip=Scopes to ask for on login. Will always ask for openid. Additionally adds api if you do not specify anything. + +bitbucket-consumer-key=Consumer Key +bitbucket-consumer-secret=Consumer Secret +bitbucket.key.tooltip=Bitbucket OAuth Consumer Key +bitbucket.secret.tooltip=Bitbucket OAuth Consumer Secret +bitbucket.default-scopes.tooltip=Scopes to ask for on login. If you do not specify anything, scope defaults to 'email'. # User federation sync-ldap-roles-to-keycloak=Sync LDAP Roles To Keycloak @@ -833,6 +844,8 @@ reset-credentials=Reset Credentials reset-credentials.tooltip=Select the flow you want to use when the user has forgotten their credentials. client-authentication=Client Authentication client-authentication.tooltip=Select the flow you want to use for authentication of clients. +docker-auth=Docker Authentication +docker-auth.tooptip=Select the flow you want to use for authenticatoin against a docker client. new=New copy=Copy add-execution=Add execution @@ -1327,6 +1340,8 @@ manage-permissions-group.tooltip=Fine grain permssions for admins that want to m manage-authz-group-scope-description=Policies that decide if an admin can manage this group view-authz-group-scope-description=Policies that decide if an admin can view this group view-members-authz-group-scope-description=Policies that decide if an admin can manage the members of this group +exchange-to-authz-client-scope-description=Policies that decide which clients are allowed exchange tokens for a token that is targeted to this client. +exchange-from-authz-client-scope-description=Policies that decide which clients are allowed to exchange tokens that were generated for this client. manage-authz-client-scope-description=Policies that decide if an admin can manage this client configure-authz-client-scope-description=Reduced management permissions for admin. Cannot set scope, template, or protocol mappers. view-authz-client-scope-description=Policies that decide if an admin can view this client diff --git a/themes/src/main/resources/theme/base/admin/messages/admin-messages_zh_CN.properties b/themes/src/main/resources/theme/base/admin/messages/admin-messages_zh_CN.properties new file mode 100644 index 0000000000..8d2dd08ac8 --- /dev/null +++ b/themes/src/main/resources/theme/base/admin/messages/admin-messages_zh_CN.properties @@ -0,0 +1,1227 @@ +consoleTitle=Keycloak管理界面 + +# Common messages +enabled=开启 +name=名称 +displayName=显示名称 +displayNameHtml=HTML 显示名称 +save=保存 +cancel=取消 +onText=开 +offText=关 +client=客户端 +clients=客户端 +clear=清除 +selectOne=选择一个... + +true=是 +false=否 + +endpoints=服务路径 + +# Realm settings +realm-detail.enabled.tooltip=只有当域启用时,用户和客户程序才能访问 +realm-detail.oidc-endpoints.tooltip=显示openID connect服务路径的配置 +registrationAllowed=用户注册 +registrationAllowed.tooltip=开启/关闭注册页面,注册页面的链接也会显示在登录页面 +registrationEmailAsUsername=Email当做用户名 +registrationEmailAsUsername.tooltip=当开启时注册表单的用户名域会被隐藏而且Email会作为新用户的用户名 +editUsernameAllowed=编辑用户名 +editUsernameAllowed.tooltip=如果开启,用户名域是可以编辑的。否则用户名域是只读的。 +resetPasswordAllowed=忘记密码 +resetPasswordAllowed.tooltip=当用户忘记他们的密码时,在登录界面显示一个连接给用户点击。 +rememberMe=记住我 +rememberMe.tooltip=显示一个选择框来让用户在重启浏览器时仍然处于登录状态,直到会话过期。 +verifyEmail=验证email +verifyEmail.tooltip=要求用户在首次登录时验证他们的邮箱。 +sslRequired=需要SSL +sslRequired.option.all=所有请求 +sslRequired.option.external=外部请求 +sslRequired.option.none=无 +sslRequired.tooltip=是否需要HTTPS?‘无’代表对于任何客户端IP地址都不需要HTTPS,‘外部请求’代表localhost和私有ip地址可以不通过https访问,‘所有请求’代表所有IP地址都需要通过https访问。 +publicKey=公有秘钥 +privateKey=私有秘钥 +gen-new-keys=生成新秘钥 +certificate=证书 +host=主机 +smtp-host=SMTP 主机 +port=端口号 +smtp-port=SMTP 端口号(默认25) +from=来自 +sender-email-addr=邮件发送者email地址 +enable-ssl=启用 SSL +enable-start-tls=启用 StartTLS +enable-auth=启用认证 +username=用户名 +login-username=登录用户名 +password=密码 +login-password=登录密码 +login-theme=登录主题 +login-theme.tooltip=为登录、授权、注册、忘记密码界面选择页面主题 +account-theme=账户主题 +account-theme.tooltip=为用户管理界面选择主题 +admin-console-theme=管理员控制台主题 +select-theme-admin-console=为管理员控制台选择主题 +email-theme=邮件主题 +select-theme-email=为服务器发送的邮件选择主题 +i18n-enabled=启用国际化 +supported-locales=支持的语言 +supported-locales.placeholder=输入一个locale并按回车 +default-locale=默认语言 +realm-cache-clear=域缓存 +realm-cache-clear.tooltip=从域缓存中清理所有条目(这会清理所有域的条目) +user-cache-clear=用户缓存 +user-cache-clear.tooltip=清理用户缓存的所有条目(这会清理所有域中的条目) +revoke-refresh-token=收回 Refresh Token +revoke-refresh-token.tooltip=如果开启 refresh tokens只能使用一次,否则refresh token不会被收回并且可以使用多次 +sso-session-idle=SSO会话空闲时间 +seconds=秒 +minutes=分 +hours=小时 +days=天 +sso-session-max=SSO会话最长时间 +sso-session-idle.tooltip=设置会话在过期之前可以空闲的时间长度,当会话过期时 Token 和浏览器会话都会被设置为无效。 +sso-session-max.tooltip=会话的最大时间长度,当会话过期时 Token 和浏览器会话都会被设置为无效。 +offline-session-idle=离线会话的空闲时间 +offline-session-idle.tooltip=离线会话允许的空闲时间。你需要使用离线Token在这段时间内至少刷新一次否则会话就会过期 +access-token-lifespan=Access Token 有效期 +access-token-lifespan.tooltip=access token最长有效时间,这个值推荐要比SSO超时要短一些。 +access-token-lifespan-for-implicit-flow =隐式流的访问令牌生命周期 +access-token-lifespan-for-implicit-flow.tooltip =在OpenID连接隐式流期间发出的访问令牌到期之前的最长时间。建议该值小于SSO超时。没有可能在隐式流期间刷新令牌,这就是为什么有单独的超时不同于“访问令牌寿命”。 +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从已声明的内容类型中嗅探响应(点击标签了解更多信息) +max-login-failures =最大登录失败 +max-login-failures.tooltip =触发等待之前的失败次数。 +wait-increment =等待增量 +wait-increment.tooltip =当满足故障阈值时,用户应该锁定多长时间? +quick-login-check-millis =快速登录检查Milli秒 +quick-login-check-millis.tooltip =如果故障同时发生太快,则锁定用户。 +min-quick-login-wait =最小快速登录等待 +min-quick-login-wait.tooltip =快速登录失败后等待多长时间。 +max-wait = Max Wait +max-wait.tooltip =用户将被锁定的最长时间。 +failure-reset-time =故障复位时间 +failure-reset-time.tooltip =何时将故障计数复位? +realm-tab-login=登录 +realm-tab-keys=秘钥 +realm-tab-email=Email +realm-tab-themes=主题 +realm-tab-cache=缓存 +realm-tab-tokens=Tokens +realm-tab-client-registration=客户端注册 +realm-tab-security-defenses=安全防护 +realm-tab-general=通用 +add-realm=添加域 + +#Session settings +realm-sessions=域会话 +revocation=回收 +logout-all=登出所有 +active-sessions=活跃的会话 +sessions=会话 +not-before=不早于 +not-before.tooltip=回收早于日期授予的token +set-to-now=设置到现在 +push=推送 +push.tooltip=对于每个拥有管理员权限的用户,通知他们新的回收策略 +#Protocol Mapper +usermodel.prop.label=属性 +usermodel.prop.tooltip=UserModel 接口中属性方法的名字. 例如, 'email' 会引用UserModel.getEmail() 方法. +usermodel.attr.label=用户属性 +usermodel.attr.tooltip=在UserModel.attribute映射中定义的存储的用户属性名。 +userSession.modelNote.label=用户会话标记 +userSession.modelNote.tooltip=用户会话标记在 UserSessionModel.note映射中的属性名。 +multivalued.label=多值的 +multivalued.tooltip=表示此值是否支持多值.如果为真,所有值会设置为已知。如果为假,只有第一个值是已知。 +selectRole.label=选择角色 +selectRole.tooltip=在左边文本框输入角色或点击这个按钮浏览并选择您想要的角色。 +tokenClaimName.label=Token申请名 +tokenClaimName.tooltip=token中加入的申请者名. 这可以是个完整的分级信息例如 'address.street'. 这种情况下,会生成一个复杂的json回复 +jsonType.label=申请 JSON 的类型 +jsonType.tooltip=用来展现申请的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 = Realm角色前缀 +usermodel.realmRoleMapping.rolePrefix.tooltip =每个领域角色的前缀(可选)。 +sectorIdentifierUri.label =扇区标识符URI +sectorIdentifierUri.tooltip =使用成对子值和支持的提供程序动态客户端注册应使用sector_identifier_uri参数。它为一组在共同管理控制下的网站提供了一种独立于各个域名的具有一致的成对子值的方法。它还为客户端更改redirect_uri域而不必重新注册其所有用户提供了一种方法。 +pairwiseSubAlgorithmSalt.label = Salt +pairwiseSubAlgorithmSalt.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,这也是authn请求的预期发放者值 +client.name.tooltip =指定客户端的显示名称。例如“我的客户端”。支持本地化值的键。例如\\uff1a$ {my_client} +client.enabled.tooltip =禁用客户端无法启动登录或获取访问令牌。 +consent-required =同意必需 +consent-required.tooltip =如果已启用的用户必须同意客户端访问。 +client-protocol =客户端协议 +client-protocol.tooltip ='OpenID connect'允许客户端基于授权服务器执行的认证来验证最终用户的身份。'SAML'启用基于Web的身份验证和授权方案,包括跨域单点登录(SSO),并使用包含断言的安全令牌传递信息。 +access-type =访问类型 +access-type.tooltip ='机密'客户端需要一个秘密启动登录协议。 “公共”客户不需要一个秘密。 “仅承载”客户端是从不启动登录的Web服务。 +standard-flow-enabled =启用标准流程 +standard-flow-enabled.tooltip =这使标准的基于OpenID Connect重定向的身份验证与授权码。根据OpenID Connect或OAuth2规范,这将支持此客户端的“授权代码流”。 +implicit-flow-enabled =启用隐式流 +implicit-flow-enabled.tooltip =这启用对无授权代码的基于OpenID Connect重定向的身份验证的支持。根据OpenID Connect或OAuth2规范,这将支持此客户端的“隐式流”。 +direct-access-grants-enabled =启用直接访问授权 +direct-access-grants-enabled.tooltip =这启用对直接访问授权的支持,这意味着客户端可以访问用户的用户名/密码,并直接与Keycloak服务器交换访问令牌。在OAuth2规范方面,这允许支持此客户端的“资源所有者密码凭据授权”。 +service-accounts-enabled =启用服务帐户 +service-accounts-enabled.tooltip =允许您向Keycloak验证此客户端并检索专用于此客户端的访问令牌。在OAuth2规范方面,这将支持此客户端的“客户端凭据授予”。 +include-authnstatement = Include AuthnStatement +include-authnstatement.tooltip =是否应该在登录响应中包含指定方法和时间戳的语句? +sign-documents =签署文件 +sign-documents.tooltip = SAML文档是否应该由领域签名? +sign-documents-redirect-enable-key-info-ext =优化REDIRECT签名密钥查找 +sign-documents-redirect-enable-key-info-ext.tooltip =在由Keycloak适配器保护的SP的REDIRECT绑定中签名SAML文档时,如果签名密钥的ID包含在元素中的SAML协议消息中?这将优化签名的验证,因为验证方使用单个密钥,而不是尝试每个已知密钥进行验证。 +sign-assertions =符号断言 +sign-assertions.tooltip = SAML文档中的断言是否应该签名?如果文档已签署,则不需要此设置。 +signature-algorithm =签名算法 +signature-algorithm.tooltip =用于签署文档的签名算法。 +canonicalization-method =规范化方法 +canonicalization-method.tooltip = XML签名的规范化方法。 +encrypt-assertions =加密断言 +encrypt-assertions.tooltip =是否应使用AES通过客户端的公钥对SAML断言进行加密? +client-signature-required =需要客户端签名 +client-signature-required.tooltip =客户端是否签署了saml请求和响应?他们应该验证吗? +force-post-binding =强制POST绑定 +force-post-binding.tooltip =始终对POST响应使用POST绑定。 +front-channel-logout =前通道注销 +front-channel-logout.tooltip =当为true时,注销需要浏览器重定向到客户端。当为false时,服务器对注销执行后台调用。 +force-name-id-format =强制名称ID格式 +force-name-id-format.tooltip =忽略请求的NameID主题格式并使用管理控制台配置的。 +name-id-format =名称ID格式 +name-id-format.tooltip =要用于主题的名称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的,如果没有指定,则使用auth服务器根URL。对于SAML,如果您依赖嵌入登录请求的使用者服务URL,则必须设置有效的URI模式。 +base-url.tooltip =当auth服务器需要重定向或链接回客户端时使用的默认URL。 +admin-url =管理员网址 +admin-url.tooltip =客户端管理界面的URL。如果客户端支持适配器REST API,请设置此选项。此REST API允许auth服务器推送吊销策略和其他管理任务。通常将此设置为客户端的基本URL。 +master-saml-processing-url =主SAML处理URL +master-saml-processing-url.tooltip =如果配置,此URL将用于每次绑定到SP的断言使用者和单一注销服务。这可以对细粒度SAML端点配置中的每个绑定和服务单独进行覆盖。 +idp-sso-url-ref = IDP发起的SSO URL名称 +idp-sso-url-ref.tooltip =当您想要进行IDP发起的SSO时,引用客户端的URL片段名称。留下此空将禁用IDP启动的SSO。您将从浏览器引用的URL为:{server-root} / realms / {realm} / protocol / saml / clients / {client-url-name} +idp-sso-relay-state = IDP发起的SSO中继状态 +idp-sso-relay-state.tooltip =当您想要执行IDP发起的SSO时,要使用SAML请求发送的中继状态。 +web-origins = Web起源 +web-origins.tooltip =允许的CORS起点。要允许有效重定向URI的所有来源,请添加“+”。允许所有起点添加'*'。 +fine-oidc-endpoint-conf = Fine Grain OpenID连接配置 +fine-oidc-endpoint-conf.tooltip =展开此部分以配置与OpenID Connect协议相关的此客户端的高级设置 +user-info-signed-response-alg =用户信息签名的响应算法 +user-info-signed-response-alg.tooltip =用于签名的用户信息端点响应的JWA算法。如果设置为“unsigned”,则用户信息响应将不会被签名,并将以application / json格式返回。 +request-object-signature-alg =请求对象签名算法 +request-object-signature-alg.tooltip = JWA算法,客户端在发送由'request'或'request_uri'参数指定的OIDC请求对象时需要使用。如果设置为“any”,则Request对象可以由任何算法(包括“none”)签名。 +fine-saml-endpoint-conf =细粒度SAML端点配置 +fine-saml-endpoint-conf.tooltip =展开此部分以配置Assertion Consumer和单一注销服务的确切URL。 +assertion-consumer-post-binding-url =断言使用者服务POST绑定URL +assertion-consumer-post-binding-url.tooltip = SAML POST绑定客户端断言使用者服务的URL(登录响应)。如果您没有此绑定的URL,则可以将此字段留空。 +assertion-consumer-redirect-binding-url =断言使用者服务重定向绑定URL +assertion-consumer-redirect-binding-url.tooltip = SAML重定向客户端断言使用者服务的绑定URL(登录响应)。如果您没有此绑定的URL,则可以将此字段留空。 +logout-service-post-binding-url =注销服务POST绑定URL +logout-service-post-binding-url.tooltip = SAML POST绑定客户端单一注销服务的URL。如果使用不同的绑定,则可以将此留空 +logout-service-redir-binding-url =注销服务重定向绑定URL +logout-service-redir-binding-url.tooltip = SAML重定向客户端单一注销服务的绑定URL。如果使用不同的绑定,则可以将此留空。 + +#client import +import-client =导入客户端 +format-option =格式选项 +select-format =选择格式 +import-file =导入文件 + +#client tabs +settings =设置 +credentials =凭据 +saml-keys = SAML键 +roles =角色 +mappers = 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 =由客户端发出并由客户端私钥签署的validate JWT的公钥。 +no-client-certificate-configured =未配置客户端证书 +gen-new-keys-and-cert =生成新密钥和证书 +import-certificate =导入证书 +gen-client-private-key =生成客户端私钥 +generate-private-key =生成私钥 +kid =孩子 +kid.tooltip =来自导入的JWKS的客户端公钥的KID(密钥ID)。 +use-jwks-url =使用JWKS URL +use-jwks-url.tooltip =如果开关打开,那么将从给定的JWKS URL下载客户端公钥。这允许很大的灵活性,因为当客户端生成新的密钥对时,新密钥将总是重新下载。如果交换机关闭,则使用来自Keycloak DB的公钥(或证书),因此当客户端密钥更改时,您总是需要将新密钥(或证书)导入到Keycloak数据库。 +jwks-url = JWKS URL +jwks-url.tooltip =存储JWK格式的客户端密钥的URL。有关更多详细信息,请参阅JWK规范。如果您使用带有“jwt”凭据的keycloak客户端适配器,那么您可以使用带有'/ k_jwks'后缀的应用程序的URL。例如“http://www.myhost.com/myapp/k_jwks”。 +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 = Composite +description =描述 +no-client-roles-available =没有可用的客户端角色 +scope-param-required = Scope Param必需 +scope-param-required.tooltip =只有在授权/令牌请求期间使用具有角色名称的scope参数时,才会授予此角色。 +composite-roles =复合角色 +composite-roles.tooltip =当将此角色(un)分配给用户时,与其关联的任何角色将被隐式分配(un)。 +realm-roles = Realm角色 +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 = Category +type = 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 =从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 = Realm证书也存储在归档中。这是它的别名。 +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 =别名 +display-name =显示名称 +identity-provider.alias.tooltip =别名唯一标识身份提供者,它也用于构建重定向uri。 +identity-provider.display-name.tooltip =身份提供者的友好名称。 +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角色。 +disableUserInfo =禁用用户信息 +identity-provider.disableUserInfo.tooltip =禁用用户信息服务的使用以获取其他用户信息?默认是使用此OIDC服务。 +userIp =使用userIp参数 +identity-provider.google-userIp.tooltip =在Google的用户信息服务上调用时设置'userIp'查询参数。这将使用用户的IP地址。如果Google正在限制对用户信息服务的访问,则此选项非常有用。 +update-profile-on-first-login =首次登录时更新配置文件 +on =开 +on-missing-info =缺少信息 +off =关闭 +update-profile-on-first-login.tooltip =定义用户在首次登录期间必须更新其配置文件的条件。 +trust-email =信任电子邮件 +trust-email.tooltip =如果启用,则此提供商提供的电子邮件不会验证,即使已启用对领域的验证。 +gui-order.tooltip = GUI中提供者的定义顺序的数字(例如,在登录页面上)。 +first-broker-login-flow.tooltip =认证流的别名,在首次使用此身份提供者登录后触发。术语“首次登录”意味着尚未存在与认证身份提供商帐户链接的Keycloak帐户。 +post-broker-login-flow.tooltip =认证流的别名,在每次使用此身份提供程序登录后触发。如果您需要对通过此身份提供程序(例如OTP)验证的每个用户进行额外验证,这将非常有用。如果您不希望在使用此身份提供商登录后触发任何其他验证器,请将此空白留空。还要注意,认证者实现必须假定用户已经在ClientSession中设置为身份提供者已经设置。 +openid-connect-config = OpenID连接配置 +openid-connect-config.tooltip = OIDC SP和外部IDP配置。 +authorization-url =授权URL +authorization-url.tooltip =授权网址。 +token-url =令牌URL +token-url.tooltip =令牌URL。 +logout-url =注销URL +identity-provider.logout-url.tooltip =用于从外部IDP注销用户的会话终结点。 +backchannel-logout = Backchannel注销 +backchannel-logout.tooltip =外部IDP是否支持反向通道注销? +user-info-url =用户信息URL +user-info-url.tooltip =用户信息网址。这是可选的。 +identity-provider.client-id.tooltip =在身份提供者中注册的客户端或客户端标识符。 +client-secret =客户端密钥 +show-secret =显示密码 +hide-secret =隐藏秘密 +client-secret.tooltip =在身份提供程序中注册的客户端或客户端机密。 +issuer =发行人 +issuer.tooltip =响应的发行者的发行者标识符。如果未提供,将不执行验证。 +default-scopes =默认范围 +identity-provider.default-scopes.tooltip =在请求授权时要发送的作用域。它可以是以空格分隔的范围列表。默认为'openid'。 +prompt =提示 +unspecified.option =未指定 +none.option = none +consent.option =同意 +login.option = login +select-account.option = select_account +prompt.tooltip =指定授权服务器是否提示最终用户重新认证和同意。 +validate-signatures =验证签名 +identity-provider.validate-signatures.tooltip =启用/禁用外部IDP签名的签名验证。 +identity-provider.use-jwks-url.tooltip =如果交换机打开,那么将从给定的JWKS URL下载身份提供程序公钥。这允许很大的灵活性,因为当身份提供商生成新的密钥对时,新密钥将总是被重新下载。如果交换机关闭,则使用来自Keycloak DB的公钥(或证书),因此当身份提供商密钥更改时,您始终需要将新密钥导入到Keycloak数据库。 +identity-provider.jwks-url.tooltip =存储JWK格式的身份提供者密钥的URL。有关更多详细信息,请参阅JWK规范。如果你使用外部keycloak身份提供者,那么你可以使用像http:// broker-keycloak:8180 / auth / realms / test / protocol / openid-connect / certs这样的URL,假设你的代理keycloak是运行在http: / broker-keycloak:8180',它的境界是'test'。 +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 =必须用于发送注销请求的网址。 +nameid-policy-format = NameID策略格式 +nameid-policy-format.tooltip =指定与名称标识符格式相对应的URI引用。默认为urn:oasis:names:tc:SAML:2.0:nameid-format:persistent。 +http-post-binding-response = HTTP-POST绑定响应 +http-post-binding-response.tooltip =指示是否使用HTTP-POST绑定响应请求。如果为false,将使用HTTP-REDIRECT绑定。 +http-post-binding-for-authn-request = HTTP-POST AuthnRequest的绑定 +http-post-binding-for-authn-request.tooltip =指示是否必须使用HTTP-POST绑定发送AuthnRequest。如果为false,将使用HTTP-REDIRECT绑定。 +want-authn-requests-signed =需要AuthnRequests签名 +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客户端注册获取的密钥。 + +# 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 = Realm + +identity-provider-mappers =身份提供者映射器 +create-identity-provider-mapper =创建身份提供者映射器 +add-identity-provider-mapper =添加身份提供者映射器 +client.description.tooltip =指定客户端的描述。例如“我的客户端的时间表”。支持本地化值的键。例如\\uff1a$ {my_client_description} + +expires =到期 +expiration =到期 +expiration.tooltip =指定令牌有效的时间 +count = Count +count.tooltip =指定可以使用令牌创建多少个客户端 +remainingCount =剩余计数 +created =已创建 +back =返回 +initial-access-tokens=初始接入令牌 +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 =没有初始访问令牌可用 + +client-reg-policies =客户端注册策略 +client-reg-policy.name.tooltip =显示策略的名称 +anonymous-policies =匿名访问策略 +anonymous-policies.tooltip =当客户端注册服务由未经身份验证的请求调用时,使用这些策略。这意味着请求不包含初始接入令牌或承载令牌。 +auth-policies =验证的访问策略 +auth-policies.tooltip =当通过认证请求调用客户端注册服务时使用这些策略。这意味着请求包含初始接入令牌或承载令牌。 +policy-name =策略名称 +no-client-reg-policies-configured =无客户端注册策略 +trusted-hosts.label =受信任的主机 +trusted-hosts.tooltip =主机列表,它们是受信任的,并且允许调用客户端注册服务和/或用作客户端URI的值。您可以使用主机名或IP地址。如果您在开头使用星号(例如“* .example.com”),则整个域example.com将受信任。 +host-sending-registration-request-must-match.label =主机发送客户端注册请求必须匹配 +host-sending-registration-request-must-match.tooltip =如果开启,则只要客户端注册服务是从某个受信任的主机或域发送的,就允许任何请求。 +client-uris-must-match.label =客户端URI必须匹配 +client-uris-must-match.tooltip =如果启用,则所有客户端URI(重定向URI和其他)只有在它们匹配一些受信任的主机或域时才允许。 +allowed-protocol-mappers.label =允许的协议映射器 +allowed-protocol-mappers.tooltip =允许的协议映射器提供程序的白名单。如果尝试注册客户端,其中包含一些未列入白名单的协议映射器,则注册请求将被拒绝。 +consent-required-for-all-mappers.label =需要同意Mappers +consent-required-for-all-mappers.tooltip =如果打开,则所有新注册的协议映射器将自动具有consentRequired开启。这意味着用户将需要批准同意屏幕。注意:只有在客户端已启用consentRequired开关时,才会显示同意屏幕。所以通常很好地使用这个开关与需要同意的政策。 +allowed-client-templates.label =允许的客户端模板 +allowed-client-templates.tooltip =客户端模板的白名单,可以在新注册的客户端上使用。尝试向某个未列入白名单的客户端模板注册客户端将被拒绝。默认情况下,白名单为空,因此不允许任何客户端模板。 +max-clients.label =每个领域的最大客户端 +max-clients.tooltip =如果域中现有客户端的数量等于或大于配置的限制,将不允许注册新客户端。 + +client-templates =客户端模板 +client-templates.tooltip =客户机模板允许您定义在多个客户机之间共享的公共配置 + +groups =组 + +group.add-selected.tooltip =可以分配给组的领域角色。 +group.assigned-roles.tooltip =映射到组的Realm角色 +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 =映射到用户的Realm角色 +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默认角色 +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 = 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 = 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 +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存储。可写意味着数据将按需同步回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 = LDAP属性的名称,映射为Keycloak用户名。对于许多LDAP服务器供应商,它可以是“uid”。对于活动目录,可以是“sAMAccountName”或“cn”。应该为要从LDAP导入到Keycloak的所有LDAP用户记录填充该属性。 +rdn-ldap-attribute = RDN LDAP属性 +ldap-attribute-name-for-user-rdn =用户RDN的LDAP属性名称 +rdn-ldap-attribute.tooltip = LDAP属性的名称,用作典型用户DN的RDN(top属性)。通常它与用户名LDAP属性相同,但不是必需的。例如对于Active目录,当username属性可能是“sAMAccountName”时,通常使用“cn”作为RDN属性。 +uuid-ldap-attribute = UUID LDAP属性 +ldap-attribute-name-for-uuid = UUID的LDAP属性名称 +uuid-ldap-attribute.tooltip = LDAP属性的名称,用作LDAP中对象的唯一对象标识符(UUID)。对于许多LDAP服务器供应商,它的'entryUUID',但有些是不同的。例如对于Active目录,它应该是'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绑定DN +ldap-bind-credentials = LDAP绑定凭据 +ldap-filter = LDAP过滤器 +ldap.user-object-classes.tooltip = LDAP中用户的LDAP objectClass属性的所有值除以逗号。例如:'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用户的父级。它可以是例如'ou = users,dc = example,dc = com',假设您的典型用户将有DN像'uid = john,ou = users,dc = example,dc = com' +authentication-type =认证类型 +ldap.authentication-type.tooltip = LDAP认证类型。现在只有“无”(匿名LDAP身份验证)或“简单”(绑定凭据+绑定密码身份验证)机制可用 +bind-dn =绑定DN +ldap.bind-dn.tooltip = LDAP管理员的DN,Keycloak将使用它来访问LDAP服务器 +bind-credential =绑定凭据 +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 =对于一个级别,我们仅在用户DN指定的DN中搜索用户。对于子树,我们搜索整个他们的子树。有关更多详细信息,请参阅LDAP文档 +use-truststore-spi =使用Truststore SPI +ldap.use-truststore-spi.tooltip =指定LDAP连接是否将使用具有在standalone.xml / domain.xml中配置的信任库的truststore SPI。 “永远”意味着它总是使用它。 “从不”意味着它不会使用它。 '只有ldaps'意味着它将使用,如果你的连接URL使用ldaps。即使未配置standalone.xml / domain.xml,也将使用由“javax.net.ssl.trustStore”属性指定的缺省Java cacerts或证书。 +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 =使用Kerberos登录模块用于针对Kerberos服务器的身份验证用户名/密码,而不是使用Directory Service API对LDAP服务器进行身份验证 +batch-size =批量大小 +ldap.batch-size.tooltip =在单个事务中要从LDAP导入到Keycloak的LDAP用户的计数。 +ldap.periodic-full-sync.tooltip =是否应该启用LDAP用户到Keycloak的周期性完全同步 +ldap.periodic-changed-users-sync.tooltip =应该启用更改的或新创建的LDAP用户到Keycloak的周期性同步 +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 =添加执行 +add-flow =添加流 +auth-type =认证类型 +requirement=需求 +config = Config +no-executions-available =没有可用的执行 +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 / **”匹配主域中的任何内容。 +date-(from)= Date(From) +date-(to)=日期(To) +authentication-details =验证详细信息 +ip-address = IP地址 +time=时间 +operation-type =操作类型 +resource-type =资源类型 +auth = Auth +representation =表示 +register=寄存器 +required-action =必需操作 +default-action =默认操作 +auth.default-action.tooltip =如果启用,任何新用户都将分配此必需操作。 +no-required-actions-configured =未配置所需的操作 +defaults-to-id =默认为id +flows=流量 +bindings =绑定 +required-actions =必需操作 +password-policy =密码策略 +otp-policy = OTP策略 +user-groups =用户组 +default-groups =默认组 +groups.default-groups.tooltip =新用户将自动加入的组的集合。 +cut = Cut +paste =粘贴 + +create-group =创建组 +create-authenticator-execution =创建Authenticator执行 +create-form-action-execution =创建表单操作执行 +create-top-level-form =创建顶级表单 +flow.alias.tooltip =指定流的显示名称。 +top-level-flow-type =顶级流类型 +flow.generic = generic +flow.client = client +top-level-flow-type.tooltip =什么样的顶层流是什么?类型“客户端”用于客户端(应用程序)的认证,当通用是为用户和其他 +create-execution-flow =创建执行流 +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 = Value +table-of-group-members =组成员表 +last-name =姓氏 +first-name =名字 +email =电子邮件 +toggle-navigation =切换导航 +manage-account =管理帐户 +sign-out=退出 +server-info =服务器信息 +resource-not-found =资源未找到 ... +resource-not-found.instruction =我们找不到您要找的资源。请确保您输入的网址正确无误。 +go-to-the-home-page =前往首页&raquo; +page-not-found =页面未找到 ... +page-not-found.instruction =我们找不到您要寻找的页面。请确保您输入的网址正确无误。 +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 =包含表示 +include-representation.tooltip =包含用于创建和更新请求的JSON表示。 +clear-admin-events.tooltip =删除数据库中的所有管理事件。 +server-version =服务器版本 +server-profile =服务器配置文件 +info =信息 +providers =提供者 +server-time =服务器时间 +server-uptime =服务器正常运行时间 +memory =内存 +total-memory =总内存 +free-memory =可用内存 +used-memory =使用的内存 +system = System +current-working-directory =当前工作目录 +java-version = Java版本 +java-vendor = Java供应商 +java-runtime = Java运行时 +java-vm = Java VM +java-vm-version = Java虚拟机版本 +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 =附加赠款 +consent-created-date =创建 +consent-last-updated-date =最后更新 +revoke =撤消 +new-password =新密码 +password-confirmation =密码确认 +reset-password =重置密码 +credentials.temporary.tooltip =如果启用,用户需要在下次登录时更改密码 +remove-totp =删除TOTP +credentials.remove-totp.tooltip =为用户删除一次性密码生成器。 +reset-actions =复位操作 +credentials.reset-actions.tooltip =发送用户重置操作电子邮件时要执行的操作的集合。 “验证电子邮件”向用户发送电子邮件以验证其电子邮件地址。 “更新个人资料”要求用户输入新的个人信息。 “更新密码”要求用户输入新密码。 '配置TOTP'需要设置移动密码生成器。 +reset-actions-email =重置操作电子邮件 +send-email =发送电子邮件 +credentials.reset-actions-email.tooltip =向具有嵌入链接的用户发送电子邮件。单击链接将允许用户执行重置操作。他们不必在此之前登录。例如,将操作设置为更新密码,单击此按钮,用户将无需登录即可更改其密码。 +add-user =添加用户 +created-at =创建于 +user-enabled =用户已启用 +user-enabled.tooltip =禁用的用户无法登录。 +user-temporarily-locked =用户临时锁定 +user-temporarily-locked.tooltip =用户可能由于无法登录太多次而被锁定。 +unlock-user =解锁用户 +federation-link =联合链接 +email-verified =电子邮件验证 +email-verified.tooltip =用户的电子邮件经过验证吗? +required-user-actions =必需的用户操作 +required-user-actions.tooltip =需要用户登录时的操作。“验证电子邮件”向用户发送电子邮件以验证其电子邮件地址。 “更新个人资料”要求用户输入新的个人信息。 “更新密码”要求用户输入新密码。 '配置TOTP'需要设置移动密码生成器。 +locale =语言环境 +select-one.placeholder =选择一个... +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 =表的Realm用户 +view-all-users =查看所有用户 +unlock-users =解锁用户 +no-users-available =没有可用的用户 +users.instruction =请输入搜索,或点击查看所有用户 +consents=同意 +started =开始 +logout-all-sessions =注销所有会话 +logout =注销 +new-name =新名称 +ok =好的 +attributes =属性 +role-mappings =角色映射 +members =成员 +details =详细 +identity-provider-links =身份提供者链接 +register-required-action =注册所需的操作 +gender =性别 +address = Address +phone =电话 +profile-url =个人资料网址 +picture-url =图片网址 +website =网站 +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 = 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”表示即使没有与给定资源相关联的策略也允许请求。 “禁用”完全禁用策略的评估,并允许访问任何资源。 +authz-policy-enforcement-mode-enforcing =强制 +authz-policy-enforcement-mode-permissive = Permissive +authz-policy-enforcement-mode-disabled =禁用 + +authz-remote-resource-management =远程资源管理 +authz-remote-resource-management.tooltip =资源服务器是否应该远程管理资源?如果为false,则只能从此管理控制台管理资源。 + +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=Add Scope +authz-no-scopes-available=No scopes available. + +#Authz作用域详细信息 +authz-scope-name.tooltip =此作用域的唯一名称。该名称可用于唯一标识范围,在查询特定范围时很有用。 + +#Authz策略列表 +authz-all-types =所有类型 +authz-create-policy =创建策略 +authz-no-policies-available =没有可用的策略。 + +#Authz策略详细信息 +authz-policy-name.tooltip =此策略的名称。 +authz-policy-description.tooltip =此策略的描述。 +authz-policy-logic =逻辑 +authz-policy-logic-positive =肯定 +authz-policy-logic-negative = Negative +authz-policy-logic.tooltip =逻辑决定如何进行策略决策。如果为“积极”,则在评估本政策期间获得的效果(许可或拒绝)将用于执行决策。如果为“否定”,则所得的效果将被否定,换句话说,许可证变为拒绝,反之亦然。 +authz-policy-apply-policy =应用策略 +authz-policy-apply-policy.tooltip =指定必须应用于此策略或权限定义的范围的所有策略。 +authz-policy-decision-strategy =决策策略 +authz-policy-decision-strategy.tooltip =决策策略规定如何评估与给定权限相关联的策略以及如何获得最终决策。 “肯定”意味着至少一个政策必须评估为积极的决定,以使最终决定也是积极的。 “一致”是指所有政策必须评估为一个积极的决定,以使最终决定也是积极的。 “共识”意味着积极决策的数量必须大于负面决策的数量。如果正数和负数相同,最终决定将为负数。 +authz-policy-decision-strategy-affirmative =肯定 +authz-policy-decision-strategy-unanimous =一致 +authz-policy-decision-strategy-consensus=共识 +authz-select-a-policy =选择一个策略 + +#Authz角色策略详细信息 +authz-add-role-policy =添加角色策略 +authz-no-roles-assigned =未分配角色。 +authz-policy-role-realm-roles.tooltip =指定此策略允许的* realm *角色。 +authz-policy-role-clients.tooltip =选择客户端以过滤可应用于此策略的客户端角色。 +authz-policy-role-client-roles.tooltip =指定此策略允许的客户端角色。 + +#Authz用户策略详细信息 +authz-add-user-policy =添加用户策略 +authz-no-users-assigned =未分配用户。 +authz-policy-user-users.tooltip =指定此策略允许哪些用户。 + +#Authz时间策略详细信息 +authz-add-time-policy =添加时间策略 +authz-policy-time-not-before.tooltip =定义不得授予策略的时间。仅当当前日期/时间晚于或等于此值时才被授予。 +authz-policy-time-not-on-after =不开或之后 +authz-policy-time-not-on-after.tooltip =定义不能授予策略的时间。仅当当前日期/时间在此值之前或之前时才被授予。 +authz-policy-time-day-month =日期 +authz-policy-time-day-month.tooltip =定义必须授予策略的月份日期。您还可以通过填充第二个字段来提供范围。在这种情况下,只有当月的当天介于或等于您提供的两个值之后,才会授予权限。 +authz-policy-time-month = month +authz-policy-time-month.tooltip =定义必须授予策略的月份。您还可以通过填充第二个字段来提供范围。在这种情况下,仅当当前月份介于或等于您提供的两个值之间时才会授予权限。 +authz-policy-time-year =年 +authz-policy-time-year.tooltip =定义策略必须授予的年份。您还可以通过填充第二个字段来提供范围。在这种情况下,仅当当前年份介于或等于您提供的两个值之间时才会授予权限。 +authz-policy-time-hour =小时 +authz-policy-time-hour.tooltip =定义策略必须被授予的小时。您还可以通过填充第二个字段来提供范围。在这种情况下,只有当前小时介于或等于您提供的两个值之间时才会授予权限。 +authz-policy-time-minute =分钟 +authz-policy-time-minute.tooltip =定义策略必须被授予的分钟。您还可以通过填充第二个字段来提供范围。在这种情况下,仅当当前分钟介于或等于您提供的两个值之间时才会授予权限。 + +#Authz Drools策略详细信息 +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,您可以点击* Resolve *来加载* Module *和* Session *字段。 +authz-policy-drools-module = 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策略详细信息 +authz-add-js-policy =添加JavaScript策略 +authz-policy-js-code =代码 +authz-policy-js-code.tooltip =提供此策略条件的JavaScript代码。 + + +#Authz聚合策略详细信息 +authz-aggregated=聚合 +authz-add-aggregation-policy =添加聚合策略 + +#Authz权限列表 +authz-no-permissions-available =没有可用的权限。 + +#Authz权限详细信息 +authz-permission-name.tooltip =此权限的名称。 +authz-permission-description.tooltip =此权限的描述。 + +#Authz资源许可详细信息 +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 =指定此权限必须应用于一个或多个作用域。 + +# 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向客户端请求权限的问题。针对根据当前授权请求授予的权限,检查“授权”声明。 +authz-show-authorization-data =显示授权数据 + +keys=秘钥 +all=所有 +status=状态 +keystore=钥匙链 +keystores= 钥匙链 +add-keystore=添加 钥匙链 +add-keystore.placeholder=添加 钥匙链... +view=查看 +active=活跃 + +Sunday=星期天 +Monday=星期一 +Tuesday=星期二 +Wednesday=星期三 +Thursday=星期四 +Friday=星期五 +Saturday=星期六 + +user-storage-cache-policy=缓存设置 +userStorage.cachePolicy=缓存策略 +userStorage.cachePolicy.option.DEFAULT=默认 +userStorage.cachePolicy.option.EVICT_WEEKLY=EVICT_WEEKLY +userStorage.cachePolicy.option.EVICT_DAILY=EVICT_DAILY +userStorage.cachePolicy.option.MAX_LIFESPAN=MAX_LIFESPAN +userStorage.cachePolicy.option.NO_CACHE=NO_CACHE +userStorage.cachePolicy.tooltip=这个存储源的缓存策略. '默认' 是全局的默认缓存策略。'EVICT_DAILY'是每天特定时间缓存会失效. 'EVICT_WEEKLY'是每周第n天的特定时间缓存会失效. 'MAX-LIFESPAN' 是指缓存条目的最大生命周期 +userStorage.cachePolicy.evictionDay=Eviction Day +userStorage.cachePolicy.evictionDay.tooltip=每周第n天缓存失效 +userStorage.cachePolicy.evictionHour=Eviction Hour +userStorage.cachePolicy.evictionHour.tooltip=一天中几点缓存失效 +userStorage.cachePolicy.evictionMinute=Eviction Minute +userStorage.cachePolicy.evictionMinute.tooltip=缓存失效的分钟 +userStorage.cachePolicy.maxLifespan=最大生命周期 +userStorage.cachePolicy.maxLifespan.tooltip=以微秒计数的最大生命周期 +user-origin-link=存储源 + +disable=关闭 +disableable-credential-types=可以关闭的类型 +credentials.disableable.tooltip=可以关闭的密码类型列表 +disable-credential-types=关闭密码类型 +credentials.disable.tooltip=点击按钮关闭密码类型 +credential-types=密码类型 +manage-user-password=管理密码 +disable-credentials=关闭密码 +credential-reset-actions=重置密码 +ldap-mappers=LDAP 映射器 +create-ldap-mapper=创建 LDAP 映射 +loginWithEmailAllowed=使用电子邮件登录 +duplicateEmailsAllowed=重复的邮件 +hidden=隐藏 + + + + + + diff --git a/themes/src/main/resources/theme/base/admin/messages/messages_zh_CN.properties b/themes/src/main/resources/theme/base/admin/messages/messages_zh_CN.properties new file mode 100644 index 0000000000..a04cc03fc7 --- /dev/null +++ b/themes/src/main/resources/theme/base/admin/messages/messages_zh_CN.properties @@ -0,0 +1,25 @@ +invalidPasswordMinLengthMessage=无效的密码:最短长度 {0}. +invalidPasswordMinLowerCaseCharsMessage=无效的密码:至少包含 {0} 小写字母 +invalidPasswordMinDigitsMessage=无效的密码:至少包含 {0} 个数字 +invalidPasswordMinUpperCaseCharsMessage=无效的密码:最短长度 {0} 大写字母 +invalidPasswordMinSpecialCharsMessage=无效的密码:最短长度 {0} 特殊字符 +invalidPasswordNotUsernameMessage=无效的密码: 不可以与用户名相同 +invalidPasswordRegexPatternMessage=无效的密码: 无法与正则表达式匹配 +invalidPasswordHistoryMessage=无效的密码:不能与最后使用的 {0} 个密码相同 + +ldapErrorInvalidCustomFilter=定制的 LDAP过滤器不是以 "(" 开头或以 ")"结尾. +ldapErrorConnectionTimeoutNotNumber=Connection Timeout 必须是个数字 +ldapErrorMissingClientId=当域角色映射未启用时,客户端 ID 需要指定。 +ldapErrorCantPreserveGroupInheritanceWithUIDMembershipType=无法在使用UID成员类型的同时维护组继承属性。 +ldapErrorCantWriteOnlyForReadOnlyLdap=当LDAP提供方不是可写模式时,无法设置只写 +ldapErrorCantWriteOnlyAndReadOnly=无法同时设置只读和只写 + +clientRedirectURIsFragmentError=重定向URL不应包含URI片段 +clientRootURLFragmentError=根URL 不应包含 URL 片段 + +pairwiseMalformedClientRedirectURI=客户端包含一个无效的重定向URL +pairwiseClientRedirectURIsMissingHost=客户端重定向URL需要有一个有效的主机 +pairwiseClientRedirectURIsMultipleHosts=Without a configured Sector Identifier URI, client redirect URIs must not contain multiple host components. +pairwiseMalformedSectorIdentifierURI=Malformed Sector Identifier URI. +pairwiseFailedToGetRedirectURIs=无法从服务器获得重定向URL +pairwiseRedirectURIsMismatch=客户端的重定向URI与服务器端获取的URI配置不匹配。 diff --git a/themes/src/main/resources/theme/base/admin/resources/js/app.js b/themes/src/main/resources/theme/base/admin/resources/js/app.js index c4e870fdac..c650d00681 100755 --- a/themes/src/main/resources/theme/base/admin/resources/js/app.js +++ b/themes/src/main/resources/theme/base/admin/resources/js/app.js @@ -125,6 +125,12 @@ module.config(['$translateProvider', function($translateProvider) { $translateProvider.translations(locale, resourceBundle); }]); +// Change for upgrade to AngularJS 1.6 +// See https://github.com/angular/angular.js/commit/aa077e81129c740041438688dff2e8d20c3d7b52 +module.config(['$locationProvider', function($locationProvider) { + $locationProvider.hashPrefix(''); +}]); + module.config([ '$routeProvider', function($routeProvider) { $routeProvider .when('/create/realm', { @@ -1709,8 +1715,8 @@ module.config([ '$routeProvider', function($routeProvider) { flows : function(AuthenticationFlowsLoader) { return AuthenticationFlowsLoader(); }, - serverInfo : function(ServerInfo) { - return ServerInfo.delay; + serverInfo : function(ServerInfoLoader) { + return ServerInfoLoader(); } }, controller : 'RealmFlowBindingCtrl' 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 034d595496..14c9392af4 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 @@ -79,7 +79,72 @@ module.controller('ResourceServerDetailCtrl', function($scope, $http, $route, $l }); }); -module.controller('ResourceServerResourceCtrl', function($scope, $http, $route, $location, realm, ResourceServer, ResourceServerResource, client) { +var Resources = { + delete: function(ResourceServerResource, realm, client, $scope, AuthzDialog, $location, Notifications, $route) { + ResourceServerResource.permissions({ + realm : realm, + client : client.id, + rsrid : $scope.resource._id + }, function (permissions) { + var msg = ""; + + if (permissions.length > 0 && !$scope.deleteConsent) { + msg = "

    This resource is referenced in some permissions:

    "; + msg += "
      "; + for (i = 0; i < permissions.length; i++) { + msg+= "
    • " + permissions[i].name + "
    • "; + } + msg += "
    "; + msg += "

    If you remove this resource, the permissions above will be affected and will not be associated with this resource anymore.

    "; + } + + AuthzDialog.confirmDeleteWithMsg($scope.resource.name, "Resource", msg, function() { + ResourceServerResource.delete({realm : realm, client : $scope.client.id, rsrid : $scope.resource._id}, null, function() { + $location.url("/realms/" + realm + "/clients/" + $scope.client.id + "/authz/resource-server/resource"); + $route.reload(); + Notifications.success("The resource has been deleted."); + }); + }); + }); + } +} + +var Policies = { + delete: function(service, realm, client, $scope, AuthzDialog, $location, Notifications, $route, isPermission) { + var msg = ""; + + service.dependentPolicies({ + realm : realm, + client : client.id, + id : $scope.policy.id + }, function (dependentPolicies) { + if (dependentPolicies.length > 0 && !$scope.deleteConsent) { + msg = "

    This policy is being used by other policies:

    "; + msg += "
      "; + for (i = 0; i < dependentPolicies.length; i++) { + msg+= "
    • " + dependentPolicies[i].name + "
    • "; + } + msg += "
    "; + msg += "

    If you remove this policy, the policies above will be affected and will not be associated with this policy anymore.

    "; + } + + AuthzDialog.confirmDeleteWithMsg($scope.policy.name, isPermission ? "Permission" : "Policy", msg, function() { + service.delete({realm : realm, client : $scope.client.id, id : $scope.policy.id}, null, function() { + if (isPermission) { + $location.url("/realms/" + realm + "/clients/" + $scope.client.id + "/authz/resource-server/permission"); + Notifications.success("The permission has been deleted."); + } else { + $location.url("/realms/" + realm + "/clients/" + $scope.client.id + "/authz/resource-server/policy"); + Notifications.success("The policy has been deleted."); + } + $route.reload(); + }); + }); + }); + } +} + +module.controller('ResourceServerResourceCtrl', function($scope, $http, $route, $location, realm, ResourceServer, ResourceServerResource, client, AuthzDialog, Notifications) { $scope.realm = realm; $scope.client = client; @@ -171,6 +236,11 @@ module.controller('ResourceServerResourceCtrl', function($scope, $http, $route, } } }; + + $scope.delete = function(resource) { + $scope.resource = resource; + Resources.delete(ResourceServerResource, $route.current.params.realm, client, $scope, AuthzDialog, $location, Notifications, $route); + }; }); module.controller('ResourceServerResourceDetailCtrl', function($scope, $http, $route, $location, realm, ResourceServer, client, ResourceServerResource, ResourceServerScope, AuthzDialog, Notifications) { @@ -282,30 +352,7 @@ module.controller('ResourceServerResourceDetailCtrl', function($scope, $http, $r } $scope.remove = function() { - ResourceServerResource.permissions({ - realm : $route.current.params.realm, - client : client.id, - rsrid : $scope.resource._id - }, function (permissions) { - var msg = ""; - - if (permissions.length > 0 && !$scope.deleteConsent) { - msg = "

    This resource is referenced in some policies:

    "; - msg += "
      "; - for (i = 0; i < permissions.length; i++) { - msg+= "
    • " + permissions[i].name + "
    • "; - } - msg += "
    "; - msg += "

    If you remove this resource, the policies above will be affected and will not be associated with this resource anymore.

    "; - } - - AuthzDialog.confirmDeleteWithMsg($scope.resource.name, "Resource", msg, function() { - ResourceServerResource.delete({realm : realm.realm, client : $scope.client.id, rsrid : $scope.resource._id}, null, function() { - $location.url("/realms/" + realm.realm + "/clients/" + $scope.client.id + "/authz/resource-server/resource"); - Notifications.success("The resource has been deleted."); - }); - }); - }); + Resources.delete(ResourceServerResource, $route.current.params.realm, client, $scope, AuthzDialog, $location, Notifications, $route); } $scope.reset = function() { @@ -338,7 +385,37 @@ module.controller('ResourceServerResourceDetailCtrl', function($scope, $http, $r } }); -module.controller('ResourceServerScopeCtrl', function($scope, $http, $route, $location, realm, ResourceServer, ResourceServerScope, client) { +var Scopes = { + delete: function(ResourceServerScope, realm, client, $scope, AuthzDialog, $location, Notifications, $route) { + ResourceServerScope.permissions({ + realm : realm, + client : client.id, + id : $scope.scope.id + }, function (permissions) { + var msg = ""; + + if (permissions.length > 0 && !$scope.deleteConsent) { + msg = "

    This scope is referenced in some permissions:

    "; + msg += "
      "; + for (i = 0; i < permissions.length; i++) { + msg+= "
    • " + permissions[i].name + "
    • "; + } + msg += "
    "; + msg += "

    If you remove this scope, the permissions above will be affected and will not be associated with this scope anymore.

    "; + } + + AuthzDialog.confirmDeleteWithMsg($scope.scope.name, "Scope", msg, function() { + ResourceServerScope.delete({realm : realm, client : $scope.client.id, id : $scope.scope.id}, null, function() { + $location.url("/realms/" + realm + "/clients/" + $scope.client.id + "/authz/resource-server/scope"); + $route.reload(); + Notifications.success("The scope has been deleted."); + }); + }); + }); + } +} + +module.controller('ResourceServerScopeCtrl', function($scope, $http, $route, $location, realm, ResourceServer, ResourceServerScope,client, AuthzDialog, Notifications) { $scope.realm = realm; $scope.client = client; @@ -430,6 +507,11 @@ module.controller('ResourceServerScopeCtrl', function($scope, $http, $route, $lo } } }; + + $scope.delete = function(scope) { + $scope.scope = scope; + Scopes.delete(ResourceServerScope, $route.current.params.realm, client, $scope, AuthzDialog, $location, Notifications, $route); + }; }); module.controller('ResourceServerScopeDetailCtrl', function($scope, $http, $route, $location, realm, ResourceServer, client, ResourceServerScope, AuthzDialog, Notifications) { @@ -499,30 +581,7 @@ module.controller('ResourceServerScopeDetailCtrl', function($scope, $http, $rout } $scope.remove = function() { - ResourceServerScope.permissions({ - realm : $route.current.params.realm, - client : client.id, - id : $scope.scope.id - }, function (permissions) { - var msg = ""; - - if (permissions.length > 0 && !$scope.deleteConsent) { - msg = "

    This scope is referenced in some policies:

    "; - msg += "
      "; - for (i = 0; i < permissions.length; i++) { - msg+= "
    • " + permissions[i].name + "
    • "; - } - msg += "
    "; - msg += "

    If you remove this scope, the policies above will be affected and will not be associated with this scope anymore.

    "; - } - - AuthzDialog.confirmDeleteWithMsg($scope.scope.name, "Scope", msg, function() { - ResourceServerScope.delete({realm : realm.realm, client : $scope.client.id, id : $scope.scope.id}, null, function() { - $location.url("/realms/" + realm.realm + "/clients/" + client.id + "/authz/resource-server/scope"); - Notifications.success("The scope has been deleted."); - }); - }); - }); + Scopes.delete(ResourceServerScope, $route.current.params.realm, client, $scope, AuthzDialog, $location, Notifications, $route); } $scope.reset = function() { @@ -554,7 +613,7 @@ module.controller('ResourceServerScopeDetailCtrl', function($scope, $http, $rout } }); -module.controller('ResourceServerPolicyCtrl', function($scope, $http, $route, $location, realm, ResourceServer, ResourceServerPolicy, PolicyProvider, client) { +module.controller('ResourceServerPolicyCtrl', function($scope, $http, $route, $location, realm, ResourceServer, ResourceServerPolicy, PolicyProvider, client, AuthzDialog, Notifications) { $scope.realm = realm; $scope.client = client; $scope.policyProviders = []; @@ -650,9 +709,14 @@ module.controller('ResourceServerPolicyCtrl', function($scope, $http, $route, $l } } }; + + $scope.delete = function(policy) { + $scope.policy = policy; + Policies.delete(ResourceServerPolicy, $route.current.params.realm, client, $scope, AuthzDialog, $location, Notifications, $route, false); + }; }); -module.controller('ResourceServerPermissionCtrl', function($scope, $http, $route, $location, realm, ResourceServer, ResourceServerPermission, PolicyProvider, client) { +module.controller('ResourceServerPermissionCtrl', function($scope, $http, $route, $location, realm, ResourceServer, ResourceServerPermission, PolicyProvider, client, AuthzDialog, Notifications) { $scope.realm = realm; $scope.client = client; $scope.policyProviders = []; @@ -747,6 +811,11 @@ module.controller('ResourceServerPermissionCtrl', function($scope, $http, $route } } }; + + $scope.delete = function(policy) { + $scope.policy = policy; + Policies.delete(ResourceServerPermission, $route.current.params.realm, client, $scope, AuthzDialog, $location, Notifications, $route, true); + }; }); module.controller('ResourceServerPolicyDroolsDetailCtrl', function($scope, $http, $route, realm, client, PolicyController) { @@ -766,8 +835,8 @@ module.controller('ResourceServerPolicyDroolsDetailCtrl', function($scope, $http delete policy.config; $http.post(authUrl + '/admin/realms/'+ $route.current.params.realm + '/clients/' + client.id + '/authz/resource-server/policy/rules/provider/resolveModules' - , policy).success(function(data) { - $scope.drools.moduleNames = data; + , policy).then(function(response) { + $scope.drools.moduleNames = response.data; $scope.resolveSessions(); }); } @@ -776,8 +845,8 @@ module.controller('ResourceServerPolicyDroolsDetailCtrl', function($scope, $http delete $scope.policy.config; $http.post(authUrl + '/admin/realms/'+ $route.current.params.realm + '/clients/' + client.id + '/authz/resource-server/policy/rules/provider/resolveSessions' - , $scope.policy).success(function(data) { - $scope.drools.moduleSessions = data; + , $scope.policy).then(function(response) { + $scope.drools.moduleSessions = response.data; }); } }, @@ -1137,27 +1206,28 @@ module.controller('ResourceServerPolicyScopeDetailCtrl', function($scope, $route rsrid: resource[0]._id }, function (scopes) { $scope.resourceScopes = scopes; - ResourceServerPolicy.scopes({ - realm : $route.current.params.realm, - client : client.id, - id : policy.id - }, function(scopes) { - $scope.selectedScopes = []; - for (i = 0; i < scopes.length; i++) { - scopes[i].text = scopes[i].name; - $scope.selectedScopes.push(scopes[i].id); - } - var copy = angular.copy($scope.selectedScopes); - $scope.$watch('selectedScopes', function() { - if (!angular.equals($scope.selectedScopes, copy)) { - $scope.changed = true; - } - }, true); - }); }); }); }); } + + ResourceServerPolicy.scopes({ + realm : $route.current.params.realm, + client : client.id, + id : policy.id + }, function(scopes) { + $scope.selectedScopes = []; + for (i = 0; i < scopes.length; i++) { + scopes[i].text = scopes[i].name; + $scope.selectedScopes.push(scopes[i].id); + } + var copy = angular.copy($scope.selectedScopes); + $scope.$watch('selectedScopes', function() { + if (!angular.equals($scope.selectedScopes, copy)) { + $scope.changed = true; + } + }, true); + }); } else { $scope.selectedResource = null; var copy = angular.copy($scope.selectedResource); @@ -2098,35 +2168,7 @@ module.service("PolicyController", function($http, $route, $location, ResourceSe }); $scope.remove = function() { - var msg = ""; - - service.dependentPolicies({ - realm : $route.current.params.realm, - client : client.id, - id : $scope.policy.id - }, function (dependentPolicies) { - if (dependentPolicies.length > 0 && !$scope.deleteConsent) { - msg = "

    This policy is being used by other policies:

    "; - msg += "
      "; - for (i = 0; i < dependentPolicies.length; i++) { - msg+= "
    • " + dependentPolicies[i].name + "
    • "; - } - msg += "
    "; - msg += "

    If you remove this policy, the policies above will be affected and will not be associated with this policy anymore.

    "; - } - - AuthzDialog.confirmDeleteWithMsg($scope.policy.name, "Policy", msg, function() { - service.delete({realm : $scope.realm.realm, client : $scope.client.id, id : $scope.policy.id}, null, function() { - if (delegate.isPermission()) { - $location.url("/realms/" + realm.realm + "/clients/" + client.id + "/authz/resource-server/permission"); - Notifications.success("The permission has been deleted."); - } else { - $location.url("/realms/" + realm.realm + "/clients/" + client.id + "/authz/resource-server/policy"); - Notifications.success("The policy has been deleted."); - } - }); - }); - }); + Policies.delete(ResourceServerPolicy, $route.current.params.realm, client, $scope, AuthzDialog, $location, Notifications, $route, delegate.isPermission()); } } }); @@ -2354,8 +2396,8 @@ module.controller('PolicyEvaluateCtrl', function($scope, $http, $route, $locatio } $http.post(authUrl + '/admin/realms/'+ $route.current.params.realm + '/clients/' + client.id + '/authz/resource-server/policy/evaluate' - , $scope.authzRequest).success(function(data) { - $scope.evaluationResult = data; + , $scope.authzRequest).then(function(response) { + $scope.evaluationResult = response.data; $scope.showResultTab(); }); } @@ -2363,8 +2405,8 @@ module.controller('PolicyEvaluateCtrl', function($scope, $http, $route, $locatio $scope.entitlements = function() { $scope.authzRequest.entitlements = true; $http.post(authUrl + '/admin/realms/'+ $route.current.params.realm + '/clients/' + client.id + '/authz/resource-server/policy/evaluate' - , $scope.authzRequest).success(function(data) { - $scope.evaluationResult = data; + , $scope.authzRequest).then(function(response) { + $scope.evaluationResult = response.data; $scope.showResultTab(); }); } @@ -2496,16 +2538,17 @@ module.controller('RealmRolePermissionsCtrl', function($scope, $http, $route, $l $scope.realm = realm; RoleManagementPermissions.get({realm: realm.realm, role: role.id}, function(data) { $scope.permissions = data; + $scope.$watch('permissions.enabled', function(newVal, oldVal) { + if (newVal != oldVal) { + console.log('Changing permissions enabled to: ' + $scope.permissions.enabled); + var param = {enabled: $scope.permissions.enabled}; + $scope.permissions= RoleManagementPermissions.update({realm: realm.realm, role:role.id}, param); + } + }, true); }); Client.query({realm: realm.realm, clientId: getManageClientId(realm)}, function(data) { $scope.realmManagementClientId = data[0].id; }); - $scope.setEnabled = function() { - var param = { enabled: $scope.permissions.enabled}; - $scope.permissions= RoleManagementPermissions.update({realm: realm.realm, role:role.id}, param); - }; - - }); module.controller('ClientRolePermissionsCtrl', function($scope, $http, $route, $location, realm, client, role, Client, RoleManagementPermissions, Client, Notifications) { console.log('RealmRolePermissionsCtrl'); @@ -2514,33 +2557,39 @@ module.controller('ClientRolePermissionsCtrl', function($scope, $http, $route, $ $scope.realm = realm; RoleManagementPermissions.get({realm: realm.realm, role: role.id}, function(data) { $scope.permissions = data; + $scope.$watch('permissions.enabled', function(newVal, oldVal) { + if (newVal != oldVal) { + console.log('Changing permissions enabled to: ' + $scope.permissions.enabled); + var param = {enabled: $scope.permissions.enabled}; + $scope.permissions = RoleManagementPermissions.update({realm: realm.realm, role:role.id}, param); + } + }, true); }); Client.query({realm: realm.realm, clientId: getManageClientId(realm)}, function(data) { $scope.realmManagementClientId = data[0].id; }); - $scope.setEnabled = function() { - console.log('perssions enabled: ' + $scope.permissions.enabled); - var param = { enabled: $scope.permissions.enabled}; - $scope.permissions = RoleManagementPermissions.update({realm: realm.realm, role:role.id}, param); - }; - - }); module.controller('UsersPermissionsCtrl', function($scope, $http, $route, $location, realm, UsersManagementPermissions, Client, Notifications) { console.log('UsersPermissionsCtrl'); $scope.realm = realm; + var first = true; UsersManagementPermissions.get({realm: realm.realm}, function(data) { $scope.permissions = data; + $scope.$watch('permissions.enabled', function(newVal, oldVal) { + if (newVal != oldVal) { + console.log('Changing permissions enabled to: ' + $scope.permissions.enabled); + var param = {enabled: $scope.permissions.enabled}; + $scope.permissions = UsersManagementPermissions.update({realm: realm.realm}, param); + + } + }, true); }); Client.query({realm: realm.realm, clientId: getManageClientId(realm)}, function(data) { $scope.realmManagementClientId = data[0].id; }); - $scope.changeIt = function() { - console.log('before permissions.enabled=' + $scope.permissions.enabled); - var param = { enabled: $scope.permissions.enabled}; - $scope.permissions = UsersManagementPermissions.update({realm: realm.realm}, param); - }; + + }); @@ -2550,16 +2599,17 @@ module.controller('ClientPermissionsCtrl', function($scope, $http, $route, $loca $scope.realm = realm; ClientManagementPermissions.get({realm: realm.realm, client: client.id}, function(data) { $scope.permissions = data; + $scope.$watch('permissions.enabled', function(newVal, oldVal) { + if (newVal != oldVal) { + console.log('Changing permissions enabled to: ' + $scope.permissions.enabled); + var param = {enabled: $scope.permissions.enabled}; + $scope.permissions = ClientManagementPermissions.update({realm: realm.realm, client: client.id}, param); + } + }, true); }); Client.query({realm: realm.realm, clientId: getManageClientId(realm)}, function(data) { $scope.realmManagementClientId = data[0].id; }); - $scope.setEnabled = function() { - var param = { enabled: $scope.permissions.enabled}; - $scope.permissions = ClientManagementPermissions.update({realm: realm.realm, client: client.id}, param); - }; - - }); module.controller('GroupPermissionsCtrl', function($scope, $http, $route, $location, realm, group, GroupManagementPermissions, Client, Notifications) { @@ -2570,13 +2620,14 @@ module.controller('GroupPermissionsCtrl', function($scope, $http, $route, $locat }); GroupManagementPermissions.get({realm: realm.realm, group: group.id}, function(data) { $scope.permissions = data; + $scope.$watch('permissions.enabled', function(newVal, oldVal) { + if (newVal != oldVal) { + console.log('Changing permissions enabled to: ' + $scope.permissions.enabled); + var param = {enabled: $scope.permissions.enabled}; + $scope.permissions = GroupManagementPermissions.update({realm: realm.realm, group: group.id}, param); + } + }, true); }); - $scope.setEnabled = function() { - var param = { enabled: $scope.permissions.enabled}; - $scope.permissions = GroupManagementPermissions.update({realm: realm.realm, group: group.id}, param); - }; - - }); 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 515eb99720..33cb93be30 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 @@ -495,8 +495,8 @@ module.controller('ClientCertificateExportCtrl', function($scope, $location, $ht 'Content-Type': 'application/json', 'Accept': 'application/octet-stream' } - }).success(function(data){ - var blob = new Blob([data], { + }).then(function(response){ + var blob = new Blob([response.data], { type: 'application/octet-stream' }); var ext = ".jks"; @@ -508,10 +508,10 @@ module.controller('ClientCertificateExportCtrl', function($scope, $location, $ht } saveAs(blob, 'keystore' + ext); - }).error(function(data) { + }).catch(function(response) { var errorMsg = 'Error downloading'; try { - var error = JSON.parse(String.fromCharCode.apply(null, new Uint8Array(data))); + var error = JSON.parse(String.fromCharCode.apply(null, new Uint8Array(response.data))); errorMsg = error['error_description'] ? error['error_description'] : errorMsg; } catch (err) { } @@ -782,16 +782,16 @@ module.controller('ClientInstallationCtrl', function($scope, realm, client, serv method: 'GET', responseType: 'arraybuffer', cache: false - }).success(function(data) { - var installation = data; + }).then(function(response) { + var installation = response.data; $scope.installation = installation; } ); } else { - $http.get(url).success(function (data) { - var installation = data; + $http.get(url).then(function (response) { + var installation = response.data; if ($scope.configFormat.mediaType == 'application/json') { - installation = angular.fromJson(data); + installation = angular.fromJson(response.data); installation = angular.toJson(installation, true); } $scope.installation = installation; @@ -814,7 +814,7 @@ module.controller('ClientDetailCtrl', function($scope, realm, client, templates, "bearer-only" ]; - $scope.protocols = Object.keys(serverInfo.providers['login-protocol'].providers).sort(); + $scope.protocols = serverInfo.listProviderIds('login-protocol'); $scope.templates = [ {name:'NONE'}]; for (var i = 0; i < templates.length; i++) { @@ -1240,7 +1240,7 @@ module.controller('ClientDetailCtrl', function($scope, realm, client, templates, }); module.controller('CreateClientCtrl', function($scope, realm, client, templates, $route, serverInfo, Client, ClientDescriptionConverter, $location, $modal, Dialog, Notifications) { - $scope.protocols = Object.keys(serverInfo.providers['login-protocol'].providers).sort(); + $scope.protocols = serverInfo.listProviderIds('login-protocol'); $scope.create = true; $scope.templates = [ {name:'NONE'}]; var templateNameMap = new Object(); @@ -1428,7 +1428,7 @@ module.controller('ClientScopeMappingCtrl', function($scope, $http, realm, clien var roles = $scope.selectedRealmRoles; $scope.selectedRealmRoles = []; $http.post(authUrl + '/admin/realms/' + realm.realm + '/clients/' + client.id + '/scope-mappings/realm', - roles).success(function() { + roles).then(function() { updateRealmRoles(); Notifications.success("Scope mappings updated."); }); @@ -1438,7 +1438,7 @@ module.controller('ClientScopeMappingCtrl', function($scope, $http, realm, clien var roles = $scope.selectedRealmMappings; $scope.selectedRealmMappings = []; $http.delete(authUrl + '/admin/realms/' + realm.realm + '/clients/' + client.id + '/scope-mappings/realm', - {data : roles, headers : {"content-type" : "application/json"}}).success(function () { + {data : roles, headers : {"content-type" : "application/json"}}).then(function () { updateRealmRoles(); Notifications.success("Scope mappings updated."); }); @@ -1448,7 +1448,7 @@ module.controller('ClientScopeMappingCtrl', function($scope, $http, realm, clien var roles = $scope.selectedClientRoles; $scope.selectedClientRoles = []; $http.post(authUrl + '/admin/realms/' + realm.realm + '/clients/' + client.id + '/scope-mappings/clients/' + $scope.targetClient.id, - roles).success(function () { + roles).then(function () { updateClientRoles(); Notifications.success("Scope mappings updated."); }); @@ -1458,7 +1458,7 @@ module.controller('ClientScopeMappingCtrl', function($scope, $http, realm, clien var roles = $scope.selectedClientMappings; $scope.selectedClientMappings = []; $http.delete(authUrl + '/admin/realms/' + realm.realm + '/clients/' + client.id + '/scope-mappings/clients/' + $scope.targetClient.id, - {data : roles, headers : {"content-type" : "application/json"}}).success(function () { + {data : roles, headers : {"content-type" : "application/json"}}).then(function () { updateClientRoles(); Notifications.success("Scope mappings updated."); }); @@ -1688,10 +1688,10 @@ module.controller('AddBuiltinProtocolMapperCtrl', function($scope, realm, client } } $http.post(authUrl + '/admin/realms/' + realm.realm + '/clients/' + client.id + '/protocol-mappers/add-models', - toAdd).success(function() { + toAdd).then(function() { Notifications.success("Mappers added"); $location.url('/realms/' + realm.realm + '/clients/' + client.id + '/mappers'); - }).error(function() { + }).catch(function() { Notifications.error("Error adding mappers"); $location.url('/realms/' + realm.realm + '/clients/' + client.id + '/mappers'); }); @@ -1915,7 +1915,7 @@ module.controller('ClientTemplateListCtrl', function($scope, realm, templates, C }); module.controller('ClientTemplateDetailCtrl', function($scope, realm, template, $route, serverInfo, ClientTemplate, $location, $modal, Dialog, Notifications) { - $scope.protocols = Object.keys(serverInfo.providers['login-protocol'].providers).sort(); + $scope.protocols = serverInfo.listProviderIds('login-protocol'); $scope.realm = realm; $scope.create = !template.name; @@ -2202,10 +2202,10 @@ module.controller('ClientTemplateAddBuiltinProtocolMapperCtrl', function($scope, } } $http.post(authUrl + '/admin/realms/' + realm.realm + '/client-templates/' + template.id + '/protocol-mappers/add-models', - toAdd).success(function() { + toAdd).then(function() { Notifications.success("Mappers added"); $location.url('/realms/' + realm.realm + '/client-templates/' + template.id + '/mappers'); - }).error(function() { + }).catch(function() { Notifications.error("Error adding mappers"); $location.url('/realms/' + realm.realm + '/client-templates/' + template.id + '/mappers'); }); @@ -2273,7 +2273,7 @@ module.controller('ClientTemplateScopeMappingCtrl', function($scope, $http, real var roles = $scope.selectedRealmRoles; $scope.selectedRealmRoles = []; $http.post(authUrl + '/admin/realms/' + realm.realm + '/client-templates/' + template.id + '/scope-mappings/realm', - roles).success(function() { + roles).then(function() { updateTemplateRealmRoles(); Notifications.success("Scope mappings updated."); }); @@ -2283,7 +2283,7 @@ module.controller('ClientTemplateScopeMappingCtrl', function($scope, $http, real var roles = $scope.selectedRealmMappings; $scope.selectedRealmMappings = []; $http.delete(authUrl + '/admin/realms/' + realm.realm + '/client-templates/' + template.id + '/scope-mappings/realm', - {data : roles, headers : {"content-type" : "application/json"}}).success(function () { + {data : roles, headers : {"content-type" : "application/json"}}).then(function () { updateTemplateRealmRoles(); Notifications.success("Scope mappings updated."); }); @@ -2293,7 +2293,7 @@ module.controller('ClientTemplateScopeMappingCtrl', function($scope, $http, real var roles = $scope.selectedClientRoles; $scope.selectedClientRoles = []; $http.post(authUrl + '/admin/realms/' + realm.realm + '/client-templates/' + template.id + '/scope-mappings/clients/' + $scope.targetClient.id, - roles).success(function () { + roles).then(function () { updateTemplateClientRoles(); Notifications.success("Scope mappings updated."); }); @@ -2303,7 +2303,7 @@ module.controller('ClientTemplateScopeMappingCtrl', function($scope, $http, real var roles = $scope.selectedClientMappings; $scope.selectedClientMappings = []; $http.delete(authUrl + '/admin/realms/' + realm.realm + '/client-templates/' + template.id + '/scope-mappings/clients/' + $scope.targetClient.id, - {data : roles, headers : {"content-type" : "application/json"}}).success(function () { + {data : roles, headers : {"content-type" : "application/json"}}).then(function () { updateTemplateClientRoles(); Notifications.success("Scope mappings updated."); }); diff --git a/themes/src/main/resources/theme/base/admin/resources/js/controllers/groups.js b/themes/src/main/resources/theme/base/admin/resources/js/controllers/groups.js index 00c8e93772..aa0cfadd15 100755 --- a/themes/src/main/resources/theme/base/admin/resources/js/controllers/groups.js +++ b/themes/src/main/resources/theme/base/admin/resources/js/controllers/groups.js @@ -236,7 +236,7 @@ module.controller('GroupRoleMappingCtrl', function($scope, $http, realm, group, var roles = $scope.selectedRealmRoles; $scope.selectedRealmRoles = []; $http.post(authUrl + '/admin/realms/' + realm.realm + '/groups/' + group.id + '/role-mappings/realm', - roles).success(function() { + roles).then(function() { $scope.realmMappings = GroupRealmRoleMapping.query({realm : realm.realm, groupId : group.id}); $scope.realmRoles = GroupAvailableRealmRoleMapping.query({realm : realm.realm, groupId : group.id}); $scope.realmComposite = GroupCompositeRealmRoleMapping.query({realm : realm.realm, groupId : group.id}); @@ -257,7 +257,7 @@ module.controller('GroupRoleMappingCtrl', function($scope, $http, realm, group, $scope.deleteRealmRole = function() { $http.delete(authUrl + '/admin/realms/' + realm.realm + '/groups/' + group.id + '/role-mappings/realm', - {data : $scope.selectedRealmMappings, headers : {"content-type" : "application/json"}}).success(function() { + {data : $scope.selectedRealmMappings, headers : {"content-type" : "application/json"}}).then(function() { $scope.realmMappings = GroupRealmRoleMapping.query({realm : realm.realm, groupId : group.id}); $scope.realmRoles = GroupAvailableRealmRoleMapping.query({realm : realm.realm, groupId : group.id}); $scope.realmComposite = GroupCompositeRealmRoleMapping.query({realm : realm.realm, groupId : group.id}); @@ -277,7 +277,7 @@ module.controller('GroupRoleMappingCtrl', function($scope, $http, realm, group, $scope.addClientRole = function() { $http.post(authUrl + '/admin/realms/' + realm.realm + '/groups/' + group.id + '/role-mappings/clients/' + $scope.targetClient.id, - $scope.selectedClientRoles).success(function() { + $scope.selectedClientRoles).then(function() { $scope.clientMappings = GroupClientRoleMapping.query({realm : realm.realm, groupId : group.id, client : $scope.targetClient.id}); $scope.clientRoles = GroupAvailableClientRoleMapping.query({realm : realm.realm, groupId : group.id, client : $scope.targetClient.id}); $scope.clientComposite = GroupCompositeClientRoleMapping.query({realm : realm.realm, groupId : group.id, client : $scope.targetClient.id}); @@ -291,7 +291,7 @@ module.controller('GroupRoleMappingCtrl', function($scope, $http, realm, group, $scope.deleteClientRole = function() { $http.delete(authUrl + '/admin/realms/' + realm.realm + '/groups/' + group.id + '/role-mappings/clients/' + $scope.targetClient.id, - {data : $scope.selectedClientMappings, headers : {"content-type" : "application/json"}}).success(function() { + {data : $scope.selectedClientMappings, headers : {"content-type" : "application/json"}}).then(function() { $scope.clientMappings = GroupClientRoleMapping.query({realm : realm.realm, groupId : group.id, client : $scope.targetClient.id}); $scope.clientRoles = GroupAvailableClientRoleMapping.query({realm : realm.realm, groupId : group.id, client : $scope.targetClient.id}); $scope.clientComposite = GroupCompositeClientRoleMapping.query({realm : realm.realm, groupId : group.id, client : $scope.targetClient.id}); diff --git a/themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js b/themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js index ab4e72a878..77dc9cdb4e 100644 --- a/themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js +++ b/themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js @@ -406,16 +406,20 @@ module.controller('RealmThemeCtrl', function($scope, Current, Realm, realm, serv $scope.supportedLocalesOptions = { 'multiple' : true, - 'simple_tags' : true + 'simple_tags' : true, + 'tags' : [] }; - + + updateSupported(); + function localeForTheme(type, name) { name = name || 'base'; for (var i = 0; i < serverInfo.themes[type].length; i++) { if (serverInfo.themes[type][i].name == name) { - return serverInfo.themes[type][i].locales; + return serverInfo.themes[type][i].locales || []; } } + return []; } function updateSupported() { @@ -923,11 +927,11 @@ module.controller('RealmIdentityProviderCtrl', function($scope, $filter, $upload //formDataAppender: function(formData, key, val){} }).progress(function(evt) { console.log('percent: ' + parseInt(100.0 * evt.loaded / evt.total)); - }).success(function(data, status, headers) { - setConfig(data); + }).then(function(response) { + setConfig(response.data); $scope.clearFileSelect(); Notifications.success("The IDP metadata has been loaded from file."); - }).error(function() { + }).catch(function() { Notifications.error("The file can not be uploaded. Please verify the file."); }); } @@ -943,12 +947,12 @@ module.controller('RealmIdentityProviderCtrl', function($scope, $filter, $upload providerId: providerFactory.id } $http.post(authUrl + '/admin/realms/' + realm.realm + '/identity-provider/import-config', input) - .success(function(data, status, headers) { - setConfig(data); + .then(function(response) { + setConfig(response.data); $scope.fromUrl.data = ''; $scope.importUrl = false; Notifications.success("Imported config information from url."); - }).error(function() { + }).catch(function() { Notifications.error("Config can not be imported. Please verify the url."); }); }; @@ -1047,9 +1051,9 @@ module.controller('RealmIdentityProviderExportCtrl', function(realm, identityPro $scope.exportedType = ""; var url = IdentityProviderExport.url({realm: realm.realm, alias: identityProvider.alias}) ; - $http.get(url).success(function(data, status, headers, config) { - $scope.exportedType = headers('Content-Type'); - $scope.exported = data; + $http.get(url).then(function(response) { + $scope.exportedType = response.headers('Content-Type'); + $scope.exported = response.data; }); $scope.download = function() { @@ -1449,7 +1453,7 @@ module.controller('RoleDetailCtrl', function($scope, realm, role, roles, clients $http, $location, Notifications, Dialog); }); -module.controller('RealmSMTPSettingsCtrl', function($scope, Current, Realm, realm, $http, $location, Dialog, Notifications) { +module.controller('RealmSMTPSettingsCtrl', function($scope, Current, Realm, realm, $http, $location, Dialog, Notifications, RealmSMTPConnectionTester) { console.log('RealmSMTPSettingsCtrl'); var booleanSmtpAtts = ["auth","ssl","starttls"]; @@ -1484,6 +1488,25 @@ module.controller('RealmSMTPSettingsCtrl', function($scope, Current, Realm, real $scope.changed = false; }; + var initSMTPTest = function() { + return { + realm: $scope.realm.realm, + config: JSON.stringify(realm.smtpServer) + }; + }; + + $scope.testConnection = function() { + RealmSMTPConnectionTester.send(initSMTPTest(), function() { + Notifications.success("SMTP connection successful. E-mail was sent!"); + }, function(errorResponse) { + if (error.data.errorMessage) { + Notifications.error(error.data.errorMessage); + } else { + Notifications.error('Unexpected error during SMTP validation'); + } + }); + }; + /* Convert string attributes containing a boolean to actual boolean type + convert an integer string (port) to integer. */ function typeObject(obj){ for (var att in obj){ @@ -1526,9 +1549,15 @@ module.controller('RealmEventsConfigCtrl', function($scope, eventsConfig, RealmE $scope.eventsConfig.expirationUnit = TimeUnit.autoUnit(eventsConfig.eventsExpiration); $scope.eventsConfig.eventsExpiration = TimeUnit.toUnit(eventsConfig.eventsExpiration, $scope.eventsConfig.expirationUnit); - + $scope.eventListeners = Object.keys(serverInfo.providers.eventsListener.providers); - + + $scope.eventsConfigSelectOptions = { + 'multiple': true, + 'simple_tags': true, + 'tags': $scope.eventListeners + }; + $scope.eventSelectOptions = { 'multiple': true, 'simple_tags': true, @@ -1916,6 +1945,8 @@ module.controller('RealmFlowBindingCtrl', function($scope, flows, Current, Realm } } + $scope.profileInfo = serverInfo.profileInfo; + genericRealmUpdate($scope, Current, Realm, realm, serverInfo, $http, $location, Dialog, Notifications, "/realms/" + realm.realm + "/authentication/flow-bindings"); }); @@ -2110,6 +2141,9 @@ module.controller('AuthenticationFlowsCtrl', function($scope, $route, realm, flo } else if (realm.clientAuthenticationFlow == $scope.flow.alias) { Notifications.error("Cannot remove flow, it is currently being used as the client authentication flow."); + } else if (realm.dockerAuthenticationFlow == $scope.flow.alias) { + Notifications.error("Cannot remove flow, it is currently being used as the docker authentication flow."); + } else { AuthenticationFlows.remove({realm: realm.realm, flow: $scope.flow.id}, function () { $location.url("/realms/" + realm.realm + '/authentication/flows/' + flows[0].alias); @@ -2770,11 +2804,11 @@ module.controller('RealmExportCtrl', function($scope, realm, $http, exportUrl += '?' + $httpParamSerializer(params); } $http.post(exportUrl) - .success(function(data, status, headers) { - var download = angular.fromJson(data); + .then(function(response) { + var download = angular.fromJson(response.data); download = angular.toJson(download, true); saveAs(new Blob([download], { type: 'application/json' }), 'realm-export.json'); - }).error(function() { + }).catch(function() { Notifications.error("Sorry, something went wrong."); }); } diff --git a/themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js b/themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js index 580d66175d..549f621568 100755 --- a/themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js +++ b/themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js @@ -23,7 +23,7 @@ module.controller('UserRoleMappingCtrl', function($scope, $http, realm, user, cl var roles = $scope.selectedRealmRoles; $scope.selectedRealmRoles = []; $http.post(authUrl + '/admin/realms/' + realm.realm + '/users/' + user.id + '/role-mappings/realm', - roles).success(function() { + roles).then(function() { $scope.realmMappings = RealmRoleMapping.query({realm : realm.realm, userId : user.id}); $scope.realmRoles = AvailableRealmRoleMapping.query({realm : realm.realm, userId : user.id}); $scope.realmComposite = CompositeRealmRoleMapping.query({realm : realm.realm, userId : user.id}); @@ -44,7 +44,7 @@ module.controller('UserRoleMappingCtrl', function($scope, $http, realm, user, cl $scope.deleteRealmRole = function() { $http.delete(authUrl + '/admin/realms/' + realm.realm + '/users/' + user.id + '/role-mappings/realm', - {data : $scope.selectedRealmMappings, headers : {"content-type" : "application/json"}}).success(function() { + {data : $scope.selectedRealmMappings, headers : {"content-type" : "application/json"}}).then(function() { $scope.realmMappings = RealmRoleMapping.query({realm : realm.realm, userId : user.id}); $scope.realmRoles = AvailableRealmRoleMapping.query({realm : realm.realm, userId : user.id}); $scope.realmComposite = CompositeRealmRoleMapping.query({realm : realm.realm, userId : user.id}); @@ -64,7 +64,7 @@ module.controller('UserRoleMappingCtrl', function($scope, $http, realm, user, cl $scope.addClientRole = function() { $http.post(authUrl + '/admin/realms/' + realm.realm + '/users/' + user.id + '/role-mappings/clients/' + $scope.targetClient.id, - $scope.selectedClientRoles).success(function() { + $scope.selectedClientRoles).then(function() { $scope.clientMappings = ClientRoleMapping.query({realm : realm.realm, userId : user.id, client : $scope.targetClient.id}); $scope.clientRoles = AvailableClientRoleMapping.query({realm : realm.realm, userId : user.id, client : $scope.targetClient.id}); $scope.clientComposite = CompositeClientRoleMapping.query({realm : realm.realm, userId : user.id, client : $scope.targetClient.id}); @@ -78,7 +78,7 @@ module.controller('UserRoleMappingCtrl', function($scope, $http, realm, user, cl $scope.deleteClientRole = function() { $http.delete(authUrl + '/admin/realms/' + realm.realm + '/users/' + user.id + '/role-mappings/clients/' + $scope.targetClient.id, - {data : $scope.selectedClientMappings, headers : {"content-type" : "application/json"}}).success(function() { + {data : $scope.selectedClientMappings, headers : {"content-type" : "application/json"}}).then(function() { $scope.clientMappings = ClientRoleMapping.query({realm : realm.realm, userId : user.id, client : $scope.targetClient.id}); $scope.clientRoles = AvailableClientRoleMapping.query({realm : realm.realm, userId : user.id, client : $scope.targetClient.id}); $scope.clientComposite = CompositeClientRoleMapping.query({realm : realm.realm, userId : user.id, client : $scope.targetClient.id}); @@ -402,7 +402,11 @@ module.controller('UserDetailCtrl', function($scope, realm, user, BruteForceUser $scope.userReqActionList.push(item); } } - + console.log("---------------------"); + console.log("ng-model: user.requiredActions=" + JSON.stringify($scope.user.requiredActions)); + console.log("---------------------"); + console.log("ng-repeat: userReqActionList=" + JSON.stringify($scope.userReqActionList)); + console.log("---------------------"); }); $scope.$watch('user', function() { if (!angular.equals($scope.user, user)) { diff --git a/themes/src/main/resources/theme/base/admin/resources/js/services.js b/themes/src/main/resources/theme/base/admin/resources/js/services.js index e850b3bc4b..fca7b334f1 100755 --- a/themes/src/main/resources/theme/base/admin/resources/js/services.js +++ b/themes/src/main/resources/theme/base/admin/resources/js/services.js @@ -320,12 +320,57 @@ module.factory('RealmLDAPConnectionTester', function($resource) { return $resource(authUrl + '/admin/realms/:realm/testLDAPConnection'); }); +module.factory('RealmSMTPConnectionTester', function($resource) { + return $resource(authUrl + '/admin/realms/:realm/testSMTPConnection/:config', { + realm : '@realm', + config : '@config' + }, { + send: { + method: 'POST' + } + }); +}); + module.service('ServerInfo', function($resource, $q, $http) { var info = {}; var delay = $q.defer(); - $http.get(authUrl + '/admin/serverinfo').success(function(data) { + function copyInfo(data, info) { angular.copy(data, info); + + info.listProviderIds = function(spi) { + var providers = info.providers[spi].providers; + var ids = Object.keys(providers); + ids.sort(function(a, b) { + var s1; + var s2; + + if (providers[a].order != providers[b].order) { + s1 = providers[b].order; + s2 = providers[a].order; + } else { + s1 = a; + s2 = b; + } + + if (s1 < s2) { + return -1; + } else if (s1 > s2) { + return 1; + } else { + return 0; + } + }); + return ids; + } + + info.featureEnabled = function(provider) { + return info.profileInfo.disabledFeatures.indexOf(provider) == -1; + } + } + + $http.get(authUrl + '/admin/serverinfo').then(function(response) { + copyInfo(response.data, info); delay.resolve(info); }); @@ -334,8 +379,8 @@ module.service('ServerInfo', function($resource, $q, $http) { return info; }, reload: function() { - $http.get(authUrl + '/admin/serverinfo').success(function(data) { - angular.copy(data, info); + $http.get(authUrl + '/admin/serverinfo').then(function(response) { + copyInfo(response.data, info); }); }, promise: delay.promise @@ -732,7 +777,7 @@ function roleControl($scope, realm, role, roles, clients, $scope.addRealmRole = function() { $scope.compositeSwitchDisabled=true; $http.post(authUrl + '/admin/realms/' + realm.realm + '/roles-by-id/' + role.id + '/composites', - $scope.selectedRealmRoles).success(function() { + $scope.selectedRealmRoles).then(function() { for (var i = 0; i < $scope.selectedRealmRoles.length; i++) { var role = $scope.selectedRealmRoles[i]; var idx = $scope.realmRoles.indexOf($scope.selectedRealmRoles[i]); @@ -749,7 +794,7 @@ function roleControl($scope, realm, role, roles, clients, $scope.deleteRealmRole = function() { $scope.compositeSwitchDisabled=true; $http.delete(authUrl + '/admin/realms/' + realm.realm + '/roles-by-id/' + role.id + '/composites', - {data : $scope.selectedRealmMappings, headers : {"content-type" : "application/json"}}).success(function() { + {data : $scope.selectedRealmMappings, headers : {"content-type" : "application/json"}}).then(function() { for (var i = 0; i < $scope.selectedRealmMappings.length; i++) { var role = $scope.selectedRealmMappings[i]; var idx = $scope.realmMappings.indexOf($scope.selectedRealmMappings[i]); @@ -766,7 +811,7 @@ function roleControl($scope, realm, role, roles, clients, $scope.addClientRole = function() { $scope.compositeSwitchDisabled=true; $http.post(authUrl + '/admin/realms/' + realm.realm + '/roles-by-id/' + role.id + '/composites', - $scope.selectedClientRoles).success(function() { + $scope.selectedClientRoles).then(function() { for (var i = 0; i < $scope.selectedClientRoles.length; i++) { var role = $scope.selectedClientRoles[i]; var idx = $scope.clientRoles.indexOf($scope.selectedClientRoles[i]); @@ -782,7 +827,7 @@ function roleControl($scope, realm, role, roles, clients, $scope.deleteClientRole = function() { $scope.compositeSwitchDisabled=true; $http.delete(authUrl + '/admin/realms/' + realm.realm + '/roles-by-id/' + role.id + '/composites', - {data : $scope.selectedClientMappings, headers : {"content-type" : "application/json"}}).success(function() { + {data : $scope.selectedClientMappings, headers : {"content-type" : "application/json"}}).then(function() { for (var i = 0; i < $scope.selectedClientMappings.length; i++) { var role = $scope.selectedClientMappings[i]; var idx = $scope.clientMappings.indexOf($scope.selectedClientMappings[i]); diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/authentication-flow-bindings.html b/themes/src/main/resources/theme/base/admin/resources/partials/authentication-flow-bindings.html index 8a9d0e1ecd..6bf39f3bae 100755 --- a/themes/src/main/resources/theme/base/admin/resources/partials/authentication-flow-bindings.html +++ b/themes/src/main/resources/theme/base/admin/resources/partials/authentication-flow-bindings.html @@ -47,7 +47,7 @@
    - +
    + +
    +
    + {{:: 'docker-auth.tooltip' | translate}} +
    +
    diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/authz/mgmt/client-permissions.html b/themes/src/main/resources/theme/base/admin/resources/partials/authz/mgmt/client-permissions.html index abc21a4d90..7f29fd71c7 100644 --- a/themes/src/main/resources/theme/base/admin/resources/partials/authz/mgmt/client-permissions.html +++ b/themes/src/main/resources/theme/base/admin/resources/partials/authz/mgmt/client-permissions.html @@ -11,7 +11,7 @@
    - +
    {{:: 'permissions-enabled-role.tooltip' | translate}}
    diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/authz/mgmt/client-role-permissions.html b/themes/src/main/resources/theme/base/admin/resources/partials/authz/mgmt/client-role-permissions.html index c5f37ea320..c76ecec055 100644 --- a/themes/src/main/resources/theme/base/admin/resources/partials/authz/mgmt/client-role-permissions.html +++ b/themes/src/main/resources/theme/base/admin/resources/partials/authz/mgmt/client-role-permissions.html @@ -12,7 +12,7 @@
    - +
    {{:: 'permissions-enabled-role.tooltip' | translate}}
    diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/authz/mgmt/group-permissions.html b/themes/src/main/resources/theme/base/admin/resources/partials/authz/mgmt/group-permissions.html index 897a0ed36d..f2be6d99a9 100644 --- a/themes/src/main/resources/theme/base/admin/resources/partials/authz/mgmt/group-permissions.html +++ b/themes/src/main/resources/theme/base/admin/resources/partials/authz/mgmt/group-permissions.html @@ -11,7 +11,7 @@
    - +
    {{:: 'permissions-enabled-role.tooltip' | translate}}
    diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/authz/mgmt/realm-role-permissions.html b/themes/src/main/resources/theme/base/admin/resources/partials/authz/mgmt/realm-role-permissions.html index 9c03333397..e21ee63ed7 100644 --- a/themes/src/main/resources/theme/base/admin/resources/partials/authz/mgmt/realm-role-permissions.html +++ b/themes/src/main/resources/theme/base/admin/resources/partials/authz/mgmt/realm-role-permissions.html @@ -11,7 +11,7 @@
    - +
    {{:: 'permissions-enabled-role.tooltip' | translate}}
    diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/authz/mgmt/users-permissions.html b/themes/src/main/resources/theme/base/admin/resources/partials/authz/mgmt/users-permissions.html index 4a5661f28b..2665bba99a 100644 --- a/themes/src/main/resources/theme/base/admin/resources/partials/authz/mgmt/users-permissions.html +++ b/themes/src/main/resources/theme/base/admin/resources/partials/authz/mgmt/users-permissions.html @@ -7,7 +7,7 @@
    - +
    {{:: 'permissions-enabled-users.tooltip' | translate}}
    diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/authz/permission/provider/resource-server-policy-scope-detail.html b/themes/src/main/resources/theme/base/admin/resources/partials/authz/permission/provider/resource-server-policy-scope-detail.html index 79cec9a603..df4377fd77 100644 --- a/themes/src/main/resources/theme/base/admin/resources/partials/authz/permission/provider/resource-server-policy-scope-detail.html +++ b/themes/src/main/resources/theme/base/admin/resources/partials/authz/permission/provider/resource-server-policy-scope-detail.html @@ -38,7 +38,6 @@
    -
    @@ -110,7 +110,7 @@
    -
    +
    {{:: 'authz-authorization-services-enabled.tooltip' | translate}}
    @@ -239,7 +239,7 @@ {{:: 'name-id-format.tooltip' | translate}}
    -
    +
    @@ -247,7 +247,7 @@ {{:: 'root-url.tooltip' | translate}}
    -
    +
    @@ -269,14 +269,14 @@ {{:: 'valid-redirect-uris.tooltip' | translate}}
    -
    +
    {{:: 'base-url.tooltip' | translate}}
    -
    +
    {{:: 'idp-sso-relay-state.tooltip' | translate}}
    -
    +
    diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/realm-events-config.html b/themes/src/main/resources/theme/base/admin/resources/partials/realm-events-config.html index a304784b2a..c610f68aa4 100755 --- a/themes/src/main/resources/theme/base/admin/resources/partials/realm-events-config.html +++ b/themes/src/main/resources/theme/base/admin/resources/partials/realm-events-config.html @@ -19,9 +19,7 @@ {{:: 'event-listeners.tooltip' | translate}}
    - +
    diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-bitbucket.html b/themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-bitbucket.html new file mode 100755 index 0000000000..90d5c1fbd4 --- /dev/null +++ b/themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-bitbucket.html @@ -0,0 +1,130 @@ +
    + + + + +
    + + + +
    +
    + +
    + +
    + {{:: 'redirect-uri.tooltip' | translate}} +
    +
    +
    +
    + +
    + +
    + {{:: 'bitbucket.key.tooltip' | translate}} +
    +
    + +
    + +
    + {{:: 'bitbucket.secret.tooltip' | translate}} +
    +
    + +
    + +
    + {{:: 'bitbucket.default-scopes.tooltip' | translate}} +
    +
    + +
    + +
    + {{:: 'identity-provider.store-tokens.tooltip' | translate}} +
    +
    + +
    + +
    + {{:: 'identity-provider.stored-tokens-readable.tooltip' | translate}} +
    +
    + +
    + +
    + {{:: 'identity-provider.enabled.tooltip' | translate}} +
    +
    + +
    + +
    + {{:: 'trust-email.tooltip' | translate}} +
    +
    + +
    + +
    + {{:: 'link-only.tooltip' | translate}} +
    +
    + +
    + +
    + {{:: 'hide-on-login-page.tooltip' | translate}} +
    +
    + +
    + +
    + {{:: 'gui-order.tooltip' | translate}} +
    +
    + +
    +
    + +
    +
    + {{:: 'first-broker-login-flow.tooltip' | translate}} +
    +
    + +
    +
    + +
    +
    + {{:: 'post-broker-login-flow.tooltip' | translate}} +
    +
    + +
    +
    + + +
    +
    +
    +
    + + \ No newline at end of file diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-gitlab.html b/themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-gitlab.html new file mode 100755 index 0000000000..152d1f1f40 --- /dev/null +++ b/themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-gitlab.html @@ -0,0 +1,130 @@ +
    + + + + +
    + + + +
    +
    + +
    + +
    + {{:: 'redirect-uri.tooltip' | translate}} +
    +
    +
    +
    + +
    + +
    + {{:: 'gitlab.application-id.tooltip' | translate}} +
    +
    + +
    + +
    + {{:: 'gitlab.application-secret.tooltip' | translate}} +
    +
    + +
    + +
    + {{:: 'gitlab.default-scopes.tooltip' | translate}} +
    +
    + +
    + +
    + {{:: 'identity-provider.store-tokens.tooltip' | translate}} +
    +
    + +
    + +
    + {{:: 'identity-provider.stored-tokens-readable.tooltip' | translate}} +
    +
    + +
    + +
    + {{:: 'identity-provider.enabled.tooltip' | translate}} +
    +
    + +
    + +
    + {{:: 'trust-email.tooltip' | translate}} +
    +
    + +
    + +
    + {{:: 'link-only.tooltip' | translate}} +
    +
    + +
    + +
    + {{:: 'hide-on-login-page.tooltip' | translate}} +
    +
    + +
    + +
    + {{:: 'gui-order.tooltip' | translate}} +
    +
    + +
    +
    + +
    +
    + {{:: 'first-broker-login-flow.tooltip' | translate}} +
    +
    + +
    +
    + +
    +
    + {{:: 'post-broker-login-flow.tooltip' | translate}} +
    +
    + +
    +
    + + +
    +
    +
    +
    + + \ No newline at end of file diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/realm-smtp.html b/themes/src/main/resources/theme/base/admin/resources/partials/realm-smtp.html index 5d3c68eedb..43df76147c 100755 --- a/themes/src/main/resources/theme/base/admin/resources/partials/realm-smtp.html +++ b/themes/src/main/resources/theme/base/admin/resources/partials/realm-smtp.html @@ -10,6 +10,9 @@
    +
    diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/realm-theme-settings.html b/themes/src/main/resources/theme/base/admin/resources/partials/realm-theme-settings.html index ab1d1e2eca..be74a2af8c 100755 --- a/themes/src/main/resources/theme/base/admin/resources/partials/realm-theme-settings.html +++ b/themes/src/main/resources/theme/base/admin/resources/partials/realm-theme-settings.html @@ -65,9 +65,7 @@
    - +
    diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/required-actions.html b/themes/src/main/resources/theme/base/admin/resources/partials/required-actions.html index 45a5be6de7..17dcc05685 100755 --- a/themes/src/main/resources/theme/base/admin/resources/partials/required-actions.html +++ b/themes/src/main/resources/theme/base/admin/resources/partials/required-actions.html @@ -21,7 +21,7 @@ {{requiredAction.name}} - + {{:: 'no-required-actions-configured' | translate}} @@ -31,4 +31,4 @@
    - \ No newline at end of file + diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/user-list.html b/themes/src/main/resources/theme/base/admin/resources/partials/user-list.html index 569e1fc8d4..1fc707cbb4 100755 --- a/themes/src/main/resources/theme/base/admin/resources/partials/user-list.html +++ b/themes/src/main/resources/theme/base/admin/resources/partials/user-list.html @@ -2,7 +2,7 @@ - +
    @@ -32,7 +32,7 @@ - + @@ -54,7 +54,7 @@ - + @@ -66,4 +66,4 @@
    {{:: 'email' | translate}} {{:: 'last-name' | translate}} {{:: 'first-name' | translate}}{{:: 'actions' | translate}}{{:: 'actions' | translate}}
    {{user.lastName}} {{user.firstName}} {{:: 'edit' | translate}}{{:: 'impersonate' | translate}}{{:: 'impersonate' | translate}} {{:: 'delete' | translate}}
    - \ No newline at end of file + diff --git a/themes/src/main/resources/theme/base/admin/resources/templates/kc-menu.html b/themes/src/main/resources/theme/base/admin/resources/templates/kc-menu.html index 53b0a3d5e7..cea1692345 100755 --- a/themes/src/main/resources/theme/base/admin/resources/templates/kc-menu.html +++ b/themes/src/main/resources/theme/base/admin/resources/templates/kc-menu.html @@ -50,7 +50,7 @@
    \ No newline at end of file +
    diff --git a/themes/src/main/resources/theme/base/admin/theme.properties b/themes/src/main/resources/theme/base/admin/theme.properties new file mode 100644 index 0000000000..4bd8da4d73 --- /dev/null +++ b/themes/src/main/resources/theme/base/admin/theme.properties @@ -0,0 +1,2 @@ +import=common/keycloak +locales=ca,en,es,fr,it,ja,lt,no,pt-BR,ru,zh-CN \ No newline at end of file diff --git a/themes/src/main/resources/theme/base/email/html/email-test.ftl b/themes/src/main/resources/theme/base/email/html/email-test.ftl new file mode 100644 index 0000000000..604415d22a --- /dev/null +++ b/themes/src/main/resources/theme/base/email/html/email-test.ftl @@ -0,0 +1,5 @@ + + +${msg("emailTestBodyHtml",realmName)} + + diff --git a/themes/src/main/resources/theme/base/email/html/executeActions.ftl b/themes/src/main/resources/theme/base/email/html/executeActions.ftl index f75e10fa2e..3af8d55f53 100755 --- a/themes/src/main/resources/theme/base/email/html/executeActions.ftl +++ b/themes/src/main/resources/theme/base/email/html/executeActions.ftl @@ -1,5 +1,8 @@ +<#assign requiredActionsText> +<#if requiredActions??><#list requiredActions><#items as reqActionItem>${msg("requiredAction.${reqActionItem}")}<#sep>, <#else> + -${msg("executeActionsBodyHtml",link, linkExpiration, realmName)} +${msg("executeActionsBodyHtml",link, linkExpiration, realmName, requiredActionsText)} diff --git a/themes/src/main/resources/theme/base/email/messages/messages_en.properties b/themes/src/main/resources/theme/base/email/messages/messages_en.properties index 9281bb7f7b..5cb1b6ecfe 100755 --- a/themes/src/main/resources/theme/base/email/messages/messages_en.properties +++ b/themes/src/main/resources/theme/base/email/messages/messages_en.properties @@ -1,6 +1,9 @@ emailVerificationSubject=Verify email emailVerificationBody=Someone has created a {2} account with this email address. If this was you, click the link below to verify your email address\n\n{0}\n\nThis link will expire within {1} minutes.\n\nIf you didn''t create this account, just ignore this message. emailVerificationBodyHtml=

    Someone has created a {2} account with this email address. If this was you, click the link below to verify your email address

    Link to e-mail address verification

    This link will expire within {1} minutes.

    If you didn''t create this account, just ignore this message.

    +emailTestSubject=[KEYCLOAK] - SMTP test message +emailTestBody=This is a test message +emailTestBodyHtml=

    This is a test message

    identityProviderLinkSubject=Link {0} identityProviderLinkBody=Someone wants to link your "{1}" account with "{0}" account of user {2} . If this was you, click the link below to link accounts\n\n{3}\n\nThis link will expire within {4} minutes.\n\nIf you don''t want to link account, just ignore this message. If you link accounts, you will be able to login to {1} through {0}. identityProviderLinkBodyHtml=

    Someone wants to link your {1} account with {0} account of user {2} . If this was you, click the link below to link accounts

    Link to confirm account linking

    This link will expire within {4} minutes.

    If you don''t want to link account, just ignore this message. If you link accounts, you will be able to login to {1} through {0}.

    @@ -8,8 +11,8 @@ passwordResetSubject=Reset password passwordResetBody=Someone just requested to change your {2} account''s credentials. If this was you, click on the link below to reset them.\n\n{0}\n\nThis link and code will expire within {1} minutes.\n\nIf you don''t want to reset your credentials, just ignore this message and nothing will be changed. passwordResetBodyHtml=

    Someone just requested to change your {2} account''s credentials. If this was you, click on the link below to reset them.

    Link to reset credentials

    This link will expire within {1} minutes.

    If you don''t want to reset your credentials, just ignore this message and nothing will be changed.

    executeActionsSubject=Update Your Account -executeActionsBody=Your administrator has just requested that you update your {2} account. Click on the link below to start this process.\n\n{0}\n\nThis link will expire within {1} minutes.\n\nIf you are unaware that your admin has requested this, just ignore this message and nothing will be changed. -executeActionsBodyHtml=

    Your administrator has just requested that you update your {2} account. Click on the link below to start this process.

    Link to account update

    This link will expire within {1} minutes.

    If you are unaware that your admin has requested this, just ignore this message and nothing will be changed.

    +executeActionsBody=Your administrator has just requested that you update your {2} account by performing the following action(s): {3}. Click on the link below to start this process.\n\n{0}\n\nThis link will expire within {1} minutes.\n\nIf you are unaware that your admin has requested this, just ignore this message and nothing will be changed. +executeActionsBodyHtml=

    Your administrator has just requested that you update your {2} account by performing the following action(s): {3}. Click on the link below to start this process.

    Link to account update

    This link will expire within {1} minutes.

    If you are unaware that your admin has requested this, just ignore this message and nothing will be changed.

    eventLoginErrorSubject=Login error eventLoginErrorBody=A failed login attempt was detected to your account on {0} from {1}. If this was not you, please contact an admin. eventLoginErrorBodyHtml=

    A failed login attempt was detected to your account on {0} from {1}. If this was not you, please contact an admin.

    @@ -22,3 +25,9 @@ eventUpdatePasswordBodyHtml=

    Your password was changed on {0} from {1}. If thi eventUpdateTotpSubject=Update TOTP eventUpdateTotpBody=TOTP was updated for your account on {0} from {1}. If this was not you, please contact an admin. eventUpdateTotpBodyHtml=

    TOTP was updated for your account on {0} from {1}. If this was not you, please contact an admin.

    + +requiredAction.CONFIGURE_TOTP=Configure OTP +requiredAction.terms_and_conditions=Terms and Conditions +requiredAction.UPDATE_PASSWORD=Update Password +requiredAction.UPDATE_PROFILE=Update Profile +requiredAction.VERIFY_EMAIL=Verify Email diff --git a/themes/src/main/resources/theme/base/email/messages/messages_zh_CN.properties b/themes/src/main/resources/theme/base/email/messages/messages_zh_CN.properties new file mode 100644 index 0000000000..e27ccdac11 --- /dev/null +++ b/themes/src/main/resources/theme/base/email/messages/messages_zh_CN.properties @@ -0,0 +1,24 @@ +emailVerificationSubject=验证电子邮件 +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=TOTP在 {0} 由{1} 从您的账户中删除.如果这不是您本人操作,请联系管理员 +eventRemoveTotpBodyHtml=

    TOTP在 {0} 由{1} 从您的账户中删除.如果这不是您本人操作,请联系管理员。

    +eventUpdatePasswordSubject=更新密码 +eventUpdatePasswordBody=您的密码在{0} 由 {1}更改. 如非本人操作,请联系管理员 +eventUpdatePasswordBodyHtml=

    您的密码在{0} 由 {1}更改. 如非本人操作,请联系管理员

    +eventUpdateTotpSubject=更新 TOTP +eventUpdateTotpBody=您账户的TOTP 配置在{0} 由 {1}更改. 如非本人操作,请联系管理员。 +eventUpdateTotpBodyHtml=

    您账户的TOTP 配置在{0} 由 {1}更改. 如非本人操作,请联系管理员。

    diff --git a/themes/src/main/resources/theme/base/email/text/email-test.ftl b/themes/src/main/resources/theme/base/email/text/email-test.ftl new file mode 100644 index 0000000000..19942c791f --- /dev/null +++ b/themes/src/main/resources/theme/base/email/text/email-test.ftl @@ -0,0 +1 @@ +${msg("emailTestBody", realmName)} \ No newline at end of file diff --git a/themes/src/main/resources/theme/base/email/text/executeActions.ftl b/themes/src/main/resources/theme/base/email/text/executeActions.ftl index a33758f152..39ce047e7e 100755 --- a/themes/src/main/resources/theme/base/email/text/executeActions.ftl +++ b/themes/src/main/resources/theme/base/email/text/executeActions.ftl @@ -1 +1,4 @@ -${msg("executeActionsBody",link, linkExpiration, realmName)} \ No newline at end of file +<#assign requiredActionsText> +<#if requiredActions??><#list requiredActions><#items as reqActionItem>${msg("requiredAction.${reqActionItem}")}<#sep>, <#else> + +${msg("executeActionsBody",link, linkExpiration, realmName, requiredActionsText)} \ No newline at end of file diff --git a/themes/src/main/resources/theme/base/email/theme.properties b/themes/src/main/resources/theme/base/email/theme.properties new file mode 100644 index 0000000000..b9c3990957 --- /dev/null +++ b/themes/src/main/resources/theme/base/email/theme.properties @@ -0,0 +1 @@ +locales=ca,de,en,es,fr,it,ja,lt,no,pt-BR,ru,zh-CN \ No newline at end of file diff --git a/themes/src/main/resources/theme/base/login/info.ftl b/themes/src/main/resources/theme/base/login/info.ftl index cb228d2a15..c9e197b617 100755 --- a/themes/src/main/resources/theme/base/login/info.ftl +++ b/themes/src/main/resources/theme/base/login/info.ftl @@ -6,11 +6,13 @@ ${message.summary} <#elseif section = "form">
    -

    ${message.summary}

    +

    ${message.summary}<#if requiredActions??><#list requiredActions>: <#items as reqActionItem>${msg("requiredAction.${reqActionItem}")}<#sep>, <#else>

    <#if skipLink??> <#else> <#if pageRedirectUri??>

    ${msg("backToApplication")}

    + <#elseif actionUri??> +

    ${msg("proceedWithAction")}

    <#elseif client.baseUrl??>

    ${msg("backToApplication")}

    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 cf262361b7..dbd0a3c340 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 @@ -130,6 +130,8 @@ accountDisabledMessage=Account is disabled, contact admin. accountTemporarilyDisabledMessage=Account is temporarily disabled, contact admin or try again later. expiredCodeMessage=Login timeout. Please login again. expiredActionMessage=Action expired. Please continue with login now. +expiredActionTokenNoSessionMessage=Action expired. +expiredActionTokenSessionExistsMessage=Action expired. Please start again. missingFirstNameMessage=Please specify first name. missingLastNameMessage=Please specify last name. @@ -217,6 +219,9 @@ identityProviderNotUniqueMessage=Realm supports multiple identity providers. Cou emailVerifiedMessage=Your email address has been verified. staleEmailVerificationLink=The link you clicked is a old stale link and is no longer valid. Maybe you have already verified your email? identityProviderAlreadyLinkedMessage=Federated identity returned by {0} is already linked to another user. +confirmAccountLinking=Confirm linking the account {0} of identity provider {1} with your account. +confirmEmailAddressVerification=Confirm validity of e-mail address {0}. +confirmExecutionOfActions=Perform the following action(s) locale_ca=Catal\u00E0 locale_de=Deutsch @@ -230,6 +235,8 @@ locale_pt_BR=Portugu\u00EAs (Brasil) locale_pt-BR=Portugu\u00EAs (Brasil) locale_ru=\u0420\u0443\u0441\u0441\u043A\u0438\u0439 locale_lt=Lietuvi\u0173 +locale_zh-CN=\u4e2d\u6587\u7b80\u4f53 +locale_sv=Svenska backToApplication=« Back to Application missingParameterMessage=Missing parameters\: {0} @@ -239,5 +246,12 @@ invalidParameterMessage=Invalid parameter\: {0} alreadyLoggedIn=You are already logged in. differentUserAuthenticated=You are already authenticated as different user ''{0}'' in this session. Please logout first. brokerLinkingSessionExpired=Requested broker account linking, but current session is no longer valid. +proceedWithAction=» Click here to proceed + +requiredAction.CONFIGURE_TOTP=Configure OTP +requiredAction.terms_and_conditions=Terms and Conditions +requiredAction.UPDATE_PASSWORD=Update Password +requiredAction.UPDATE_PROFILE=Update Profile +requiredAction.VERIFY_EMAIL=Verify Email p3pPolicy=CP="This is not a P3P policy!" diff --git a/themes/src/main/resources/theme/base/login/messages/messages_zh_CN.properties b/themes/src/main/resources/theme/base/login/messages/messages_zh_CN.properties new file mode 100644 index 0000000000..7a1a0728ba --- /dev/null +++ b/themes/src/main/resources/theme/base/login/messages/messages_zh_CN.properties @@ -0,0 +1,232 @@ +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=手机验证者配置 +loginProfileTitle=更新账户信息 +loginTimeout=登录超时,请重新开始登录 +oauthGrantTitle=授权 +oauthGrantTitleHtml={0} +errorTitle=很抱歉... +errorTitleHtml=我们很抱歉 ... +emailVerifyTitle=验证电子邮件地址 +emailForgotTitle=忘记密码? +updatePasswordTitle=更新密码 +codeSuccessTitle=成功码 +codeErrorTitle=错误码\: {0} + +termsTitle=条款 +termsTitleHtml=条款 +termsText=

    需要确定的条款

    + +recaptchaFailed=无效的验证码 +recaptchaNotConfigured=需要验证码,但是没有配置 +consentDenied=许可被拒绝。 + +noAccount=新用户? +username=用户名 +usernameOrEmail=用户名 或 电子邮箱地址 +firstName=名 +givenName=姓 +fullName=全名 +lastName=姓 +familyName=姓 +email=Email +password=密码 +passwordConfirm=确认密码 +passwordNew=新密码 +passwordNewConfirm=新密码确认 +rememberMe=记住我 +authenticatorCode=一次性验证码 +address=地址 +street=街道 +locality=市 +region=省,自治区,直辖市 +postal_code=邮政编码 +country=国家 +emailVerified=电子邮件已验证 +gssDelegationCredential=GSS Delegation Credential + +loginTotpStep1=在手机安装 FreeOTP 或 Google Authenticator. 这两个应用可以在 Google Play 和 Apple App Store找到. +loginTotpStep2=打开应用扫描二维码或者输入一次性码 +loginTotpStep3=输入应用提供的一次性码点击提交完成设置 +loginTotpOneTime=一次性验证码 + +oauthGrantRequest=您是否想要授予下列权限? +inResource=in + +emailVerifyInstruction1=一封包含验证邮箱具体步骤的邮件已经发送到您的邮箱。 +emailVerifyInstruction2=邮箱没有收到验证码? +emailVerifyInstruction3=重新发送电子邮件 + +emailLinkIdpTitle=链接 {0} +emailLinkIdp1=一封包含链接账户 {0} 和账户 {1} 到账户 {2} 的邮件已经发送到您的邮箱。 +emailLinkIdp2=邮箱没有收到验证码邮件? +emailLinkIdp3=重新发送电子邮件 + +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=读取 token +role_offline-access=离线访问 +client_account=账户 +client_security-admin-console=安全管理控制台 +client_admin-cli=管理命令行工具 +client_realm-management=域管理 +client_broker=代理 + +invalidUserMessage=无效的用户名或密码。 +invalidEmailMessage=无效的电子邮件地址 +accountDisabledMessage=账户被禁用,请联系管理员。 +accountTemporarilyDisabledMessage=账户被暂时禁用,请稍后再试或联系管理员。 +expiredCodeMessage=登录超时,请重新登陆。 + +missingFirstNameMessage=请输入名 +missingLastNameMessage=请输入姓 +missingEmailMessage=请输入email. +missingUsernameMessage=请输入用户名 +missingPasswordMessage=请输入密码 +missingTotpMessage=请输入验证码 +notMatchPasswordMessage=密码不匹配。 + +invalidPasswordExistingMessage=无效的旧密码 +invalidPasswordConfirmMessage=确认密码不相同 +invalidTotpMessage=无效的验证码 + +usernameExistsMessage=用户名已被占用 +emailExistsMessage=电子邮件已存在。 + +federatedIdentityExistsMessage=用户 {0} {1} 已存在. 请登录账户管理界面链接账户. + +confirmLinkIdpTitle=账户已存在 +federatedIdentityConfirmLinkMessage=用户{0} {1} 已存在. 怎么继续? +federatedIdentityConfirmReauthenticateMessage=以 {0} 登录来将 {1} 连接到您的账户 +confirmLinkIdpReviewProfile=审查您的信息 +confirmLinkIdpContinue=添加到已知账户 + +configureTotpMessage=您需要设置验证码模块来激活您的账户 +updateProfileMessage=您需要更新您的简介来激活您的账户 +updatePasswordMessage=您需要更新您的密码来激活您的账户 +verifyEmailMessage=您需要验证您的电子邮箱来激活您的账户 +linkIdpMessage=您需要验证您的电子邮箱来连接到账户{0}. + +emailSentMessage=您很快会收到一封关于接下来操作的邮件。 +emailSendErrorMessage=无法发送邮件,请稍后再试 + +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=客户程序不允许发起指定返回类型的浏览器登录. 标准的登录流程已禁用。 +implicitFlowDisabledMessage=客户程序不允许发起指定返回类型的浏览器登录. 隐式的登录流程已禁用。 +invalidRedirectUriMessage=无效的跳转链接 +unsupportedNameIdFormatMessage=不支持的 nameID格式 +invalidRequesterMessage=无效的发起者 +registrationNotAllowedMessage=注册不允许 +resetCredentialNotAllowedMessage=不允许重置密码 + +permissionNotApprovedMessage=许可没有批准 +noRelayStateInResponseMessage=身份提供者没有返回中继状态信息 +insufficientPermissionMessage=权限不足以链接新的身份 +couldNotProceedWithAuthenticationRequestMessage=无法与身份提供者处理认证请求 +couldNotObtainTokenMessage=未从身份提供者获得token +unexpectedErrorRetrievingTokenMessage=从身份提供者获得Token时遇到未知错误 +unexpectedErrorHandlingResponseMessage=从身份提供者获得回复时遇到未知错误 +identityProviderAuthenticationFailedMessage=认证失败,无法通过身份提供者认证 +identityProviderDifferentUserMessage=认证为 {0}, 但期望认证为 {1} +couldNotSendAuthenticationRequestMessage=无法向身份提供方发送认证请求 +unexpectedErrorHandlingRequestMessage=在处理发向认证提供方的请求时,出现未知错误。 +invalidAccessCodeMessage=无效的验证码 +sessionNotActiveMessage=会话不在活动状态 +invalidCodeMessage=发生错误,请重新通过应用登录 +identityProviderUnexpectedErrorMessage=在与认证提供者认证过程中发生未知错误 +identityProviderNotFoundMessage=无法找到认证提供方 +identityProviderLinkSuccess=您的账户已经将账户{0} 与账户 {1} 链接. +staleCodeMessage=当前页面已无效,请到登录界面重新登录 +realmSupportsNoCredentialsMessage=域不支持特定类型密码 +identityProviderNotUniqueMessage=域支持通过多个身份提供者登录,不知道应用哪一种方式登录 +emailVerifiedMessage=您的电子邮箱已经验证。 +staleEmailVerificationLink=您点击的链接已无效。可能您已经验证过您的电子邮箱? + +locale_ca=Català +locale_de=Deutsch +locale_en=English +locale_es=Español +locale_fr=Français +locale_it=Italian +locale_ja=日本語 +locale_no=Norsk +locale_pt_BR=Português (Brasil) +locale_pt-BR=Português (Brasil) +locale_ru=Русский +locale_lt=Lietuvių +locale_zh-CN=中文简体 + +backToApplication=« 回到应用 +missingParameterMessage=缺少参数 \: {0} +clientNotFoundMessage=客户端未找到 +clientDisabledMessage=客户端已禁用 +invalidParameterMessage=无效的参数 \: {0} +alreadyLoggedIn=您已经登录 + +p3pPolicy="This is not a P3P policy!" diff --git a/themes/src/main/resources/theme/base/login/theme.properties b/themes/src/main/resources/theme/base/login/theme.properties new file mode 100644 index 0000000000..b9c3990957 --- /dev/null +++ b/themes/src/main/resources/theme/base/login/theme.properties @@ -0,0 +1 @@ +locales=ca,de,en,es,fr,it,ja,lt,no,pt-BR,ru,zh-CN \ No newline at end of file diff --git a/themes/src/main/resources/theme/keycloak/admin/theme.properties b/themes/src/main/resources/theme/keycloak/admin/theme.properties index c9307850ac..8519d8907c 100755 --- a/themes/src/main/resources/theme/keycloak/admin/theme.properties +++ b/themes/src/main/resources/theme/keycloak/admin/theme.properties @@ -1,3 +1,3 @@ parent=base import=common/keycloak -styles=lib/patternfly/css/patternfly.css lib/select2-3.4.1/select2.css css/styles.css lib/angular/treeview/css/angular.treeview.css \ No newline at end of file +styles=lib/patternfly/css/patternfly.css node_modules/select2/select2.css css/styles.css lib/angular/treeview/css/angular.treeview.css \ No newline at end of file diff --git a/themes/src/main/resources/theme/keycloak/common/resources/README.md b/themes/src/main/resources/theme/keycloak/common/resources/README.md new file mode 100644 index 0000000000..a5ef852162 --- /dev/null +++ b/themes/src/main/resources/theme/keycloak/common/resources/README.md @@ -0,0 +1,24 @@ +Management of javascript libraries +=================================================== + +Javascript libraries under the *./lib* directory are not managed. These +libraries are not available in the public npm repo and are thus checked into +GitHub. + +Javascript libraries under *./node_modules* directory are managed with yarn. +THEY SHOULD NOT BE CHECKED INTO GITHUB! + +Adding or Removing javascript libraries +--------------------------------------- +To add/remove/update javascript libraries you should always use yarn so that +the yarn.lock file will be updated. Then, just check in the modified version +of package.json and yarn.lock. To do this, you should locally install +nodejs/npm and yarn. + +Do not use *npm install --save*. If you try to update a dependency using +package.json and fail to update yarn.lock, then the next build will fail. + +To locally install nodejs/npm and yarn, see: + +* [Install nodejs and npm](https://www.npmjs.com/get-npm) +* [Install yarn](https://yarnpkg.com/lang/en/docs/install/) diff --git a/themes/src/main/resources/theme/keycloak/common/resources/lib/angular/angular-cookies.js b/themes/src/main/resources/theme/keycloak/common/resources/lib/angular/angular-cookies.js deleted file mode 100644 index 634386a57e..0000000000 --- a/themes/src/main/resources/theme/keycloak/common/resources/lib/angular/angular-cookies.js +++ /dev/null @@ -1,321 +0,0 @@ -/** - * @license AngularJS v1.4.4 - * (c) 2010-2015 Google, Inc. http://angularjs.org - * License: MIT - */ -(function(window, angular, undefined) {'use strict'; - -/** - * @ngdoc module - * @name ngCookies - * @description - * - * # ngCookies - * - * The `ngCookies` module provides a convenient wrapper for reading and writing browser cookies. - * - * - *
    - * - * See {@link ngCookies.$cookies `$cookies`} for usage. - */ - - -angular.module('ngCookies', ['ng']). - /** - * @ngdoc provider - * @name $cookiesProvider - * @description - * Use `$cookiesProvider` to change the default behavior of the {@link ngCookies.$cookies $cookies} service. - * */ - provider('$cookies', [function $CookiesProvider() { - /** - * @ngdoc property - * @name $cookiesProvider#defaults - * @description - * - * Object containing default options to pass when setting cookies. - * - * The object may have following properties: - * - * - **path** - `{string}` - The cookie will be available only for this path and its - * sub-paths. By default, this would be the URL that appears in your base tag. - * - **domain** - `{string}` - The cookie will be available only for this domain and - * its sub-domains. For obvious security reasons the user agent will not accept the - * cookie if the current domain is not a sub domain or equals to the requested domain. - * - **expires** - `{string|Date}` - String of the form "Wdy, DD Mon YYYY HH:MM:SS GMT" - * or a Date object indicating the exact date/time this cookie will expire. - * - **secure** - `{boolean}` - The cookie will be available only in secured connection. - * - * Note: by default the address that appears in your `` tag will be used as path. - * This is important so that cookies will be visible for all routes in case html5mode is enabled - * - **/ - var defaults = this.defaults = {}; - - function calcOptions(options) { - return options ? angular.extend({}, defaults, options) : defaults; - } - - /** - * @ngdoc service - * @name $cookies - * - * @description - * Provides read/write access to browser's cookies. - * - *
    - * Up until Angular 1.3, `$cookies` exposed properties that represented the - * current browser cookie values. In version 1.4, this behavior has changed, and - * `$cookies` now provides a standard api of getters, setters etc. - *
    - * - * Requires the {@link ngCookies `ngCookies`} module to be installed. - * - * @example - * - * ```js - * angular.module('cookiesExample', ['ngCookies']) - * .controller('ExampleController', ['$cookies', function($cookies) { - * // Retrieving a cookie - * var favoriteCookie = $cookies.get('myFavorite'); - * // Setting a cookie - * $cookies.put('myFavorite', 'oatmeal'); - * }]); - * ``` - */ - this.$get = ['$$cookieReader', '$$cookieWriter', function($$cookieReader, $$cookieWriter) { - return { - /** - * @ngdoc method - * @name $cookies#get - * - * @description - * Returns the value of given cookie key - * - * @param {string} key Id to use for lookup. - * @returns {string} Raw cookie value. - */ - get: function(key) { - return $$cookieReader()[key]; - }, - - /** - * @ngdoc method - * @name $cookies#getObject - * - * @description - * Returns the deserialized value of given cookie key - * - * @param {string} key Id to use for lookup. - * @returns {Object} Deserialized cookie value. - */ - getObject: function(key) { - var value = this.get(key); - return value ? angular.fromJson(value) : value; - }, - - /** - * @ngdoc method - * @name $cookies#getAll - * - * @description - * Returns a key value object with all the cookies - * - * @returns {Object} All cookies - */ - getAll: function() { - return $$cookieReader(); - }, - - /** - * @ngdoc method - * @name $cookies#put - * - * @description - * Sets a value for given cookie key - * - * @param {string} key Id for the `value`. - * @param {string} value Raw value to be stored. - * @param {Object=} options Options object. - * See {@link ngCookies.$cookiesProvider#defaults $cookiesProvider.defaults} - */ - put: function(key, value, options) { - $$cookieWriter(key, value, calcOptions(options)); - }, - - /** - * @ngdoc method - * @name $cookies#putObject - * - * @description - * Serializes and sets a value for given cookie key - * - * @param {string} key Id for the `value`. - * @param {Object} value Value to be stored. - * @param {Object=} options Options object. - * See {@link ngCookies.$cookiesProvider#defaults $cookiesProvider.defaults} - */ - putObject: function(key, value, options) { - this.put(key, angular.toJson(value), options); - }, - - /** - * @ngdoc method - * @name $cookies#remove - * - * @description - * Remove given cookie - * - * @param {string} key Id of the key-value pair to delete. - * @param {Object=} options Options object. - * See {@link ngCookies.$cookiesProvider#defaults $cookiesProvider.defaults} - */ - remove: function(key, options) { - $$cookieWriter(key, undefined, calcOptions(options)); - } - }; - }]; - }]); - -angular.module('ngCookies'). -/** - * @ngdoc service - * @name $cookieStore - * @deprecated - * @requires $cookies - * - * @description - * Provides a key-value (string-object) storage, that is backed by session cookies. - * Objects put or retrieved from this storage are automatically serialized or - * deserialized by angular's toJson/fromJson. - * - * Requires the {@link ngCookies `ngCookies`} module to be installed. - * - *
    - * **Note:** The $cookieStore service is **deprecated**. - * Please use the {@link ngCookies.$cookies `$cookies`} service instead. - *
    - * - * @example - * - * ```js - * angular.module('cookieStoreExample', ['ngCookies']) - * .controller('ExampleController', ['$cookieStore', function($cookieStore) { - * // Put cookie - * $cookieStore.put('myFavorite','oatmeal'); - * // Get cookie - * var favoriteCookie = $cookieStore.get('myFavorite'); - * // Removing a cookie - * $cookieStore.remove('myFavorite'); - * }]); - * ``` - */ - factory('$cookieStore', ['$cookies', function($cookies) { - - return { - /** - * @ngdoc method - * @name $cookieStore#get - * - * @description - * Returns the value of given cookie key - * - * @param {string} key Id to use for lookup. - * @returns {Object} Deserialized cookie value, undefined if the cookie does not exist. - */ - get: function(key) { - return $cookies.getObject(key); - }, - - /** - * @ngdoc method - * @name $cookieStore#put - * - * @description - * Sets a value for given cookie key - * - * @param {string} key Id for the `value`. - * @param {Object} value Value to be stored. - */ - put: function(key, value) { - $cookies.putObject(key, value); - }, - - /** - * @ngdoc method - * @name $cookieStore#remove - * - * @description - * Remove given cookie - * - * @param {string} key Id of the key-value pair to delete. - */ - remove: function(key) { - $cookies.remove(key); - } - }; - - }]); - -/** - * @name $$cookieWriter - * @requires $document - * - * @description - * This is a private service for writing cookies - * - * @param {string} name Cookie name - * @param {string=} value Cookie value (if undefined, cookie will be deleted) - * @param {Object=} options Object with options that need to be stored for the cookie. - */ -function $$CookieWriter($document, $log, $browser) { - var cookiePath = $browser.baseHref(); - var rawDocument = $document[0]; - - function buildCookieString(name, value, options) { - var path, expires; - options = options || {}; - expires = options.expires; - path = angular.isDefined(options.path) ? options.path : cookiePath; - if (value === undefined) { - expires = 'Thu, 01 Jan 1970 00:00:00 GMT'; - value = ''; - } - if (angular.isString(expires)) { - expires = new Date(expires); - } - - var str = encodeURIComponent(name) + '=' + encodeURIComponent(value); - str += path ? ';path=' + path : ''; - str += options.domain ? ';domain=' + options.domain : ''; - str += expires ? ';expires=' + expires.toUTCString() : ''; - str += options.secure ? ';secure' : ''; - - // per http://www.ietf.org/rfc/rfc2109.txt browser must allow at minimum: - // - 300 cookies - // - 20 cookies per unique domain - // - 4096 bytes per cookie - var cookieLength = str.length + 1; - if (cookieLength > 4096) { - $log.warn("Cookie '" + name + - "' possibly not set or overflowed because it was too large (" + - cookieLength + " > 4096 bytes)!"); - } - - return str; - } - - return function(name, value, options) { - rawDocument.cookie = buildCookieString(name, value, options); - }; -} - -$$CookieWriter.$inject = ['$document', '$log', '$browser']; - -angular.module('ngCookies').provider('$$cookieWriter', function $$CookieWriterProvider() { - this.$get = $$CookieWriter; -}); - - -})(window, window.angular); diff --git a/themes/src/main/resources/theme/keycloak/common/resources/lib/angular/angular-loader.js b/themes/src/main/resources/theme/keycloak/common/resources/lib/angular/angular-loader.js deleted file mode 100644 index f117396f7c..0000000000 --- a/themes/src/main/resources/theme/keycloak/common/resources/lib/angular/angular-loader.js +++ /dev/null @@ -1,443 +0,0 @@ -/** - * @license AngularJS v1.4.4 - * (c) 2010-2015 Google, Inc. http://angularjs.org - * License: MIT - */ - -(function() {'use strict'; - function isFunction(value) {return typeof value === 'function';}; - -/** - * @description - * - * This object provides a utility for producing rich Error messages within - * Angular. It can be called as follows: - * - * var exampleMinErr = minErr('example'); - * throw exampleMinErr('one', 'This {0} is {1}', foo, bar); - * - * The above creates an instance of minErr in the example namespace. The - * resulting error will have a namespaced error code of example.one. The - * resulting error will replace {0} with the value of foo, and {1} with the - * value of bar. The object is not restricted in the number of arguments it can - * take. - * - * If fewer arguments are specified than necessary for interpolation, the extra - * interpolation markers will be preserved in the final string. - * - * Since data will be parsed statically during a build step, some restrictions - * are applied with respect to how minErr instances are created and called. - * Instances should have names of the form namespaceMinErr for a minErr created - * using minErr('namespace') . Error codes, namespaces and template strings - * should all be static strings, not variables or general expressions. - * - * @param {string} module The namespace to use for the new minErr instance. - * @param {function} ErrorConstructor Custom error constructor to be instantiated when returning - * error from returned function, for cases when a particular type of error is useful. - * @returns {function(code:string, template:string, ...templateArgs): Error} minErr instance - */ - -function minErr(module, ErrorConstructor) { - ErrorConstructor = ErrorConstructor || Error; - return function() { - var SKIP_INDEXES = 2; - - var templateArgs = arguments, - code = templateArgs[0], - message = '[' + (module ? module + ':' : '') + code + '] ', - template = templateArgs[1], - paramPrefix, i; - - message += template.replace(/\{\d+\}/g, function(match) { - var index = +match.slice(1, -1), - shiftedIndex = index + SKIP_INDEXES; - - if (shiftedIndex < templateArgs.length) { - return toDebugString(templateArgs[shiftedIndex]); - } - - return match; - }); - - message += '\nhttp://errors.angularjs.org/1.4.4/' + - (module ? module + '/' : '') + code; - - for (i = SKIP_INDEXES, paramPrefix = '?'; i < templateArgs.length; i++, paramPrefix = '&') { - message += paramPrefix + 'p' + (i - SKIP_INDEXES) + '=' + - encodeURIComponent(toDebugString(templateArgs[i])); - } - - return new ErrorConstructor(message); - }; -} - -/** - * @ngdoc type - * @name angular.Module - * @module ng - * @description - * - * Interface for configuring angular {@link angular.module modules}. - */ - -function setupModuleLoader(window) { - - var $injectorMinErr = minErr('$injector'); - var ngMinErr = minErr('ng'); - - function ensure(obj, name, factory) { - return obj[name] || (obj[name] = factory()); - } - - var angular = ensure(window, 'angular', Object); - - // We need to expose `angular.$$minErr` to modules such as `ngResource` that reference it during bootstrap - angular.$$minErr = angular.$$minErr || minErr; - - return ensure(angular, 'module', function() { - /** @type {Object.} */ - var modules = {}; - - /** - * @ngdoc function - * @name angular.module - * @module ng - * @description - * - * The `angular.module` is a global place for creating, registering and retrieving Angular - * modules. - * All modules (angular core or 3rd party) that should be available to an application must be - * registered using this mechanism. - * - * Passing one argument retrieves an existing {@link angular.Module}, - * whereas passing more than one argument creates a new {@link angular.Module} - * - * - * # Module - * - * A module is a collection of services, directives, controllers, filters, and configuration information. - * `angular.module` is used to configure the {@link auto.$injector $injector}. - * - * ```js - * // Create a new module - * var myModule = angular.module('myModule', []); - * - * // register a new service - * myModule.value('appName', 'MyCoolApp'); - * - * // configure existing services inside initialization blocks. - * myModule.config(['$locationProvider', function($locationProvider) { - * // Configure existing providers - * $locationProvider.hashPrefix('!'); - * }]); - * ``` - * - * Then you can create an injector and load your modules like this: - * - * ```js - * var injector = angular.injector(['ng', 'myModule']) - * ``` - * - * However it's more likely that you'll just use - * {@link ng.directive:ngApp ngApp} or - * {@link angular.bootstrap} to simplify this process for you. - * - * @param {!string} name The name of the module to create or retrieve. - * @param {!Array.=} requires If specified then new module is being created. If - * unspecified then the module is being retrieved for further configuration. - * @param {Function=} configFn Optional configuration function for the module. Same as - * {@link angular.Module#config Module#config()}. - * @returns {module} new module with the {@link angular.Module} api. - */ - return function module(name, requires, configFn) { - var assertNotHasOwnProperty = function(name, context) { - if (name === 'hasOwnProperty') { - throw ngMinErr('badname', 'hasOwnProperty is not a valid {0} name', context); - } - }; - - assertNotHasOwnProperty(name, 'module'); - if (requires && modules.hasOwnProperty(name)) { - modules[name] = null; - } - return ensure(modules, name, function() { - if (!requires) { - throw $injectorMinErr('nomod', "Module '{0}' is not available! You either misspelled " + - "the module name or forgot to load it. If registering a module ensure that you " + - "specify the dependencies as the second argument.", name); - } - - /** @type {!Array.>} */ - var invokeQueue = []; - - /** @type {!Array.} */ - var configBlocks = []; - - /** @type {!Array.} */ - var runBlocks = []; - - var config = invokeLater('$injector', 'invoke', 'push', configBlocks); - - /** @type {angular.Module} */ - var moduleInstance = { - // Private state - _invokeQueue: invokeQueue, - _configBlocks: configBlocks, - _runBlocks: runBlocks, - - /** - * @ngdoc property - * @name angular.Module#requires - * @module ng - * - * @description - * Holds the list of modules which the injector will load before the current module is - * loaded. - */ - requires: requires, - - /** - * @ngdoc property - * @name angular.Module#name - * @module ng - * - * @description - * Name of the module. - */ - name: name, - - - /** - * @ngdoc method - * @name angular.Module#provider - * @module ng - * @param {string} name service name - * @param {Function} providerType Construction function for creating new instance of the - * service. - * @description - * See {@link auto.$provide#provider $provide.provider()}. - */ - provider: invokeLaterAndSetModuleName('$provide', 'provider'), - - /** - * @ngdoc method - * @name angular.Module#factory - * @module ng - * @param {string} name service name - * @param {Function} providerFunction Function for creating new instance of the service. - * @description - * See {@link auto.$provide#factory $provide.factory()}. - */ - factory: invokeLaterAndSetModuleName('$provide', 'factory'), - - /** - * @ngdoc method - * @name angular.Module#service - * @module ng - * @param {string} name service name - * @param {Function} constructor A constructor function that will be instantiated. - * @description - * See {@link auto.$provide#service $provide.service()}. - */ - service: invokeLaterAndSetModuleName('$provide', 'service'), - - /** - * @ngdoc method - * @name angular.Module#value - * @module ng - * @param {string} name service name - * @param {*} object Service instance object. - * @description - * See {@link auto.$provide#value $provide.value()}. - */ - value: invokeLater('$provide', 'value'), - - /** - * @ngdoc method - * @name angular.Module#constant - * @module ng - * @param {string} name constant name - * @param {*} object Constant value. - * @description - * Because the constant are fixed, they get applied before other provide methods. - * See {@link auto.$provide#constant $provide.constant()}. - */ - constant: invokeLater('$provide', 'constant', 'unshift'), - - /** - * @ngdoc method - * @name angular.Module#decorator - * @module ng - * @param {string} The name of the service to decorate. - * @param {Function} This function will be invoked when the service needs to be - * instantiated and should return the decorated service instance. - * @description - * See {@link auto.$provide#decorator $provide.decorator()}. - */ - decorator: invokeLaterAndSetModuleName('$provide', 'decorator'), - - /** - * @ngdoc method - * @name angular.Module#animation - * @module ng - * @param {string} name animation name - * @param {Function} animationFactory Factory function for creating new instance of an - * animation. - * @description - * - * **NOTE**: animations take effect only if the **ngAnimate** module is loaded. - * - * - * Defines an animation hook that can be later used with - * {@link $animate $animate} service and directives that use this service. - * - * ```js - * module.animation('.animation-name', function($inject1, $inject2) { - * return { - * eventName : function(element, done) { - * //code to run the animation - * //once complete, then run done() - * return function cancellationFunction(element) { - * //code to cancel the animation - * } - * } - * } - * }) - * ``` - * - * See {@link ng.$animateProvider#register $animateProvider.register()} and - * {@link ngAnimate ngAnimate module} for more information. - */ - animation: invokeLaterAndSetModuleName('$animateProvider', 'register'), - - /** - * @ngdoc method - * @name angular.Module#filter - * @module ng - * @param {string} name Filter name - this must be a valid angular expression identifier - * @param {Function} filterFactory Factory function for creating new instance of filter. - * @description - * See {@link ng.$filterProvider#register $filterProvider.register()}. - * - *
    - * **Note:** Filter names must be valid angular {@link expression} identifiers, such as `uppercase` or `orderBy`. - * Names with special characters, such as hyphens and dots, are not allowed. If you wish to namespace - * your filters, then you can use capitalization (`myappSubsectionFilterx`) or underscores - * (`myapp_subsection_filterx`). - *
    - */ - filter: invokeLaterAndSetModuleName('$filterProvider', 'register'), - - /** - * @ngdoc method - * @name angular.Module#controller - * @module ng - * @param {string|Object} name Controller name, or an object map of controllers where the - * keys are the names and the values are the constructors. - * @param {Function} constructor Controller constructor function. - * @description - * See {@link ng.$controllerProvider#register $controllerProvider.register()}. - */ - controller: invokeLaterAndSetModuleName('$controllerProvider', 'register'), - - /** - * @ngdoc method - * @name angular.Module#directive - * @module ng - * @param {string|Object} name Directive name, or an object map of directives where the - * keys are the names and the values are the factories. - * @param {Function} directiveFactory Factory function for creating new instance of - * directives. - * @description - * See {@link ng.$compileProvider#directive $compileProvider.directive()}. - */ - directive: invokeLaterAndSetModuleName('$compileProvider', 'directive'), - - /** - * @ngdoc method - * @name angular.Module#config - * @module ng - * @param {Function} configFn Execute this function on module load. Useful for service - * configuration. - * @description - * Use this method to register work which needs to be performed on module loading. - * For more about how to configure services, see - * {@link providers#provider-recipe Provider Recipe}. - */ - config: config, - - /** - * @ngdoc method - * @name angular.Module#run - * @module ng - * @param {Function} initializationFn Execute this function after injector creation. - * Useful for application initialization. - * @description - * Use this method to register work which should be performed when the injector is done - * loading all modules. - */ - run: function(block) { - runBlocks.push(block); - return this; - } - }; - - if (configFn) { - config(configFn); - } - - return moduleInstance; - - /** - * @param {string} provider - * @param {string} method - * @param {String=} insertMethod - * @returns {angular.Module} - */ - function invokeLater(provider, method, insertMethod, queue) { - if (!queue) queue = invokeQueue; - return function() { - queue[insertMethod || 'push']([provider, method, arguments]); - return moduleInstance; - }; - } - - /** - * @param {string} provider - * @param {string} method - * @returns {angular.Module} - */ - function invokeLaterAndSetModuleName(provider, method) { - return function(recipeName, factoryFunction) { - if (factoryFunction && isFunction(factoryFunction)) factoryFunction.$$moduleName = name; - invokeQueue.push([provider, method, arguments]); - return moduleInstance; - }; - } - }); - }; - }); - -} - -setupModuleLoader(window); -})(window); - -/** - * Closure compiler type information - * - * @typedef { { - * requires: !Array., - * invokeQueue: !Array.>, - * - * service: function(string, Function):angular.Module, - * factory: function(string, Function):angular.Module, - * value: function(string, *):angular.Module, - * - * filter: function(string, Function):angular.Module, - * - * init: function(Function):angular.Module - * } } - */ -angular.Module; - diff --git a/themes/src/main/resources/theme/keycloak/common/resources/lib/angular/angular-resource.js b/themes/src/main/resources/theme/keycloak/common/resources/lib/angular/angular-resource.js deleted file mode 100644 index 99903d20ed..0000000000 --- a/themes/src/main/resources/theme/keycloak/common/resources/lib/angular/angular-resource.js +++ /dev/null @@ -1,669 +0,0 @@ -/** - * @license AngularJS v1.4.4 - * (c) 2010-2015 Google, Inc. http://angularjs.org - * License: MIT - */ -(function(window, angular, undefined) {'use strict'; - -var $resourceMinErr = angular.$$minErr('$resource'); - -// Helper functions and regex to lookup a dotted path on an object -// stopping at undefined/null. The path must be composed of ASCII -// identifiers (just like $parse) -var MEMBER_NAME_REGEX = /^(\.[a-zA-Z_$@][0-9a-zA-Z_$@]*)+$/; - -function isValidDottedPath(path) { - return (path != null && path !== '' && path !== 'hasOwnProperty' && - MEMBER_NAME_REGEX.test('.' + path)); -} - -function lookupDottedPath(obj, path) { - if (!isValidDottedPath(path)) { - throw $resourceMinErr('badmember', 'Dotted member path "@{0}" is invalid.', path); - } - var keys = path.split('.'); - for (var i = 0, ii = keys.length; i < ii && obj !== undefined; i++) { - var key = keys[i]; - obj = (obj !== null) ? obj[key] : undefined; - } - return obj; -} - -/** - * Create a shallow copy of an object and clear other fields from the destination - */ -function shallowClearAndCopy(src, dst) { - dst = dst || {}; - - angular.forEach(dst, function(value, key) { - delete dst[key]; - }); - - for (var key in src) { - if (src.hasOwnProperty(key) && !(key.charAt(0) === '$' && key.charAt(1) === '$')) { - dst[key] = src[key]; - } - } - - return dst; -} - -/** - * @ngdoc module - * @name ngResource - * @description - * - * # ngResource - * - * The `ngResource` module provides interaction support with RESTful services - * via the $resource service. - * - * - *
    - * - * See {@link ngResource.$resource `$resource`} for usage. - */ - -/** - * @ngdoc service - * @name $resource - * @requires $http - * - * @description - * A factory which creates a resource object that lets you interact with - * [RESTful](http://en.wikipedia.org/wiki/Representational_State_Transfer) server-side data sources. - * - * The returned resource object has action methods which provide high-level behaviors without - * the need to interact with the low level {@link ng.$http $http} service. - * - * Requires the {@link ngResource `ngResource`} module to be installed. - * - * By default, trailing slashes will be stripped from the calculated URLs, - * which can pose problems with server backends that do not expect that - * behavior. This can be disabled by configuring the `$resourceProvider` like - * this: - * - * ```js - app.config(['$resourceProvider', function($resourceProvider) { - // Don't strip trailing slashes from calculated URLs - $resourceProvider.defaults.stripTrailingSlashes = false; - }]); - * ``` - * - * @param {string} url A parameterized URL template with parameters prefixed by `:` as in - * `/user/:username`. If you are using a URL with a port number (e.g. - * `http://example.com:8080/api`), it will be respected. - * - * If you are using a url with a suffix, just add the suffix, like this: - * `$resource('http://example.com/resource.json')` or `$resource('http://example.com/:id.json')` - * or even `$resource('http://example.com/resource/:resource_id.:format')` - * If the parameter before the suffix is empty, :resource_id in this case, then the `/.` will be - * collapsed down to a single `.`. If you need this sequence to appear and not collapse then you - * can escape it with `/\.`. - * - * @param {Object=} paramDefaults Default values for `url` parameters. These can be overridden in - * `actions` methods. If any of the parameter value is a function, it will be executed every time - * when a param value needs to be obtained for a request (unless the param was overridden). - * - * Each key value in the parameter object is first bound to url template if present and then any - * excess keys are appended to the url search query after the `?`. - * - * Given a template `/path/:verb` and parameter `{verb:'greet', salutation:'Hello'}` results in - * URL `/path/greet?salutation=Hello`. - * - * If the parameter value is prefixed with `@` then the value for that parameter will be extracted - * from the corresponding property on the `data` object (provided when calling an action method). For - * example, if the `defaultParam` object is `{someParam: '@someProp'}` then the value of `someParam` - * will be `data.someProp`. - * - * @param {Object.=} actions Hash with declaration of custom actions that should extend - * the default set of resource actions. The declaration should be created in the format of {@link - * ng.$http#usage $http.config}: - * - * {action1: {method:?, params:?, isArray:?, headers:?, ...}, - * action2: {method:?, params:?, isArray:?, headers:?, ...}, - * ...} - * - * Where: - * - * - **`action`** – {string} – The name of action. This name becomes the name of the method on - * your resource object. - * - **`method`** – {string} – Case insensitive HTTP method (e.g. `GET`, `POST`, `PUT`, - * `DELETE`, `JSONP`, etc). - * - **`params`** – {Object=} – Optional set of pre-bound parameters for this action. If any of - * the parameter value is a function, it will be executed every time when a param value needs to - * be obtained for a request (unless the param was overridden). - * - **`url`** – {string} – action specific `url` override. The url templating is supported just - * like for the resource-level urls. - * - **`isArray`** – {boolean=} – If true then the returned object for this action is an array, - * see `returns` section. - * - **`transformRequest`** – - * `{function(data, headersGetter)|Array.}` – - * transform function or an array of such functions. The transform function takes the http - * request body and headers and returns its transformed (typically serialized) version. - * By default, transformRequest will contain one function that checks if the request data is - * an object and serializes to using `angular.toJson`. To prevent this behavior, set - * `transformRequest` to an empty array: `transformRequest: []` - * - **`transformResponse`** – - * `{function(data, headersGetter)|Array.}` – - * transform function or an array of such functions. The transform function takes the http - * response body and headers and returns its transformed (typically deserialized) version. - * By default, transformResponse will contain one function that checks if the response looks like - * a JSON string and deserializes it using `angular.fromJson`. To prevent this behavior, set - * `transformResponse` to an empty array: `transformResponse: []` - * - **`cache`** – `{boolean|Cache}` – If true, a default $http cache will be used to cache the - * GET request, otherwise if a cache instance built with - * {@link ng.$cacheFactory $cacheFactory}, this cache will be used for - * caching. - * - **`timeout`** – `{number|Promise}` – timeout in milliseconds, or {@link ng.$q promise} that - * should abort the request when resolved. - * - **`withCredentials`** - `{boolean}` - whether to set the `withCredentials` flag on the - * XHR object. See - * [requests with credentials](https://developer.mozilla.org/en/http_access_control#section_5) - * for more information. - * - **`responseType`** - `{string}` - see - * [requestType](https://developer.mozilla.org/en-US/docs/DOM/XMLHttpRequest#responseType). - * - **`interceptor`** - `{Object=}` - The interceptor object has two optional methods - - * `response` and `responseError`. Both `response` and `responseError` interceptors get called - * with `http response` object. See {@link ng.$http $http interceptors}. - * - * @param {Object} options Hash with custom settings that should extend the - * default `$resourceProvider` behavior. The only supported option is - * - * Where: - * - * - **`stripTrailingSlashes`** – {boolean} – If true then the trailing - * slashes from any calculated URL will be stripped. (Defaults to true.) - * - * @returns {Object} A resource "class" object with methods for the default set of resource actions - * optionally extended with custom `actions`. The default set contains these actions: - * ```js - * { 'get': {method:'GET'}, - * 'save': {method:'POST'}, - * 'query': {method:'GET', isArray:true}, - * 'remove': {method:'DELETE'}, - * 'delete': {method:'DELETE'} }; - * ``` - * - * Calling these methods invoke an {@link ng.$http} with the specified http method, - * destination and parameters. When the data is returned from the server then the object is an - * instance of the resource class. The actions `save`, `remove` and `delete` are available on it - * as methods with the `$` prefix. This allows you to easily perform CRUD operations (create, - * read, update, delete) on server-side data like this: - * ```js - * var User = $resource('/user/:userId', {userId:'@id'}); - * var user = User.get({userId:123}, function() { - * user.abc = true; - * user.$save(); - * }); - * ``` - * - * It is important to realize that invoking a $resource object method immediately returns an - * empty reference (object or array depending on `isArray`). Once the data is returned from the - * server the existing reference is populated with the actual data. This is a useful trick since - * usually the resource is assigned to a model which is then rendered by the view. Having an empty - * object results in no rendering, once the data arrives from the server then the object is - * populated with the data and the view automatically re-renders itself showing the new data. This - * means that in most cases one never has to write a callback function for the action methods. - * - * The action methods on the class object or instance object can be invoked with the following - * parameters: - * - * - HTTP GET "class" actions: `Resource.action([parameters], [success], [error])` - * - non-GET "class" actions: `Resource.action([parameters], postData, [success], [error])` - * - non-GET instance actions: `instance.$action([parameters], [success], [error])` - * - * - * Success callback is called with (value, responseHeaders) arguments, where the value is - * the populated resource instance or collection object. The error callback is called - * with (httpResponse) argument. - * - * Class actions return empty instance (with additional properties below). - * Instance actions return promise of the action. - * - * The Resource instances and collection have these additional properties: - * - * - `$promise`: the {@link ng.$q promise} of the original server interaction that created this - * instance or collection. - * - * On success, the promise is resolved with the same resource instance or collection object, - * updated with data from server. This makes it easy to use in - * {@link ngRoute.$routeProvider resolve section of $routeProvider.when()} to defer view - * rendering until the resource(s) are loaded. - * - * On failure, the promise is resolved with the {@link ng.$http http response} object, without - * the `resource` property. - * - * If an interceptor object was provided, the promise will instead be resolved with the value - * returned by the interceptor. - * - * - `$resolved`: `true` after first server interaction is completed (either with success or - * rejection), `false` before that. Knowing if the Resource has been resolved is useful in - * data-binding. - * - * @example - * - * # Credit card resource - * - * ```js - // Define CreditCard class - var CreditCard = $resource('/user/:userId/card/:cardId', - {userId:123, cardId:'@id'}, { - charge: {method:'POST', params:{charge:true}} - }); - - // We can retrieve a collection from the server - var cards = CreditCard.query(function() { - // GET: /user/123/card - // server returns: [ {id:456, number:'1234', name:'Smith'} ]; - - var card = cards[0]; - // each item is an instance of CreditCard - expect(card instanceof CreditCard).toEqual(true); - card.name = "J. Smith"; - // non GET methods are mapped onto the instances - card.$save(); - // POST: /user/123/card/456 {id:456, number:'1234', name:'J. Smith'} - // server returns: {id:456, number:'1234', name: 'J. Smith'}; - - // our custom method is mapped as well. - card.$charge({amount:9.99}); - // POST: /user/123/card/456?amount=9.99&charge=true {id:456, number:'1234', name:'J. Smith'} - }); - - // we can create an instance as well - var newCard = new CreditCard({number:'0123'}); - newCard.name = "Mike Smith"; - newCard.$save(); - // POST: /user/123/card {number:'0123', name:'Mike Smith'} - // server returns: {id:789, number:'0123', name: 'Mike Smith'}; - expect(newCard.id).toEqual(789); - * ``` - * - * The object returned from this function execution is a resource "class" which has "static" method - * for each action in the definition. - * - * Calling these methods invoke `$http` on the `url` template with the given `method`, `params` and - * `headers`. - * When the data is returned from the server then the object is an instance of the resource type and - * all of the non-GET methods are available with `$` prefix. This allows you to easily support CRUD - * operations (create, read, update, delete) on server-side data. - - ```js - var User = $resource('/user/:userId', {userId:'@id'}); - User.get({userId:123}, function(user) { - user.abc = true; - user.$save(); - }); - ``` - * - * It's worth noting that the success callback for `get`, `query` and other methods gets passed - * in the response that came from the server as well as $http header getter function, so one - * could rewrite the above example and get access to http headers as: - * - ```js - var User = $resource('/user/:userId', {userId:'@id'}); - User.get({userId:123}, function(u, getResponseHeaders){ - u.abc = true; - u.$save(function(u, putResponseHeaders) { - //u => saved user object - //putResponseHeaders => $http header getter - }); - }); - ``` - * - * You can also access the raw `$http` promise via the `$promise` property on the object returned - * - ``` - var User = $resource('/user/:userId', {userId:'@id'}); - User.get({userId:123}) - .$promise.then(function(user) { - $scope.user = user; - }); - ``` - - * # Creating a custom 'PUT' request - * In this example we create a custom method on our resource to make a PUT request - * ```js - * var app = angular.module('app', ['ngResource', 'ngRoute']); - * - * // Some APIs expect a PUT request in the format URL/object/ID - * // Here we are creating an 'update' method - * app.factory('Notes', ['$resource', function($resource) { - * return $resource('/notes/:id', null, - * { - * 'update': { method:'PUT' } - * }); - * }]); - * - * // In our controller we get the ID from the URL using ngRoute and $routeParams - * // We pass in $routeParams and our Notes factory along with $scope - * app.controller('NotesCtrl', ['$scope', '$routeParams', 'Notes', - function($scope, $routeParams, Notes) { - * // First get a note object from the factory - * var note = Notes.get({ id:$routeParams.id }); - * $id = note.id; - * - * // Now call update passing in the ID first then the object you are updating - * Notes.update({ id:$id }, note); - * - * // This will PUT /notes/ID with the note object in the request payload - * }]); - * ``` - */ -angular.module('ngResource', ['ng']). - provider('$resource', function() { - var provider = this; - - this.defaults = { - // Strip slashes by default - stripTrailingSlashes: true, - - // Default actions configuration - actions: { - 'get': {method: 'GET'}, - 'save': {method: 'POST'}, - 'query': {method: 'GET', isArray: true}, - 'remove': {method: 'DELETE'}, - 'delete': {method: 'DELETE'} - } - }; - - this.$get = ['$http', '$q', function($http, $q) { - - var noop = angular.noop, - forEach = angular.forEach, - extend = angular.extend, - copy = angular.copy, - isFunction = angular.isFunction; - - /** - * We need our custom method because encodeURIComponent is too aggressive and doesn't follow - * http://www.ietf.org/rfc/rfc3986.txt with regards to the character set - * (pchar) allowed in path segments: - * segment = *pchar - * pchar = unreserved / pct-encoded / sub-delims / ":" / "@" - * pct-encoded = "%" HEXDIG HEXDIG - * unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" - * sub-delims = "!" / "$" / "&" / "'" / "(" / ")" - * / "*" / "+" / "," / ";" / "=" - */ - function encodeUriSegment(val) { - return encodeUriQuery(val, true). - replace(/%26/gi, '&'). - replace(/%3D/gi, '='). - replace(/%2B/gi, '+'); - } - - - /** - * This method is intended for encoding *key* or *value* parts of query component. We need a - * custom method because encodeURIComponent is too aggressive and encodes stuff that doesn't - * have to be encoded per http://tools.ietf.org/html/rfc3986: - * query = *( pchar / "/" / "?" ) - * pchar = unreserved / pct-encoded / sub-delims / ":" / "@" - * unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" - * pct-encoded = "%" HEXDIG HEXDIG - * sub-delims = "!" / "$" / "&" / "'" / "(" / ")" - * / "*" / "+" / "," / ";" / "=" - */ - function encodeUriQuery(val, pctEncodeSpaces) { - return encodeURIComponent(val). - replace(/%40/gi, '@'). - replace(/%3A/gi, ':'). - replace(/%24/g, '$'). - replace(/%2C/gi, ','). - replace(/%20/g, (pctEncodeSpaces ? '%20' : '+')); - } - - function Route(template, defaults) { - this.template = template; - this.defaults = extend({}, provider.defaults, defaults); - this.urlParams = {}; - } - - Route.prototype = { - setUrlParams: function(config, params, actionUrl) { - var self = this, - url = actionUrl || self.template, - val, - encodedVal; - - var urlParams = self.urlParams = {}; - forEach(url.split(/\W/), function(param) { - if (param === 'hasOwnProperty') { - throw $resourceMinErr('badname', "hasOwnProperty is not a valid parameter name."); - } - if (!(new RegExp("^\\d+$").test(param)) && param && - (new RegExp("(^|[^\\\\]):" + param + "(\\W|$)").test(url))) { - urlParams[param] = true; - } - }); - url = url.replace(/\\:/g, ':'); - - params = params || {}; - forEach(self.urlParams, function(_, urlParam) { - val = params.hasOwnProperty(urlParam) ? params[urlParam] : self.defaults[urlParam]; - if (angular.isDefined(val) && val !== null) { - encodedVal = encodeUriSegment(val); - url = url.replace(new RegExp(":" + urlParam + "(\\W|$)", "g"), function(match, p1) { - return encodedVal + p1; - }); - } else { - url = url.replace(new RegExp("(\/?):" + urlParam + "(\\W|$)", "g"), function(match, - leadingSlashes, tail) { - if (tail.charAt(0) == '/') { - return tail; - } else { - return leadingSlashes + tail; - } - }); - } - }); - - // strip trailing slashes and set the url (unless this behavior is specifically disabled) - if (self.defaults.stripTrailingSlashes) { - url = url.replace(/\/+$/, '') || '/'; - } - - // then replace collapse `/.` if found in the last URL path segment before the query - // E.g. `http://url.com/id./format?q=x` becomes `http://url.com/id.format?q=x` - url = url.replace(/\/\.(?=\w+($|\?))/, '.'); - // replace escaped `/\.` with `/.` - config.url = url.replace(/\/\\\./, '/.'); - - - // set params - delegate param encoding to $http - forEach(params, function(value, key) { - if (!self.urlParams[key]) { - config.params = config.params || {}; - config.params[key] = value; - } - }); - } - }; - - - function resourceFactory(url, paramDefaults, actions, options) { - var route = new Route(url, options); - - actions = extend({}, provider.defaults.actions, actions); - - function extractParams(data, actionParams) { - var ids = {}; - actionParams = extend({}, paramDefaults, actionParams); - forEach(actionParams, function(value, key) { - if (isFunction(value)) { value = value(); } - ids[key] = value && value.charAt && value.charAt(0) == '@' ? - lookupDottedPath(data, value.substr(1)) : value; - }); - return ids; - } - - function defaultResponseInterceptor(response) { - return response.resource; - } - - function Resource(value) { - shallowClearAndCopy(value || {}, this); - } - - Resource.prototype.toJSON = function() { - var data = extend({}, this); - delete data.$promise; - delete data.$resolved; - return data; - }; - - forEach(actions, function(action, name) { - var hasBody = /^(POST|PUT|PATCH)$/i.test(action.method); - - Resource[name] = function(a1, a2, a3, a4) { - var params = {}, data, success, error; - - /* jshint -W086 */ /* (purposefully fall through case statements) */ - switch (arguments.length) { - case 4: - error = a4; - success = a3; - //fallthrough - case 3: - case 2: - if (isFunction(a2)) { - if (isFunction(a1)) { - success = a1; - error = a2; - break; - } - - success = a2; - error = a3; - //fallthrough - } else { - params = a1; - data = a2; - success = a3; - break; - } - case 1: - if (isFunction(a1)) success = a1; - else if (hasBody) data = a1; - else params = a1; - break; - case 0: break; - default: - throw $resourceMinErr('badargs', - "Expected up to 4 arguments [params, data, success, error], got {0} arguments", - arguments.length); - } - /* jshint +W086 */ /* (purposefully fall through case statements) */ - - var isInstanceCall = this instanceof Resource; - var value = isInstanceCall ? data : (action.isArray ? [] : new Resource(data)); - var httpConfig = {}; - var responseInterceptor = action.interceptor && action.interceptor.response || - defaultResponseInterceptor; - var responseErrorInterceptor = action.interceptor && action.interceptor.responseError || - undefined; - - forEach(action, function(value, key) { - if (key != 'params' && key != 'isArray' && key != 'interceptor') { - httpConfig[key] = copy(value); - } - }); - - if (hasBody) httpConfig.data = data; - route.setUrlParams(httpConfig, - extend({}, extractParams(data, action.params || {}), params), - action.url); - - var promise = $http(httpConfig).then(function(response) { - var data = response.data, - promise = value.$promise; - - if (data) { - // Need to convert action.isArray to boolean in case it is undefined - // jshint -W018 - if (angular.isArray(data) !== (!!action.isArray)) { - throw $resourceMinErr('badcfg', - 'Error in resource configuration for action `{0}`. Expected response to ' + - 'contain an {1} but got an {2} (Request: {3} {4})', name, action.isArray ? 'array' : 'object', - angular.isArray(data) ? 'array' : 'object', httpConfig.method, httpConfig.url); - } - // jshint +W018 - if (action.isArray) { - value.length = 0; - forEach(data, function(item) { - if (typeof item === "object") { - value.push(new Resource(item)); - } else { - // Valid JSON values may be string literals, and these should not be converted - // into objects. These items will not have access to the Resource prototype - // methods, but unfortunately there - value.push(item); - } - }); - } else { - shallowClearAndCopy(data, value); - value.$promise = promise; - } - } - - value.$resolved = true; - - response.resource = value; - - return response; - }, function(response) { - value.$resolved = true; - - (error || noop)(response); - - return $q.reject(response); - }); - - promise = promise.then( - function(response) { - var value = responseInterceptor(response); - (success || noop)(value, response.headers); - return value; - }, - responseErrorInterceptor); - - if (!isInstanceCall) { - // we are creating instance / collection - // - set the initial promise - // - return the instance / collection - value.$promise = promise; - value.$resolved = false; - - return value; - } - - // instance call - return promise; - }; - - - Resource.prototype['$' + name] = function(params, success, error) { - if (isFunction(params)) { - error = success; success = params; params = {}; - } - var result = Resource[name].call(this, params, this, success, error); - return result.$promise || result; - }; - }); - - Resource.bind = function(additionalParamDefaults) { - return resourceFactory(url, extend({}, paramDefaults, additionalParamDefaults), actions); - }; - - return Resource; - } - - return resourceFactory; - }]; - }); - - -})(window, window.angular); diff --git a/themes/src/main/resources/theme/keycloak/common/resources/lib/angular/angular-route.js b/themes/src/main/resources/theme/keycloak/common/resources/lib/angular/angular-route.js deleted file mode 100644 index 353e86f22e..0000000000 --- a/themes/src/main/resources/theme/keycloak/common/resources/lib/angular/angular-route.js +++ /dev/null @@ -1,992 +0,0 @@ -/** - * @license AngularJS v1.4.4 - * (c) 2010-2015 Google, Inc. http://angularjs.org - * License: MIT - */ -(function(window, angular, undefined) {'use strict'; - -/** - * @ngdoc module - * @name ngRoute - * @description - * - * # ngRoute - * - * The `ngRoute` module provides routing and deeplinking services and directives for angular apps. - * - * ## Example - * See {@link ngRoute.$route#example $route} for an example of configuring and using `ngRoute`. - * - * - *
    - */ - /* global -ngRouteModule */ -var ngRouteModule = angular.module('ngRoute', ['ng']). - provider('$route', $RouteProvider), - $routeMinErr = angular.$$minErr('ngRoute'); - -/** - * @ngdoc provider - * @name $routeProvider - * - * @description - * - * Used for configuring routes. - * - * ## Example - * See {@link ngRoute.$route#example $route} for an example of configuring and using `ngRoute`. - * - * ## Dependencies - * Requires the {@link ngRoute `ngRoute`} module to be installed. - */ -function $RouteProvider() { - function inherit(parent, extra) { - return angular.extend(Object.create(parent), extra); - } - - var routes = {}; - - /** - * @ngdoc method - * @name $routeProvider#when - * - * @param {string} path Route path (matched against `$location.path`). If `$location.path` - * contains redundant trailing slash or is missing one, the route will still match and the - * `$location.path` will be updated to add or drop the trailing slash to exactly match the - * route definition. - * - * * `path` can contain named groups starting with a colon: e.g. `:name`. All characters up - * to the next slash are matched and stored in `$routeParams` under the given `name` - * when the route matches. - * * `path` can contain named groups starting with a colon and ending with a star: - * e.g.`:name*`. All characters are eagerly stored in `$routeParams` under the given `name` - * when the route matches. - * * `path` can contain optional named groups with a question mark: e.g.`:name?`. - * - * For example, routes like `/color/:color/largecode/:largecode*\/edit` will match - * `/color/brown/largecode/code/with/slashes/edit` and extract: - * - * * `color: brown` - * * `largecode: code/with/slashes`. - * - * - * @param {Object} route Mapping information to be assigned to `$route.current` on route - * match. - * - * Object properties: - * - * - `controller` – `{(string|function()=}` – Controller fn that should be associated with - * newly created scope or the name of a {@link angular.Module#controller registered - * controller} if passed as a string. - * - `controllerAs` – `{string=}` – An identifier name for a reference to the controller. - * If present, the controller will be published to scope under the `controllerAs` name. - * - `template` – `{string=|function()=}` – html template as a string or a function that - * returns an html template as a string which should be used by {@link - * ngRoute.directive:ngView ngView} or {@link ng.directive:ngInclude ngInclude} directives. - * This property takes precedence over `templateUrl`. - * - * If `template` is a function, it will be called with the following parameters: - * - * - `{Array.}` - route parameters extracted from the current - * `$location.path()` by applying the current route - * - * - `templateUrl` – `{string=|function()=}` – path or function that returns a path to an html - * template that should be used by {@link ngRoute.directive:ngView ngView}. - * - * If `templateUrl` is a function, it will be called with the following parameters: - * - * - `{Array.}` - route parameters extracted from the current - * `$location.path()` by applying the current route - * - * - `resolve` - `{Object.=}` - An optional map of dependencies which should - * be injected into the controller. If any of these dependencies are promises, the router - * will wait for them all to be resolved or one to be rejected before the controller is - * instantiated. - * If all the promises are resolved successfully, the values of the resolved promises are - * injected and {@link ngRoute.$route#$routeChangeSuccess $routeChangeSuccess} event is - * fired. If any of the promises are rejected the - * {@link ngRoute.$route#$routeChangeError $routeChangeError} event is fired. The map object - * is: - * - * - `key` – `{string}`: a name of a dependency to be injected into the controller. - * - `factory` - `{string|function}`: If `string` then it is an alias for a service. - * Otherwise if function, then it is {@link auto.$injector#invoke injected} - * and the return value is treated as the dependency. If the result is a promise, it is - * resolved before its value is injected into the controller. Be aware that - * `ngRoute.$routeParams` will still refer to the previous route within these resolve - * functions. Use `$route.current.params` to access the new route parameters, instead. - * - * - `redirectTo` – {(string|function())=} – value to update - * {@link ng.$location $location} path with and trigger route redirection. - * - * If `redirectTo` is a function, it will be called with the following parameters: - * - * - `{Object.}` - route parameters extracted from the current - * `$location.path()` by applying the current route templateUrl. - * - `{string}` - current `$location.path()` - * - `{Object}` - current `$location.search()` - * - * The custom `redirectTo` function is expected to return a string which will be used - * to update `$location.path()` and `$location.search()`. - * - * - `[reloadOnSearch=true]` - {boolean=} - reload route when only `$location.search()` - * or `$location.hash()` changes. - * - * If the option is set to `false` and url in the browser changes, then - * `$routeUpdate` event is broadcasted on the root scope. - * - * - `[caseInsensitiveMatch=false]` - {boolean=} - match routes without being case sensitive - * - * If the option is set to `true`, then the particular route can be matched without being - * case sensitive - * - * @returns {Object} self - * - * @description - * Adds a new route definition to the `$route` service. - */ - this.when = function(path, route) { - //copy original route object to preserve params inherited from proto chain - var routeCopy = angular.copy(route); - if (angular.isUndefined(routeCopy.reloadOnSearch)) { - routeCopy.reloadOnSearch = true; - } - if (angular.isUndefined(routeCopy.caseInsensitiveMatch)) { - routeCopy.caseInsensitiveMatch = this.caseInsensitiveMatch; - } - routes[path] = angular.extend( - routeCopy, - path && pathRegExp(path, routeCopy) - ); - - // create redirection for trailing slashes - if (path) { - var redirectPath = (path[path.length - 1] == '/') - ? path.substr(0, path.length - 1) - : path + '/'; - - routes[redirectPath] = angular.extend( - {redirectTo: path}, - pathRegExp(redirectPath, routeCopy) - ); - } - - return this; - }; - - /** - * @ngdoc property - * @name $routeProvider#caseInsensitiveMatch - * @description - * - * A boolean property indicating if routes defined - * using this provider should be matched using a case insensitive - * algorithm. Defaults to `false`. - */ - this.caseInsensitiveMatch = false; - - /** - * @param path {string} path - * @param opts {Object} options - * @return {?Object} - * - * @description - * Normalizes the given path, returning a regular expression - * and the original path. - * - * Inspired by pathRexp in visionmedia/express/lib/utils.js. - */ - function pathRegExp(path, opts) { - var insensitive = opts.caseInsensitiveMatch, - ret = { - originalPath: path, - regexp: path - }, - keys = ret.keys = []; - - path = path - .replace(/([().])/g, '\\$1') - .replace(/(\/)?:(\w+)([\?\*])?/g, function(_, slash, key, option) { - var optional = option === '?' ? option : null; - var star = option === '*' ? option : null; - keys.push({ name: key, optional: !!optional }); - slash = slash || ''; - return '' - + (optional ? '' : slash) - + '(?:' - + (optional ? slash : '') - + (star && '(.+?)' || '([^/]+)') - + (optional || '') - + ')' - + (optional || ''); - }) - .replace(/([\/$\*])/g, '\\$1'); - - ret.regexp = new RegExp('^' + path + '$', insensitive ? 'i' : ''); - return ret; - } - - /** - * @ngdoc method - * @name $routeProvider#otherwise - * - * @description - * Sets route definition that will be used on route change when no other route definition - * is matched. - * - * @param {Object|string} params Mapping information to be assigned to `$route.current`. - * If called with a string, the value maps to `redirectTo`. - * @returns {Object} self - */ - this.otherwise = function(params) { - if (typeof params === 'string') { - params = {redirectTo: params}; - } - this.when(null, params); - return this; - }; - - - this.$get = ['$rootScope', - '$location', - '$routeParams', - '$q', - '$injector', - '$templateRequest', - '$sce', - function($rootScope, $location, $routeParams, $q, $injector, $templateRequest, $sce) { - - /** - * @ngdoc service - * @name $route - * @requires $location - * @requires $routeParams - * - * @property {Object} current Reference to the current route definition. - * The route definition contains: - * - * - `controller`: The controller constructor as define in route definition. - * - `locals`: A map of locals which is used by {@link ng.$controller $controller} service for - * controller instantiation. The `locals` contain - * the resolved values of the `resolve` map. Additionally the `locals` also contain: - * - * - `$scope` - The current route scope. - * - `$template` - The current route template HTML. - * - * @property {Object} routes Object with all route configuration Objects as its properties. - * - * @description - * `$route` is used for deep-linking URLs to controllers and views (HTML partials). - * It watches `$location.url()` and tries to map the path to an existing route definition. - * - * Requires the {@link ngRoute `ngRoute`} module to be installed. - * - * You can define routes through {@link ngRoute.$routeProvider $routeProvider}'s API. - * - * The `$route` service is typically used in conjunction with the - * {@link ngRoute.directive:ngView `ngView`} directive and the - * {@link ngRoute.$routeParams `$routeParams`} service. - * - * @example - * This example shows how changing the URL hash causes the `$route` to match a route against the - * URL, and the `ngView` pulls in the partial. - * - * - * - *
    - * Choose: - * Moby | - * Moby: Ch1 | - * Gatsby | - * Gatsby: Ch4 | - * Scarlet Letter
    - * - *
    - * - *
    - * - *
    $location.path() = {{$location.path()}}
    - *
    $route.current.templateUrl = {{$route.current.templateUrl}}
    - *
    $route.current.params = {{$route.current.params}}
    - *
    $route.current.scope.name = {{$route.current.scope.name}}
    - *
    $routeParams = {{$routeParams}}
    - *
    - *
    - * - * - * controller: {{name}}
    - * Book Id: {{params.bookId}}
    - *
    - * - * - * controller: {{name}}
    - * Book Id: {{params.bookId}}
    - * Chapter Id: {{params.chapterId}} - *
    - * - * - * angular.module('ngRouteExample', ['ngRoute']) - * - * .controller('MainController', function($scope, $route, $routeParams, $location) { - * $scope.$route = $route; - * $scope.$location = $location; - * $scope.$routeParams = $routeParams; - * }) - * - * .controller('BookController', function($scope, $routeParams) { - * $scope.name = "BookController"; - * $scope.params = $routeParams; - * }) - * - * .controller('ChapterController', function($scope, $routeParams) { - * $scope.name = "ChapterController"; - * $scope.params = $routeParams; - * }) - * - * .config(function($routeProvider, $locationProvider) { - * $routeProvider - * .when('/Book/:bookId', { - * templateUrl: 'book.html', - * controller: 'BookController', - * resolve: { - * // I will cause a 1 second delay - * delay: function($q, $timeout) { - * var delay = $q.defer(); - * $timeout(delay.resolve, 1000); - * return delay.promise; - * } - * } - * }) - * .when('/Book/:bookId/ch/:chapterId', { - * templateUrl: 'chapter.html', - * controller: 'ChapterController' - * }); - * - * // configure html5 to get links working on jsfiddle - * $locationProvider.html5Mode(true); - * }); - * - * - * - * - * it('should load and compile correct template', function() { - * element(by.linkText('Moby: Ch1')).click(); - * var content = element(by.css('[ng-view]')).getText(); - * expect(content).toMatch(/controller\: ChapterController/); - * expect(content).toMatch(/Book Id\: Moby/); - * expect(content).toMatch(/Chapter Id\: 1/); - * - * element(by.partialLinkText('Scarlet')).click(); - * - * content = element(by.css('[ng-view]')).getText(); - * expect(content).toMatch(/controller\: BookController/); - * expect(content).toMatch(/Book Id\: Scarlet/); - * }); - * - *
    - */ - - /** - * @ngdoc event - * @name $route#$routeChangeStart - * @eventType broadcast on root scope - * @description - * Broadcasted before a route change. At this point the route services starts - * resolving all of the dependencies needed for the route change to occur. - * Typically this involves fetching the view template as well as any dependencies - * defined in `resolve` route property. Once all of the dependencies are resolved - * `$routeChangeSuccess` is fired. - * - * The route change (and the `$location` change that triggered it) can be prevented - * by calling `preventDefault` method of the event. See {@link ng.$rootScope.Scope#$on} - * for more details about event object. - * - * @param {Object} angularEvent Synthetic event object. - * @param {Route} next Future route information. - * @param {Route} current Current route information. - */ - - /** - * @ngdoc event - * @name $route#$routeChangeSuccess - * @eventType broadcast on root scope - * @description - * Broadcasted after a route change has happened successfully. - * The `resolve` dependencies are now available in the `current.locals` property. - * - * {@link ngRoute.directive:ngView ngView} listens for the directive - * to instantiate the controller and render the view. - * - * @param {Object} angularEvent Synthetic event object. - * @param {Route} current Current route information. - * @param {Route|Undefined} previous Previous route information, or undefined if current is - * first route entered. - */ - - /** - * @ngdoc event - * @name $route#$routeChangeError - * @eventType broadcast on root scope - * @description - * Broadcasted if any of the resolve promises are rejected. - * - * @param {Object} angularEvent Synthetic event object - * @param {Route} current Current route information. - * @param {Route} previous Previous route information. - * @param {Route} rejection Rejection of the promise. Usually the error of the failed promise. - */ - - /** - * @ngdoc event - * @name $route#$routeUpdate - * @eventType broadcast on root scope - * @description - * The `reloadOnSearch` property has been set to false, and we are reusing the same - * instance of the Controller. - * - * @param {Object} angularEvent Synthetic event object - * @param {Route} current Current/previous route information. - */ - - var forceReload = false, - preparedRoute, - preparedRouteIsUpdateOnly, - $route = { - routes: routes, - - /** - * @ngdoc method - * @name $route#reload - * - * @description - * Causes `$route` service to reload the current route even if - * {@link ng.$location $location} hasn't changed. - * - * As a result of that, {@link ngRoute.directive:ngView ngView} - * creates new scope and reinstantiates the controller. - */ - reload: function() { - forceReload = true; - $rootScope.$evalAsync(function() { - // Don't support cancellation of a reload for now... - prepareRoute(); - commitRoute(); - }); - }, - - /** - * @ngdoc method - * @name $route#updateParams - * - * @description - * Causes `$route` service to update the current URL, replacing - * current route parameters with those specified in `newParams`. - * Provided property names that match the route's path segment - * definitions will be interpolated into the location's path, while - * remaining properties will be treated as query params. - * - * @param {!Object} newParams mapping of URL parameter names to values - */ - updateParams: function(newParams) { - if (this.current && this.current.$$route) { - newParams = angular.extend({}, this.current.params, newParams); - $location.path(interpolate(this.current.$$route.originalPath, newParams)); - // interpolate modifies newParams, only query params are left - $location.search(newParams); - } else { - throw $routeMinErr('norout', 'Tried updating route when with no current route'); - } - } - }; - - $rootScope.$on('$locationChangeStart', prepareRoute); - $rootScope.$on('$locationChangeSuccess', commitRoute); - - return $route; - - ///////////////////////////////////////////////////// - - /** - * @param on {string} current url - * @param route {Object} route regexp to match the url against - * @return {?Object} - * - * @description - * Check if the route matches the current url. - * - * Inspired by match in - * visionmedia/express/lib/router/router.js. - */ - function switchRouteMatcher(on, route) { - var keys = route.keys, - params = {}; - - if (!route.regexp) return null; - - var m = route.regexp.exec(on); - if (!m) return null; - - for (var i = 1, len = m.length; i < len; ++i) { - var key = keys[i - 1]; - - var val = m[i]; - - if (key && val) { - params[key.name] = val; - } - } - return params; - } - - function prepareRoute($locationEvent) { - var lastRoute = $route.current; - - preparedRoute = parseRoute(); - preparedRouteIsUpdateOnly = preparedRoute && lastRoute && preparedRoute.$$route === lastRoute.$$route - && angular.equals(preparedRoute.pathParams, lastRoute.pathParams) - && !preparedRoute.reloadOnSearch && !forceReload; - - if (!preparedRouteIsUpdateOnly && (lastRoute || preparedRoute)) { - if ($rootScope.$broadcast('$routeChangeStart', preparedRoute, lastRoute).defaultPrevented) { - if ($locationEvent) { - $locationEvent.preventDefault(); - } - } - } - } - - function commitRoute() { - var lastRoute = $route.current; - var nextRoute = preparedRoute; - - if (preparedRouteIsUpdateOnly) { - lastRoute.params = nextRoute.params; - angular.copy(lastRoute.params, $routeParams); - $rootScope.$broadcast('$routeUpdate', lastRoute); - } else if (nextRoute || lastRoute) { - forceReload = false; - $route.current = nextRoute; - if (nextRoute) { - if (nextRoute.redirectTo) { - if (angular.isString(nextRoute.redirectTo)) { - $location.path(interpolate(nextRoute.redirectTo, nextRoute.params)).search(nextRoute.params) - .replace(); - } else { - $location.url(nextRoute.redirectTo(nextRoute.pathParams, $location.path(), $location.search())) - .replace(); - } - } - } - - $q.when(nextRoute). - then(function() { - if (nextRoute) { - var locals = angular.extend({}, nextRoute.resolve), - template, templateUrl; - - angular.forEach(locals, function(value, key) { - locals[key] = angular.isString(value) ? - $injector.get(value) : $injector.invoke(value, null, null, key); - }); - - if (angular.isDefined(template = nextRoute.template)) { - if (angular.isFunction(template)) { - template = template(nextRoute.params); - } - } else if (angular.isDefined(templateUrl = nextRoute.templateUrl)) { - if (angular.isFunction(templateUrl)) { - templateUrl = templateUrl(nextRoute.params); - } - if (angular.isDefined(templateUrl)) { - nextRoute.loadedTemplateUrl = $sce.valueOf(templateUrl); - template = $templateRequest(templateUrl); - } - } - if (angular.isDefined(template)) { - locals['$template'] = template; - } - return $q.all(locals); - } - }). - then(function(locals) { - // after route change - if (nextRoute == $route.current) { - if (nextRoute) { - nextRoute.locals = locals; - angular.copy(nextRoute.params, $routeParams); - } - $rootScope.$broadcast('$routeChangeSuccess', nextRoute, lastRoute); - } - }, function(error) { - if (nextRoute == $route.current) { - $rootScope.$broadcast('$routeChangeError', nextRoute, lastRoute, error); - } - }); - } - } - - - /** - * @returns {Object} the current active route, by matching it against the URL - */ - function parseRoute() { - // Match a route - var params, match; - angular.forEach(routes, function(route, path) { - if (!match && (params = switchRouteMatcher($location.path(), route))) { - match = inherit(route, { - params: angular.extend({}, $location.search(), params), - pathParams: params}); - match.$$route = route; - } - }); - // No route matched; fallback to "otherwise" route - return match || routes[null] && inherit(routes[null], {params: {}, pathParams:{}}); - } - - /** - * @returns {string} interpolation of the redirect path with the parameters - */ - function interpolate(string, params) { - var result = []; - angular.forEach((string || '').split(':'), function(segment, i) { - if (i === 0) { - result.push(segment); - } else { - var segmentMatch = segment.match(/(\w+)(?:[?*])?(.*)/); - var key = segmentMatch[1]; - result.push(params[key]); - result.push(segmentMatch[2] || ''); - delete params[key]; - } - }); - return result.join(''); - } - }]; -} - -ngRouteModule.provider('$routeParams', $RouteParamsProvider); - - -/** - * @ngdoc service - * @name $routeParams - * @requires $route - * - * @description - * The `$routeParams` service allows you to retrieve the current set of route parameters. - * - * Requires the {@link ngRoute `ngRoute`} module to be installed. - * - * The route parameters are a combination of {@link ng.$location `$location`}'s - * {@link ng.$location#search `search()`} and {@link ng.$location#path `path()`}. - * The `path` parameters are extracted when the {@link ngRoute.$route `$route`} path is matched. - * - * In case of parameter name collision, `path` params take precedence over `search` params. - * - * The service guarantees that the identity of the `$routeParams` object will remain unchanged - * (but its properties will likely change) even when a route change occurs. - * - * Note that the `$routeParams` are only updated *after* a route change completes successfully. - * This means that you cannot rely on `$routeParams` being correct in route resolve functions. - * Instead you can use `$route.current.params` to access the new route's parameters. - * - * @example - * ```js - * // Given: - * // URL: http://server.com/index.html#/Chapter/1/Section/2?search=moby - * // Route: /Chapter/:chapterId/Section/:sectionId - * // - * // Then - * $routeParams ==> {chapterId:'1', sectionId:'2', search:'moby'} - * ``` - */ -function $RouteParamsProvider() { - this.$get = function() { return {}; }; -} - -ngRouteModule.directive('ngView', ngViewFactory); -ngRouteModule.directive('ngView', ngViewFillContentFactory); - - -/** - * @ngdoc directive - * @name ngView - * @restrict ECA - * - * @description - * # Overview - * `ngView` is a directive that complements the {@link ngRoute.$route $route} service by - * including the rendered template of the current route into the main layout (`index.html`) file. - * Every time the current route changes, the included view changes with it according to the - * configuration of the `$route` service. - * - * Requires the {@link ngRoute `ngRoute`} module to be installed. - * - * @animations - * enter - animation is used to bring new content into the browser. - * leave - animation is used to animate existing content away. - * - * The enter and leave animation occur concurrently. - * - * @scope - * @priority 400 - * @param {string=} onload Expression to evaluate whenever the view updates. - * - * @param {string=} autoscroll Whether `ngView` should call {@link ng.$anchorScroll - * $anchorScroll} to scroll the viewport after the view is updated. - * - * - If the attribute is not set, disable scrolling. - * - If the attribute is set without value, enable scrolling. - * - Otherwise enable scrolling only if the `autoscroll` attribute value evaluated - * as an expression yields a truthy value. - * @example - - -
    - Choose: - Moby | - Moby: Ch1 | - Gatsby | - Gatsby: Ch4 | - Scarlet Letter
    - -
    -
    -
    -
    - -
    $location.path() = {{main.$location.path()}}
    -
    $route.current.templateUrl = {{main.$route.current.templateUrl}}
    -
    $route.current.params = {{main.$route.current.params}}
    -
    $routeParams = {{main.$routeParams}}
    -
    -
    - - -
    - controller: {{book.name}}
    - Book Id: {{book.params.bookId}}
    -
    -
    - - -
    - controller: {{chapter.name}}
    - Book Id: {{chapter.params.bookId}}
    - Chapter Id: {{chapter.params.chapterId}} -
    -
    - - - .view-animate-container { - position:relative; - height:100px!important; - background:white; - border:1px solid black; - height:40px; - overflow:hidden; - } - - .view-animate { - padding:10px; - } - - .view-animate.ng-enter, .view-animate.ng-leave { - -webkit-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 1.5s; - transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 1.5s; - - display:block; - width:100%; - border-left:1px solid black; - - position:absolute; - top:0; - left:0; - right:0; - bottom:0; - padding:10px; - } - - .view-animate.ng-enter { - left:100%; - } - .view-animate.ng-enter.ng-enter-active { - left:0; - } - .view-animate.ng-leave.ng-leave-active { - left:-100%; - } - - - - angular.module('ngViewExample', ['ngRoute', 'ngAnimate']) - .config(['$routeProvider', '$locationProvider', - function($routeProvider, $locationProvider) { - $routeProvider - .when('/Book/:bookId', { - templateUrl: 'book.html', - controller: 'BookCtrl', - controllerAs: 'book' - }) - .when('/Book/:bookId/ch/:chapterId', { - templateUrl: 'chapter.html', - controller: 'ChapterCtrl', - controllerAs: 'chapter' - }); - - $locationProvider.html5Mode(true); - }]) - .controller('MainCtrl', ['$route', '$routeParams', '$location', - function($route, $routeParams, $location) { - this.$route = $route; - this.$location = $location; - this.$routeParams = $routeParams; - }]) - .controller('BookCtrl', ['$routeParams', function($routeParams) { - this.name = "BookCtrl"; - this.params = $routeParams; - }]) - .controller('ChapterCtrl', ['$routeParams', function($routeParams) { - this.name = "ChapterCtrl"; - this.params = $routeParams; - }]); - - - - - it('should load and compile correct template', function() { - element(by.linkText('Moby: Ch1')).click(); - var content = element(by.css('[ng-view]')).getText(); - expect(content).toMatch(/controller\: ChapterCtrl/); - expect(content).toMatch(/Book Id\: Moby/); - expect(content).toMatch(/Chapter Id\: 1/); - - element(by.partialLinkText('Scarlet')).click(); - - content = element(by.css('[ng-view]')).getText(); - expect(content).toMatch(/controller\: BookCtrl/); - expect(content).toMatch(/Book Id\: Scarlet/); - }); - -
    - */ - - -/** - * @ngdoc event - * @name ngView#$viewContentLoaded - * @eventType emit on the current ngView scope - * @description - * Emitted every time the ngView content is reloaded. - */ -ngViewFactory.$inject = ['$route', '$anchorScroll', '$animate']; -function ngViewFactory($route, $anchorScroll, $animate) { - return { - restrict: 'ECA', - terminal: true, - priority: 400, - transclude: 'element', - link: function(scope, $element, attr, ctrl, $transclude) { - var currentScope, - currentElement, - previousLeaveAnimation, - autoScrollExp = attr.autoscroll, - onloadExp = attr.onload || ''; - - scope.$on('$routeChangeSuccess', update); - update(); - - function cleanupLastView() { - if (previousLeaveAnimation) { - $animate.cancel(previousLeaveAnimation); - previousLeaveAnimation = null; - } - - if (currentScope) { - currentScope.$destroy(); - currentScope = null; - } - if (currentElement) { - previousLeaveAnimation = $animate.leave(currentElement); - previousLeaveAnimation.then(function() { - previousLeaveAnimation = null; - }); - currentElement = null; - } - } - - function update() { - var locals = $route.current && $route.current.locals, - template = locals && locals.$template; - - if (angular.isDefined(template)) { - var newScope = scope.$new(); - var current = $route.current; - - // Note: This will also link all children of ng-view that were contained in the original - // html. If that content contains controllers, ... they could pollute/change the scope. - // However, using ng-view on an element with additional content does not make sense... - // Note: We can't remove them in the cloneAttchFn of $transclude as that - // function is called before linking the content, which would apply child - // directives to non existing elements. - var clone = $transclude(newScope, function(clone) { - $animate.enter(clone, null, currentElement || $element).then(function onNgViewEnter() { - if (angular.isDefined(autoScrollExp) - && (!autoScrollExp || scope.$eval(autoScrollExp))) { - $anchorScroll(); - } - }); - cleanupLastView(); - }); - - currentElement = clone; - currentScope = current.scope = newScope; - currentScope.$emit('$viewContentLoaded'); - currentScope.$eval(onloadExp); - } else { - cleanupLastView(); - } - } - } - }; -} - -// This directive is called during the $transclude call of the first `ngView` directive. -// It will replace and compile the content of the element with the loaded template. -// We need this directive so that the element content is already filled when -// the link function of another directive on the same element as ngView -// is called. -ngViewFillContentFactory.$inject = ['$compile', '$controller', '$route']; -function ngViewFillContentFactory($compile, $controller, $route) { - return { - restrict: 'ECA', - priority: -400, - link: function(scope, $element) { - var current = $route.current, - locals = current.locals; - - $element.html(locals.$template); - - var link = $compile($element.contents()); - - if (current.controller) { - locals.$scope = scope; - var controller = $controller(current.controller, locals); - if (current.controllerAs) { - scope[current.controllerAs] = controller; - } - $element.data('$ngControllerController', controller); - $element.children().data('$ngControllerController', controller); - } - - link(scope); - } - }; -} - - -})(window, window.angular); diff --git a/themes/src/main/resources/theme/keycloak/common/resources/lib/angular/angular-sanitize.js b/themes/src/main/resources/theme/keycloak/common/resources/lib/angular/angular-sanitize.js deleted file mode 100644 index 06d558ccf9..0000000000 --- a/themes/src/main/resources/theme/keycloak/common/resources/lib/angular/angular-sanitize.js +++ /dev/null @@ -1,683 +0,0 @@ -/** - * @license AngularJS v1.4.4 - * (c) 2010-2015 Google, Inc. http://angularjs.org - * License: MIT - */ -(function(window, angular, undefined) {'use strict'; - -/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * - * Any commits to this file should be reviewed with security in mind. * - * Changes to this file can potentially create security vulnerabilities. * - * An approval from 2 Core members with history of modifying * - * this file is required. * - * * - * Does the change somehow allow for arbitrary javascript to be executed? * - * Or allows for someone to change the prototype of built-in objects? * - * Or gives undesired access to variables likes document or window? * - * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ - -var $sanitizeMinErr = angular.$$minErr('$sanitize'); - -/** - * @ngdoc module - * @name ngSanitize - * @description - * - * # ngSanitize - * - * The `ngSanitize` module provides functionality to sanitize HTML. - * - * - *
    - * - * See {@link ngSanitize.$sanitize `$sanitize`} for usage. - */ - -/* - * HTML Parser By Misko Hevery (misko@hevery.com) - * based on: HTML Parser By John Resig (ejohn.org) - * Original code by Erik Arvidsson, Mozilla Public License - * http://erik.eae.net/simplehtmlparser/simplehtmlparser.js - * - * // Use like so: - * htmlParser(htmlString, { - * start: function(tag, attrs, unary) {}, - * end: function(tag) {}, - * chars: function(text) {}, - * comment: function(text) {} - * }); - * - */ - - -/** - * @ngdoc service - * @name $sanitize - * @kind function - * - * @description - * The input is sanitized by parsing the HTML into tokens. All safe tokens (from a whitelist) are - * then serialized back to properly escaped html string. This means that no unsafe input can make - * it into the returned string, however, since our parser is more strict than a typical browser - * parser, it's possible that some obscure input, which would be recognized as valid HTML by a - * browser, won't make it through the sanitizer. The input may also contain SVG markup. - * The whitelist is configured using the functions `aHrefSanitizationWhitelist` and - * `imgSrcSanitizationWhitelist` of {@link ng.$compileProvider `$compileProvider`}. - * - * @param {string} html HTML input. - * @returns {string} Sanitized HTML. - * - * @example - - - -
    - Snippet: - - - - - - - - - - - - - - - - - - - - - - - - - -
    DirectiveHowSourceRendered
    ng-bind-htmlAutomatically uses $sanitize
    <div ng-bind-html="snippet">
    </div>
    ng-bind-htmlBypass $sanitize by explicitly trusting the dangerous value -
    <div ng-bind-html="deliberatelyTrustDangerousSnippet()">
    -</div>
    -
    ng-bindAutomatically escapes
    <div ng-bind="snippet">
    </div>
    -
    -
    - - it('should sanitize the html snippet by default', function() { - expect(element(by.css('#bind-html-with-sanitize div')).getInnerHtml()). - toBe('

    an html\nclick here\nsnippet

    '); - }); - - it('should inline raw snippet if bound to a trusted value', function() { - expect(element(by.css('#bind-html-with-trust div')).getInnerHtml()). - toBe("

    an html\n" + - "click here\n" + - "snippet

    "); - }); - - it('should escape snippet without any filter', function() { - expect(element(by.css('#bind-default div')).getInnerHtml()). - toBe("<p style=\"color:blue\">an html\n" + - "<em onmouseover=\"this.textContent='PWN3D!'\">click here</em>\n" + - "snippet</p>"); - }); - - it('should update', function() { - element(by.model('snippet')).clear(); - element(by.model('snippet')).sendKeys('new text'); - expect(element(by.css('#bind-html-with-sanitize div')).getInnerHtml()). - toBe('new text'); - expect(element(by.css('#bind-html-with-trust div')).getInnerHtml()).toBe( - 'new text'); - expect(element(by.css('#bind-default div')).getInnerHtml()).toBe( - "new <b onclick=\"alert(1)\">text</b>"); - }); -
    -
    - */ -function $SanitizeProvider() { - this.$get = ['$$sanitizeUri', function($$sanitizeUri) { - return function(html) { - var buf = []; - htmlParser(html, htmlSanitizeWriter(buf, function(uri, isImage) { - return !/^unsafe/.test($$sanitizeUri(uri, isImage)); - })); - return buf.join(''); - }; - }]; -} - -function sanitizeText(chars) { - var buf = []; - var writer = htmlSanitizeWriter(buf, angular.noop); - writer.chars(chars); - return buf.join(''); -} - - -// Regular Expressions for parsing tags and attributes -var START_TAG_REGEXP = - /^<((?:[a-zA-Z])[\w:-]*)((?:\s+[\w:-]+(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)\s*(>?)/, - END_TAG_REGEXP = /^<\/\s*([\w:-]+)[^>]*>/, - ATTR_REGEXP = /([\w:-]+)(?:\s*=\s*(?:(?:"((?:[^"])*)")|(?:'((?:[^'])*)')|([^>\s]+)))?/g, - BEGIN_TAG_REGEXP = /^/g, - DOCTYPE_REGEXP = /]*?)>/i, - CDATA_REGEXP = //g, - SURROGATE_PAIR_REGEXP = /[\uD800-\uDBFF][\uDC00-\uDFFF]/g, - // Match everything outside of normal chars and " (quote character) - NON_ALPHANUMERIC_REGEXP = /([^\#-~| |!])/g; - - -// Good source of info about elements and attributes -// http://dev.w3.org/html5/spec/Overview.html#semantics -// http://simon.html5.org/html-elements - -// Safe Void Elements - HTML5 -// http://dev.w3.org/html5/spec/Overview.html#void-elements -var voidElements = makeMap("area,br,col,hr,img,wbr"); - -// Elements that you can, intentionally, leave open (and which close themselves) -// http://dev.w3.org/html5/spec/Overview.html#optional-tags -var optionalEndTagBlockElements = makeMap("colgroup,dd,dt,li,p,tbody,td,tfoot,th,thead,tr"), - optionalEndTagInlineElements = makeMap("rp,rt"), - optionalEndTagElements = angular.extend({}, - optionalEndTagInlineElements, - optionalEndTagBlockElements); - -// Safe Block Elements - HTML5 -var blockElements = angular.extend({}, optionalEndTagBlockElements, makeMap("address,article," + - "aside,blockquote,caption,center,del,dir,div,dl,figure,figcaption,footer,h1,h2,h3,h4,h5," + - "h6,header,hgroup,hr,ins,map,menu,nav,ol,pre,script,section,table,ul")); - -// Inline Elements - HTML5 -var inlineElements = angular.extend({}, optionalEndTagInlineElements, makeMap("a,abbr,acronym,b," + - "bdi,bdo,big,br,cite,code,del,dfn,em,font,i,img,ins,kbd,label,map,mark,q,ruby,rp,rt,s," + - "samp,small,span,strike,strong,sub,sup,time,tt,u,var")); - -// SVG Elements -// https://wiki.whatwg.org/wiki/Sanitization_rules#svg_Elements -// Note: the elements animate,animateColor,animateMotion,animateTransform,set are intentionally omitted. -// They can potentially allow for arbitrary javascript to be executed. See #11290 -var svgElements = makeMap("circle,defs,desc,ellipse,font-face,font-face-name,font-face-src,g,glyph," + - "hkern,image,linearGradient,line,marker,metadata,missing-glyph,mpath,path,polygon,polyline," + - "radialGradient,rect,stop,svg,switch,text,title,tspan,use"); - -// Special Elements (can contain anything) -var specialElements = makeMap("script,style"); - -var validElements = angular.extend({}, - voidElements, - blockElements, - inlineElements, - optionalEndTagElements, - svgElements); - -//Attributes that have href and hence need to be sanitized -var uriAttrs = makeMap("background,cite,href,longdesc,src,usemap,xlink:href"); - -var htmlAttrs = makeMap('abbr,align,alt,axis,bgcolor,border,cellpadding,cellspacing,class,clear,' + - 'color,cols,colspan,compact,coords,dir,face,headers,height,hreflang,hspace,' + - 'ismap,lang,language,nohref,nowrap,rel,rev,rows,rowspan,rules,' + - 'scope,scrolling,shape,size,span,start,summary,tabindex,target,title,type,' + - 'valign,value,vspace,width'); - -// SVG attributes (without "id" and "name" attributes) -// https://wiki.whatwg.org/wiki/Sanitization_rules#svg_Attributes -var svgAttrs = makeMap('accent-height,accumulate,additive,alphabetic,arabic-form,ascent,' + - 'baseProfile,bbox,begin,by,calcMode,cap-height,class,color,color-rendering,content,' + - 'cx,cy,d,dx,dy,descent,display,dur,end,fill,fill-rule,font-family,font-size,font-stretch,' + - 'font-style,font-variant,font-weight,from,fx,fy,g1,g2,glyph-name,gradientUnits,hanging,' + - 'height,horiz-adv-x,horiz-origin-x,ideographic,k,keyPoints,keySplines,keyTimes,lang,' + - 'marker-end,marker-mid,marker-start,markerHeight,markerUnits,markerWidth,mathematical,' + - 'max,min,offset,opacity,orient,origin,overline-position,overline-thickness,panose-1,' + - 'path,pathLength,points,preserveAspectRatio,r,refX,refY,repeatCount,repeatDur,' + - 'requiredExtensions,requiredFeatures,restart,rotate,rx,ry,slope,stemh,stemv,stop-color,' + - 'stop-opacity,strikethrough-position,strikethrough-thickness,stroke,stroke-dasharray,' + - 'stroke-dashoffset,stroke-linecap,stroke-linejoin,stroke-miterlimit,stroke-opacity,' + - 'stroke-width,systemLanguage,target,text-anchor,to,transform,type,u1,u2,underline-position,' + - 'underline-thickness,unicode,unicode-range,units-per-em,values,version,viewBox,visibility,' + - 'width,widths,x,x-height,x1,x2,xlink:actuate,xlink:arcrole,xlink:role,xlink:show,xlink:title,' + - 'xlink:type,xml:base,xml:lang,xml:space,xmlns,xmlns:xlink,y,y1,y2,zoomAndPan', true); - -var validAttrs = angular.extend({}, - uriAttrs, - svgAttrs, - htmlAttrs); - -function makeMap(str, lowercaseKeys) { - var obj = {}, items = str.split(','), i; - for (i = 0; i < items.length; i++) { - obj[lowercaseKeys ? angular.lowercase(items[i]) : items[i]] = true; - } - return obj; -} - - -/** - * @example - * htmlParser(htmlString, { - * start: function(tag, attrs, unary) {}, - * end: function(tag) {}, - * chars: function(text) {}, - * comment: function(text) {} - * }); - * - * @param {string} html string - * @param {object} handler - */ -function htmlParser(html, handler) { - if (typeof html !== 'string') { - if (html === null || typeof html === 'undefined') { - html = ''; - } else { - html = '' + html; - } - } - var index, chars, match, stack = [], last = html, text; - stack.last = function() { return stack[stack.length - 1]; }; - - while (html) { - text = ''; - chars = true; - - // Make sure we're not in a script or style element - if (!stack.last() || !specialElements[stack.last()]) { - - // Comment - if (html.indexOf("", index) === index) { - if (handler.comment) handler.comment(html.substring(4, index)); - html = html.substring(index + 3); - chars = false; - } - // DOCTYPE - } else if (DOCTYPE_REGEXP.test(html)) { - match = html.match(DOCTYPE_REGEXP); - - if (match) { - html = html.replace(match[0], ''); - chars = false; - } - // end tag - } else if (BEGING_END_TAGE_REGEXP.test(html)) { - match = html.match(END_TAG_REGEXP); - - if (match) { - html = html.substring(match[0].length); - match[0].replace(END_TAG_REGEXP, parseEndTag); - chars = false; - } - - // start tag - } else if (BEGIN_TAG_REGEXP.test(html)) { - match = html.match(START_TAG_REGEXP); - - if (match) { - // We only have a valid start-tag if there is a '>'. - if (match[4]) { - html = html.substring(match[0].length); - match[0].replace(START_TAG_REGEXP, parseStartTag); - } - chars = false; - } else { - // no ending tag found --- this piece should be encoded as an entity. - text += '<'; - html = html.substring(1); - } - } - - if (chars) { - index = html.indexOf("<"); - - text += index < 0 ? html : html.substring(0, index); - html = index < 0 ? "" : html.substring(index); - - if (handler.chars) handler.chars(decodeEntities(text)); - } - - } else { - // IE versions 9 and 10 do not understand the regex '[^]', so using a workaround with [\W\w]. - html = html.replace(new RegExp("([\\W\\w]*)<\\s*\\/\\s*" + stack.last() + "[^>]*>", 'i'), - function(all, text) { - text = text.replace(COMMENT_REGEXP, "$1").replace(CDATA_REGEXP, "$1"); - - if (handler.chars) handler.chars(decodeEntities(text)); - - return ""; - }); - - parseEndTag("", stack.last()); - } - - if (html == last) { - throw $sanitizeMinErr('badparse', "The sanitizer was unable to parse the following block " + - "of html: {0}", html); - } - last = html; - } - - // Clean up any remaining tags - parseEndTag(); - - function parseStartTag(tag, tagName, rest, unary) { - tagName = angular.lowercase(tagName); - if (blockElements[tagName]) { - while (stack.last() && inlineElements[stack.last()]) { - parseEndTag("", stack.last()); - } - } - - if (optionalEndTagElements[tagName] && stack.last() == tagName) { - parseEndTag("", tagName); - } - - unary = voidElements[tagName] || !!unary; - - if (!unary) { - stack.push(tagName); - } - - var attrs = {}; - - rest.replace(ATTR_REGEXP, - function(match, name, doubleQuotedValue, singleQuotedValue, unquotedValue) { - var value = doubleQuotedValue - || singleQuotedValue - || unquotedValue - || ''; - - attrs[name] = decodeEntities(value); - }); - if (handler.start) handler.start(tagName, attrs, unary); - } - - function parseEndTag(tag, tagName) { - var pos = 0, i; - tagName = angular.lowercase(tagName); - if (tagName) { - // Find the closest opened tag of the same type - for (pos = stack.length - 1; pos >= 0; pos--) { - if (stack[pos] == tagName) break; - } - } - - if (pos >= 0) { - // Close all the open elements, up the stack - for (i = stack.length - 1; i >= pos; i--) - if (handler.end) handler.end(stack[i]); - - // Remove the open elements from the stack - stack.length = pos; - } - } -} - -var hiddenPre=document.createElement("pre"); -/** - * decodes all entities into regular string - * @param value - * @returns {string} A string with decoded entities. - */ -function decodeEntities(value) { - if (!value) { return ''; } - - hiddenPre.innerHTML = value.replace(//g, '>'); -} - -/** - * create an HTML/XML writer which writes to buffer - * @param {Array} buf use buf.jain('') to get out sanitized html string - * @returns {object} in the form of { - * start: function(tag, attrs, unary) {}, - * end: function(tag) {}, - * chars: function(text) {}, - * comment: function(text) {} - * } - */ -function htmlSanitizeWriter(buf, uriValidator) { - var ignore = false; - var out = angular.bind(buf, buf.push); - return { - start: function(tag, attrs, unary) { - tag = angular.lowercase(tag); - if (!ignore && specialElements[tag]) { - ignore = tag; - } - if (!ignore && validElements[tag] === true) { - out('<'); - out(tag); - angular.forEach(attrs, function(value, key) { - var lkey=angular.lowercase(key); - var isImage = (tag === 'img' && lkey === 'src') || (lkey === 'background'); - if (validAttrs[lkey] === true && - (uriAttrs[lkey] !== true || uriValidator(value, isImage))) { - out(' '); - out(key); - out('="'); - out(encodeEntities(value)); - out('"'); - } - }); - out(unary ? '/>' : '>'); - } - }, - end: function(tag) { - tag = angular.lowercase(tag); - if (!ignore && validElements[tag] === true) { - out(''); - } - if (tag == ignore) { - ignore = false; - } - }, - chars: function(chars) { - if (!ignore) { - out(encodeEntities(chars)); - } - } - }; -} - - -// define ngSanitize module and register $sanitize service -angular.module('ngSanitize', []).provider('$sanitize', $SanitizeProvider); - -/* global sanitizeText: false */ - -/** - * @ngdoc filter - * @name linky - * @kind function - * - * @description - * Finds links in text input and turns them into html links. Supports http/https/ftp/mailto and - * plain email address links. - * - * Requires the {@link ngSanitize `ngSanitize`} module to be installed. - * - * @param {string} text Input text. - * @param {string} target Window (_blank|_self|_parent|_top) or named frame to open links in. - * @returns {string} Html-linkified text. - * - * @usage - - * - * @example - - - -
    - Snippet: - - - - - - - - - - - - - - - - - - - - - -
    FilterSourceRendered
    linky filter -
    <div ng-bind-html="snippet | linky">
    </div>
    -
    -
    -
    linky target -
    <div ng-bind-html="snippetWithTarget | linky:'_blank'">
    </div>
    -
    -
    -
    no filter
    <div ng-bind="snippet">
    </div>
    - - - it('should linkify the snippet with urls', function() { - expect(element(by.id('linky-filter')).element(by.binding('snippet | linky')).getText()). - toBe('Pretty text with some links: http://angularjs.org/, us@somewhere.org, ' + - 'another@somewhere.org, and one more: ftp://127.0.0.1/.'); - expect(element.all(by.css('#linky-filter a')).count()).toEqual(4); - }); - - it('should not linkify snippet without the linky filter', function() { - expect(element(by.id('escaped-html')).element(by.binding('snippet')).getText()). - toBe('Pretty text with some links: http://angularjs.org/, mailto:us@somewhere.org, ' + - 'another@somewhere.org, and one more: ftp://127.0.0.1/.'); - expect(element.all(by.css('#escaped-html a')).count()).toEqual(0); - }); - - it('should update', function() { - element(by.model('snippet')).clear(); - element(by.model('snippet')).sendKeys('new http://link.'); - expect(element(by.id('linky-filter')).element(by.binding('snippet | linky')).getText()). - toBe('new http://link.'); - expect(element.all(by.css('#linky-filter a')).count()).toEqual(1); - expect(element(by.id('escaped-html')).element(by.binding('snippet')).getText()) - .toBe('new http://link.'); - }); - - it('should work with the target property', function() { - expect(element(by.id('linky-target')). - element(by.binding("snippetWithTarget | linky:'_blank'")).getText()). - toBe('http://angularjs.org/'); - expect(element(by.css('#linky-target a')).getAttribute('target')).toEqual('_blank'); - }); - - - */ -angular.module('ngSanitize').filter('linky', ['$sanitize', function($sanitize) { - var LINKY_URL_REGEXP = - /((ftp|https?):\/\/|(www\.)|(mailto:)?[A-Za-z0-9._%+-]+@)\S*[^\s.;,(){}<>"\u201d\u2019]/i, - MAILTO_REGEXP = /^mailto:/i; - - return function(text, target) { - if (!text) return text; - var match; - var raw = text; - var html = []; - var url; - var i; - while ((match = raw.match(LINKY_URL_REGEXP))) { - // We can not end in these as they are sometimes found at the end of the sentence - url = match[0]; - // if we did not match ftp/http/www/mailto then assume mailto - if (!match[2] && !match[4]) { - url = (match[3] ? 'http://' : 'mailto:') + url; - } - i = match.index; - addText(raw.substr(0, i)); - addLink(url, match[0].replace(MAILTO_REGEXP, '')); - raw = raw.substring(i + match[0].length); - } - addText(raw); - return $sanitize(html.join('')); - - function addText(text) { - if (!text) { - return; - } - html.push(sanitizeText(text)); - } - - function addLink(url, text) { - html.push(''); - addText(text); - html.push(''); - } - }; -}]); - - -})(window, window.angular); diff --git a/themes/src/main/resources/theme/keycloak/common/resources/lib/angular/angular-translate-loader-url.js b/themes/src/main/resources/theme/keycloak/common/resources/lib/angular/angular-translate-loader-url.js deleted file mode 100644 index 619a819dd4..0000000000 --- a/themes/src/main/resources/theme/keycloak/common/resources/lib/angular/angular-translate-loader-url.js +++ /dev/null @@ -1,75 +0,0 @@ -/*! - * angular-translate - v2.7.2 - 2015-06-01 - * http://github.com/angular-translate/angular-translate - * Copyright (c) 2015 ; Licensed MIT - */ -(function (root, factory) { - if (typeof define === 'function' && define.amd) { - // AMD. Register as an anonymous module unless amdModuleId is set - define([], function () { - return (factory()); - }); - } else if (typeof exports === 'object') { - // Node. Does not work with strict CommonJS, but - // only CommonJS-like environments that support module.exports, - // like Node. - module.exports = factory(); - } else { - factory(); - } -}(this, function () { - -angular.module('pascalprecht.translate') -/** - * @ngdoc object - * @name pascalprecht.translate.$translateUrlLoader - * @requires $q - * @requires $http - * - * @description - * Creates a loading function for a typical dynamic url pattern: - * "locale.php?lang=en_US", "locale.php?lang=de_DE", "locale.php?language=nl_NL" etc. - * Prefixing the specified url, the current requested, language id will be applied - * with "?{queryParameter}={key}". - * Using this service, the response of these urls must be an object of - * key-value pairs. - * - * @param {object} options Options object, which gets the url, key and - * optional queryParameter ('lang' is used by default). - */ -.factory('$translateUrlLoader', $translateUrlLoader); - -function $translateUrlLoader($q, $http) { - - 'use strict'; - - return function (options) { - - if (!options || !options.url) { - throw new Error('Couldn\'t use urlLoader since no url is given!'); - } - - var deferred = $q.defer(), - requestParams = {}; - - requestParams[options.queryParameter || 'lang'] = options.key; - - $http(angular.extend({ - url: options.url, - params: requestParams, - method: 'GET' - }, options.$http)).success(function (data) { - deferred.resolve(data); - }).error(function () { - deferred.reject(options.key); - }); - - return deferred.promise; - }; -} -$translateUrlLoader.$inject = ['$q', '$http']; - -$translateUrlLoader.displayName = '$translateUrlLoader'; -return 'pascalprecht.translate'; - -})); diff --git a/themes/src/main/resources/theme/keycloak/common/resources/lib/angular/angular-translate.js b/themes/src/main/resources/theme/keycloak/common/resources/lib/angular/angular-translate.js deleted file mode 100644 index e7183a0a9f..0000000000 --- a/themes/src/main/resources/theme/keycloak/common/resources/lib/angular/angular-translate.js +++ /dev/null @@ -1,2904 +0,0 @@ -/*! - * angular-translate - v2.7.2 - 2015-06-01 - * http://github.com/angular-translate/angular-translate - * Copyright (c) 2015 ; Licensed MIT - */ -(function (root, factory) { - if (typeof define === 'function' && define.amd) { - // AMD. Register as an anonymous module unless amdModuleId is set - define([], function () { - return (factory()); - }); - } else if (typeof exports === 'object') { - // Node. Does not work with strict CommonJS, but - // only CommonJS-like environments that support module.exports, - // like Node. - module.exports = factory(); - } else { - factory(); - } -}(this, function () { - -/** - * @ngdoc overview - * @name pascalprecht.translate - * - * @description - * The main module which holds everything together. - */ -angular.module('pascalprecht.translate', ['ng']) - .run(runTranslate); - -function runTranslate($translate) { - - 'use strict'; - - var key = $translate.storageKey(), - storage = $translate.storage(); - - var fallbackFromIncorrectStorageValue = function () { - var preferred = $translate.preferredLanguage(); - if (angular.isString(preferred)) { - $translate.use(preferred); - // $translate.use() will also remember the language. - // So, we don't need to call storage.put() here. - } else { - storage.put(key, $translate.use()); - } - }; - - fallbackFromIncorrectStorageValue.displayName = 'fallbackFromIncorrectStorageValue'; - - if (storage) { - if (!storage.get(key)) { - fallbackFromIncorrectStorageValue(); - } else { - $translate.use(storage.get(key))['catch'](fallbackFromIncorrectStorageValue); - } - } else if (angular.isString($translate.preferredLanguage())) { - $translate.use($translate.preferredLanguage()); - } -} -runTranslate.$inject = ['$translate']; - -runTranslate.displayName = 'runTranslate'; - -/** - * @ngdoc object - * @name pascalprecht.translate.$translateSanitizationProvider - * - * @description - * - * Configurations for $translateSanitization - */ -angular.module('pascalprecht.translate').provider('$translateSanitization', $translateSanitizationProvider); - -function $translateSanitizationProvider () { - - 'use strict'; - - var $sanitize, - currentStrategy = null, // TODO change to either 'sanitize', 'escape' or ['sanitize', 'escapeParameters'] in 3.0. - hasConfiguredStrategy = false, - hasShownNoStrategyConfiguredWarning = false, - strategies; - - /** - * Definition of a sanitization strategy function - * @callback StrategyFunction - * @param {string|object} value - value to be sanitized (either a string or an interpolated value map) - * @param {string} mode - either 'text' for a string (translation) or 'params' for the interpolated params - * @return {string|object} - */ - - /** - * @ngdoc property - * @name strategies - * @propertyOf pascalprecht.translate.$translateSanitizationProvider - * - * @description - * Following strategies are built-in: - *
    - *
    sanitize
    - *
    Sanitizes HTML in the translation text using $sanitize
    - *
    escape
    - *
    Escapes HTML in the translation
    - *
    sanitizeParameters
    - *
    Sanitizes HTML in the values of the interpolation parameters using $sanitize
    - *
    escapeParameters
    - *
    Escapes HTML in the values of the interpolation parameters
    - *
    escaped
    - *
    Support legacy strategy name 'escaped' for backwards compatibility (will be removed in 3.0)
    - *
    - * - */ - - strategies = { - sanitize: function (value, mode) { - if (mode === 'text') { - value = htmlSanitizeValue(value); - } - return value; - }, - escape: function (value, mode) { - if (mode === 'text') { - value = htmlEscapeValue(value); - } - return value; - }, - sanitizeParameters: function (value, mode) { - if (mode === 'params') { - value = mapInterpolationParameters(value, htmlSanitizeValue); - } - return value; - }, - escapeParameters: function (value, mode) { - if (mode === 'params') { - value = mapInterpolationParameters(value, htmlEscapeValue); - } - return value; - } - }; - // Support legacy strategy name 'escaped' for backwards compatibility. - // TODO should be removed in 3.0 - strategies.escaped = strategies.escapeParameters; - - /** - * @ngdoc function - * @name pascalprecht.translate.$translateSanitizationProvider#addStrategy - * @methodOf pascalprecht.translate.$translateSanitizationProvider - * - * @description - * Adds a sanitization strategy to the list of known strategies. - * - * @param {string} strategyName - unique key for a strategy - * @param {StrategyFunction} strategyFunction - strategy function - * @returns {object} this - */ - this.addStrategy = function (strategyName, strategyFunction) { - strategies[strategyName] = strategyFunction; - return this; - }; - - /** - * @ngdoc function - * @name pascalprecht.translate.$translateSanitizationProvider#removeStrategy - * @methodOf pascalprecht.translate.$translateSanitizationProvider - * - * @description - * Removes a sanitization strategy from the list of known strategies. - * - * @param {string} strategyName - unique key for a strategy - * @returns {object} this - */ - this.removeStrategy = function (strategyName) { - delete strategies[strategyName]; - return this; - }; - - /** - * @ngdoc function - * @name pascalprecht.translate.$translateSanitizationProvider#useStrategy - * @methodOf pascalprecht.translate.$translateSanitizationProvider - * - * @description - * Selects a sanitization strategy. When an array is provided the strategies will be executed in order. - * - * @param {string|StrategyFunction|array} strategy The sanitization strategy / strategies which should be used. Either a name of an existing strategy, a custom strategy function, or an array consisting of multiple names and / or custom functions. - * @returns {object} this - */ - this.useStrategy = function (strategy) { - hasConfiguredStrategy = true; - currentStrategy = strategy; - return this; - }; - - /** - * @ngdoc object - * @name pascalprecht.translate.$translateSanitization - * @requires $injector - * @requires $log - * - * @description - * Sanitizes interpolation parameters and translated texts. - * - */ - this.$get = ['$injector', '$log', function ($injector, $log) { - - var applyStrategies = function (value, mode, selectedStrategies) { - angular.forEach(selectedStrategies, function (selectedStrategy) { - if (angular.isFunction(selectedStrategy)) { - value = selectedStrategy(value, mode); - } else if (angular.isFunction(strategies[selectedStrategy])) { - value = strategies[selectedStrategy](value, mode); - } else { - throw new Error('pascalprecht.translate.$translateSanitization: Unknown sanitization strategy: \'' + selectedStrategy + '\''); - } - }); - return value; - }; - - // TODO: should be removed in 3.0 - var showNoStrategyConfiguredWarning = function () { - if (!hasConfiguredStrategy && !hasShownNoStrategyConfiguredWarning) { - $log.warn('pascalprecht.translate.$translateSanitization: No sanitization strategy has been configured. This can have serious security implications. See http://angular-translate.github.io/docs/#/guide/19_security for details.'); - hasShownNoStrategyConfiguredWarning = true; - } - }; - - if ($injector.has('$sanitize')) { - $sanitize = $injector.get('$sanitize'); - } - - return { - /** - * @ngdoc function - * @name pascalprecht.translate.$translateSanitization#useStrategy - * @methodOf pascalprecht.translate.$translateSanitization - * - * @description - * Selects a sanitization strategy. When an array is provided the strategies will be executed in order. - * - * @param {string|StrategyFunction|array} strategy The sanitization strategy / strategies which should be used. Either a name of an existing strategy, a custom strategy function, or an array consisting of multiple names and / or custom functions. - */ - useStrategy: (function (self) { - return function (strategy) { - self.useStrategy(strategy); - }; - })(this), - - /** - * @ngdoc function - * @name pascalprecht.translate.$translateSanitization#sanitize - * @methodOf pascalprecht.translate.$translateSanitization - * - * @description - * Sanitizes a value. - * - * @param {string|object} value The value which should be sanitized. - * @param {string} mode The current sanitization mode, either 'params' or 'text'. - * @param {string|StrategyFunction|array} [strategy] Optional custom strategy which should be used instead of the currently selected strategy. - * @returns {string|object} sanitized value - */ - sanitize: function (value, mode, strategy) { - if (!currentStrategy) { - showNoStrategyConfiguredWarning(); - } - - if (arguments.length < 3) { - strategy = currentStrategy; - } - - if (!strategy) { - return value; - } - - var selectedStrategies = angular.isArray(strategy) ? strategy : [strategy]; - return applyStrategies(value, mode, selectedStrategies); - } - }; - }]; - - var htmlEscapeValue = function (value) { - var element = angular.element('
    '); - element.text(value); // not chainable, see #1044 - return element.html(); - }; - - var htmlSanitizeValue = function (value) { - if (!$sanitize) { - throw new Error('pascalprecht.translate.$translateSanitization: Error cannot find $sanitize service. Either include the ngSanitize module (https://docs.angularjs.org/api/ngSanitize) or use a sanitization strategy which does not depend on $sanitize, such as \'escape\'.'); - } - return $sanitize(value); - }; - - var mapInterpolationParameters = function (value, iteratee) { - if (angular.isObject(value)) { - var result = angular.isArray(value) ? [] : {}; - - angular.forEach(value, function (propertyValue, propertyKey) { - result[propertyKey] = mapInterpolationParameters(propertyValue, iteratee); - }); - - return result; - } else if (angular.isNumber(value)) { - return value; - } else { - return iteratee(value); - } - }; -} - -/** - * @ngdoc object - * @name pascalprecht.translate.$translateProvider - * @description - * - * $translateProvider allows developers to register translation-tables, asynchronous loaders - * and similar to configure translation behavior directly inside of a module. - * - */ -angular.module('pascalprecht.translate') -.constant('pascalprechtTranslateOverrider', {}) -.provider('$translate', $translate); - -function $translate($STORAGE_KEY, $windowProvider, $translateSanitizationProvider, pascalprechtTranslateOverrider) { - - 'use strict'; - - var $translationTable = {}, - $preferredLanguage, - $availableLanguageKeys = [], - $languageKeyAliases, - $fallbackLanguage, - $fallbackWasString, - $uses, - $nextLang, - $storageFactory, - $storageKey = $STORAGE_KEY, - $storagePrefix, - $missingTranslationHandlerFactory, - $interpolationFactory, - $interpolatorFactories = [], - $loaderFactory, - $cloakClassName = 'translate-cloak', - $loaderOptions, - $notFoundIndicatorLeft, - $notFoundIndicatorRight, - $postCompilingEnabled = false, - $forceAsyncReloadEnabled = false, - NESTED_OBJECT_DELIMITER = '.', - loaderCache, - directivePriority = 0, - statefulFilter = true, - uniformLanguageTagResolver = 'default', - languageTagResolver = { - 'default': function (tag) { - return (tag || '').split('-').join('_'); - }, - java: function (tag) { - var temp = (tag || '').split('-').join('_'); - var parts = temp.split('_'); - return parts.length > 1 ? (parts[0].toLowerCase() + '_' + parts[1].toUpperCase()) : temp; - }, - bcp47: function (tag) { - var temp = (tag || '').split('_').join('-'); - var parts = temp.split('-'); - return parts.length > 1 ? (parts[0].toLowerCase() + '-' + parts[1].toUpperCase()) : temp; - } - }; - - var version = '2.7.2'; - - // tries to determine the browsers language - var getFirstBrowserLanguage = function () { - - // internal purpose only - if (angular.isFunction(pascalprechtTranslateOverrider.getLocale)) { - return pascalprechtTranslateOverrider.getLocale(); - } - - var nav = $windowProvider.$get().navigator, - browserLanguagePropertyKeys = ['language', 'browserLanguage', 'systemLanguage', 'userLanguage'], - i, - language; - - // support for HTML 5.1 "navigator.languages" - if (angular.isArray(nav.languages)) { - for (i = 0; i < nav.languages.length; i++) { - language = nav.languages[i]; - if (language && language.length) { - return language; - } - } - } - - // support for other well known properties in browsers - for (i = 0; i < browserLanguagePropertyKeys.length; i++) { - language = nav[browserLanguagePropertyKeys[i]]; - if (language && language.length) { - return language; - } - } - - return null; - }; - getFirstBrowserLanguage.displayName = 'angular-translate/service: getFirstBrowserLanguage'; - - // tries to determine the browsers locale - var getLocale = function () { - var locale = getFirstBrowserLanguage() || ''; - if (languageTagResolver[uniformLanguageTagResolver]) { - locale = languageTagResolver[uniformLanguageTagResolver](locale); - } - return locale; - }; - getLocale.displayName = 'angular-translate/service: getLocale'; - - /** - * @name indexOf - * @private - * - * @description - * indexOf polyfill. Kinda sorta. - * - * @param {array} array Array to search in. - * @param {string} searchElement Element to search for. - * - * @returns {int} Index of search element. - */ - var indexOf = function(array, searchElement) { - for (var i = 0, len = array.length; i < len; i++) { - if (array[i] === searchElement) { - return i; - } - } - return -1; - }; - - /** - * @name trim - * @private - * - * @description - * trim polyfill - * - * @returns {string} The string stripped of whitespace from both ends - */ - var trim = function() { - return this.toString().replace(/^\s+|\s+$/g, ''); - }; - - var negotiateLocale = function (preferred) { - - var avail = [], - locale = angular.lowercase(preferred), - i = 0, - n = $availableLanguageKeys.length; - - for (; i < n; i++) { - avail.push(angular.lowercase($availableLanguageKeys[i])); - } - - if (indexOf(avail, locale) > -1) { - return preferred; - } - - if ($languageKeyAliases) { - var alias; - for (var langKeyAlias in $languageKeyAliases) { - var hasWildcardKey = false; - var hasExactKey = Object.prototype.hasOwnProperty.call($languageKeyAliases, langKeyAlias) && - angular.lowercase(langKeyAlias) === angular.lowercase(preferred); - - if (langKeyAlias.slice(-1) === '*') { - hasWildcardKey = langKeyAlias.slice(0, -1) === preferred.slice(0, langKeyAlias.length-1); - } - if (hasExactKey || hasWildcardKey) { - alias = $languageKeyAliases[langKeyAlias]; - if (indexOf(avail, angular.lowercase(alias)) > -1) { - return alias; - } - } - } - } - - if (preferred) { - var parts = preferred.split('_'); - - if (parts.length > 1 && indexOf(avail, angular.lowercase(parts[0])) > -1) { - return parts[0]; - } - } - - // If everything fails, just return the preferred, unchanged. - return preferred; - }; - - /** - * @ngdoc function - * @name pascalprecht.translate.$translateProvider#translations - * @methodOf pascalprecht.translate.$translateProvider - * - * @description - * Registers a new translation table for specific language key. - * - * To register a translation table for specific language, pass a defined language - * key as first parameter. - * - *
    -   *  // register translation table for language: 'de_DE'
    -   *  $translateProvider.translations('de_DE', {
    -   *    'GREETING': 'Hallo Welt!'
    -   *  });
    -   *
    -   *  // register another one
    -   *  $translateProvider.translations('en_US', {
    -   *    'GREETING': 'Hello world!'
    -   *  });
    -   * 
    - * - * When registering multiple translation tables for for the same language key, - * the actual translation table gets extended. This allows you to define module - * specific translation which only get added, once a specific module is loaded in - * your app. - * - * Invoking this method with no arguments returns the translation table which was - * registered with no language key. Invoking it with a language key returns the - * related translation table. - * - * @param {string} key A language key. - * @param {object} translationTable A plain old JavaScript object that represents a translation table. - * - */ - var translations = function (langKey, translationTable) { - - if (!langKey && !translationTable) { - return $translationTable; - } - - if (langKey && !translationTable) { - if (angular.isString(langKey)) { - return $translationTable[langKey]; - } - } else { - if (!angular.isObject($translationTable[langKey])) { - $translationTable[langKey] = {}; - } - angular.extend($translationTable[langKey], flatObject(translationTable)); - } - return this; - }; - - this.translations = translations; - - /** - * @ngdoc function - * @name pascalprecht.translate.$translateProvider#cloakClassName - * @methodOf pascalprecht.translate.$translateProvider - * - * @description - * - * Let's you change the class name for `translate-cloak` directive. - * Default class name is `translate-cloak`. - * - * @param {string} name translate-cloak class name - */ - this.cloakClassName = function (name) { - if (!name) { - return $cloakClassName; - } - $cloakClassName = name; - return this; - }; - - /** - * @name flatObject - * @private - * - * @description - * Flats an object. This function is used to flatten given translation data with - * namespaces, so they are later accessible via dot notation. - */ - var flatObject = function (data, path, result, prevKey) { - var key, keyWithPath, keyWithShortPath, val; - - if (!path) { - path = []; - } - if (!result) { - result = {}; - } - for (key in data) { - if (!Object.prototype.hasOwnProperty.call(data, key)) { - continue; - } - val = data[key]; - if (angular.isObject(val)) { - flatObject(val, path.concat(key), result, key); - } else { - keyWithPath = path.length ? ('' + path.join(NESTED_OBJECT_DELIMITER) + NESTED_OBJECT_DELIMITER + key) : key; - if(path.length && key === prevKey){ - // Create shortcut path (foo.bar == foo.bar.bar) - keyWithShortPath = '' + path.join(NESTED_OBJECT_DELIMITER); - // Link it to original path - result[keyWithShortPath] = '@:' + keyWithPath; - } - result[keyWithPath] = val; - } - } - return result; - }; - flatObject.displayName = 'flatObject'; - - /** - * @ngdoc function - * @name pascalprecht.translate.$translateProvider#addInterpolation - * @methodOf pascalprecht.translate.$translateProvider - * - * @description - * Adds interpolation services to angular-translate, so it can manage them. - * - * @param {object} factory Interpolation service factory - */ - this.addInterpolation = function (factory) { - $interpolatorFactories.push(factory); - return this; - }; - - /** - * @ngdoc function - * @name pascalprecht.translate.$translateProvider#useMessageFormatInterpolation - * @methodOf pascalprecht.translate.$translateProvider - * - * @description - * Tells angular-translate to use interpolation functionality of messageformat.js. - * This is useful when having high level pluralization and gender selection. - */ - this.useMessageFormatInterpolation = function () { - return this.useInterpolation('$translateMessageFormatInterpolation'); - }; - - /** - * @ngdoc function - * @name pascalprecht.translate.$translateProvider#useInterpolation - * @methodOf pascalprecht.translate.$translateProvider - * - * @description - * Tells angular-translate which interpolation style to use as default, application-wide. - * Simply pass a factory/service name. The interpolation service has to implement - * the correct interface. - * - * @param {string} factory Interpolation service name. - */ - this.useInterpolation = function (factory) { - $interpolationFactory = factory; - return this; - }; - - /** - * @ngdoc function - * @name pascalprecht.translate.$translateProvider#useSanitizeStrategy - * @methodOf pascalprecht.translate.$translateProvider - * - * @description - * Simply sets a sanitation strategy type. - * - * @param {string} value Strategy type. - */ - this.useSanitizeValueStrategy = function (value) { - $translateSanitizationProvider.useStrategy(value); - return this; - }; - - /** - * @ngdoc function - * @name pascalprecht.translate.$translateProvider#preferredLanguage - * @methodOf pascalprecht.translate.$translateProvider - * - * @description - * Tells the module which of the registered translation tables to use for translation - * at initial startup by passing a language key. Similar to `$translateProvider#use` - * only that it says which language to **prefer**. - * - * @param {string} langKey A language key. - * - */ - this.preferredLanguage = function(langKey) { - setupPreferredLanguage(langKey); - return this; - - }; - var setupPreferredLanguage = function (langKey) { - if (langKey) { - $preferredLanguage = langKey; - } - return $preferredLanguage; - }; - /** - * @ngdoc function - * @name pascalprecht.translate.$translateProvider#translationNotFoundIndicator - * @methodOf pascalprecht.translate.$translateProvider - * - * @description - * Sets an indicator which is used when a translation isn't found. E.g. when - * setting the indicator as 'X' and one tries to translate a translation id - * called `NOT_FOUND`, this will result in `X NOT_FOUND X`. - * - * Internally this methods sets a left indicator and a right indicator using - * `$translateProvider.translationNotFoundIndicatorLeft()` and - * `$translateProvider.translationNotFoundIndicatorRight()`. - * - * **Note**: These methods automatically add a whitespace between the indicators - * and the translation id. - * - * @param {string} indicator An indicator, could be any string. - */ - this.translationNotFoundIndicator = function (indicator) { - this.translationNotFoundIndicatorLeft(indicator); - this.translationNotFoundIndicatorRight(indicator); - return this; - }; - - /** - * ngdoc function - * @name pascalprecht.translate.$translateProvider#translationNotFoundIndicatorLeft - * @methodOf pascalprecht.translate.$translateProvider - * - * @description - * Sets an indicator which is used when a translation isn't found left to the - * translation id. - * - * @param {string} indicator An indicator. - */ - this.translationNotFoundIndicatorLeft = function (indicator) { - if (!indicator) { - return $notFoundIndicatorLeft; - } - $notFoundIndicatorLeft = indicator; - return this; - }; - - /** - * ngdoc function - * @name pascalprecht.translate.$translateProvider#translationNotFoundIndicatorLeft - * @methodOf pascalprecht.translate.$translateProvider - * - * @description - * Sets an indicator which is used when a translation isn't found right to the - * translation id. - * - * @param {string} indicator An indicator. - */ - this.translationNotFoundIndicatorRight = function (indicator) { - if (!indicator) { - return $notFoundIndicatorRight; - } - $notFoundIndicatorRight = indicator; - return this; - }; - - /** - * @ngdoc function - * @name pascalprecht.translate.$translateProvider#fallbackLanguage - * @methodOf pascalprecht.translate.$translateProvider - * - * @description - * Tells the module which of the registered translation tables to use when missing translations - * at initial startup by passing a language key. Similar to `$translateProvider#use` - * only that it says which language to **fallback**. - * - * @param {string||array} langKey A language key. - * - */ - this.fallbackLanguage = function (langKey) { - fallbackStack(langKey); - return this; - }; - - var fallbackStack = function (langKey) { - if (langKey) { - if (angular.isString(langKey)) { - $fallbackWasString = true; - $fallbackLanguage = [ langKey ]; - } else if (angular.isArray(langKey)) { - $fallbackWasString = false; - $fallbackLanguage = langKey; - } - if (angular.isString($preferredLanguage) && indexOf($fallbackLanguage, $preferredLanguage) < 0) { - $fallbackLanguage.push($preferredLanguage); - } - - return this; - } else { - if ($fallbackWasString) { - return $fallbackLanguage[0]; - } else { - return $fallbackLanguage; - } - } - }; - - /** - * @ngdoc function - * @name pascalprecht.translate.$translateProvider#use - * @methodOf pascalprecht.translate.$translateProvider - * - * @description - * Set which translation table to use for translation by given language key. When - * trying to 'use' a language which isn't provided, it'll throw an error. - * - * You actually don't have to use this method since `$translateProvider#preferredLanguage` - * does the job too. - * - * @param {string} langKey A language key. - */ - this.use = function (langKey) { - if (langKey) { - if (!$translationTable[langKey] && (!$loaderFactory)) { - // only throw an error, when not loading translation data asynchronously - throw new Error('$translateProvider couldn\'t find translationTable for langKey: \'' + langKey + '\''); - } - $uses = langKey; - return this; - } - return $uses; - }; - - /** - * @ngdoc function - * @name pascalprecht.translate.$translateProvider#storageKey - * @methodOf pascalprecht.translate.$translateProvider - * - * @description - * Tells the module which key must represent the choosed language by a user in the storage. - * - * @param {string} key A key for the storage. - */ - var storageKey = function(key) { - if (!key) { - if ($storagePrefix) { - return $storagePrefix + $storageKey; - } - return $storageKey; - } - $storageKey = key; - return this; - }; - - this.storageKey = storageKey; - - /** - * @ngdoc function - * @name pascalprecht.translate.$translateProvider#useUrlLoader - * @methodOf pascalprecht.translate.$translateProvider - * - * @description - * Tells angular-translate to use `$translateUrlLoader` extension service as loader. - * - * @param {string} url Url - * @param {Object=} options Optional configuration object - */ - this.useUrlLoader = function (url, options) { - return this.useLoader('$translateUrlLoader', angular.extend({ url: url }, options)); - }; - - /** - * @ngdoc function - * @name pascalprecht.translate.$translateProvider#useStaticFilesLoader - * @methodOf pascalprecht.translate.$translateProvider - * - * @description - * Tells angular-translate to use `$translateStaticFilesLoader` extension service as loader. - * - * @param {Object=} options Optional configuration object - */ - this.useStaticFilesLoader = function (options) { - return this.useLoader('$translateStaticFilesLoader', options); - }; - - /** - * @ngdoc function - * @name pascalprecht.translate.$translateProvider#useLoader - * @methodOf pascalprecht.translate.$translateProvider - * - * @description - * Tells angular-translate to use any other service as loader. - * - * @param {string} loaderFactory Factory name to use - * @param {Object=} options Optional configuration object - */ - this.useLoader = function (loaderFactory, options) { - $loaderFactory = loaderFactory; - $loaderOptions = options || {}; - return this; - }; - - /** - * @ngdoc function - * @name pascalprecht.translate.$translateProvider#useLocalStorage - * @methodOf pascalprecht.translate.$translateProvider - * - * @description - * Tells angular-translate to use `$translateLocalStorage` service as storage layer. - * - */ - this.useLocalStorage = function () { - return this.useStorage('$translateLocalStorage'); - }; - - /** - * @ngdoc function - * @name pascalprecht.translate.$translateProvider#useCookieStorage - * @methodOf pascalprecht.translate.$translateProvider - * - * @description - * Tells angular-translate to use `$translateCookieStorage` service as storage layer. - */ - this.useCookieStorage = function () { - return this.useStorage('$translateCookieStorage'); - }; - - /** - * @ngdoc function - * @name pascalprecht.translate.$translateProvider#useStorage - * @methodOf pascalprecht.translate.$translateProvider - * - * @description - * Tells angular-translate to use custom service as storage layer. - */ - this.useStorage = function (storageFactory) { - $storageFactory = storageFactory; - return this; - }; - - /** - * @ngdoc function - * @name pascalprecht.translate.$translateProvider#storagePrefix - * @methodOf pascalprecht.translate.$translateProvider - * - * @description - * Sets prefix for storage key. - * - * @param {string} prefix Storage key prefix - */ - this.storagePrefix = function (prefix) { - if (!prefix) { - return prefix; - } - $storagePrefix = prefix; - return this; - }; - - /** - * @ngdoc function - * @name pascalprecht.translate.$translateProvider#useMissingTranslationHandlerLog - * @methodOf pascalprecht.translate.$translateProvider - * - * @description - * Tells angular-translate to use built-in log handler when trying to translate - * a translation Id which doesn't exist. - * - * This is actually a shortcut method for `useMissingTranslationHandler()`. - * - */ - this.useMissingTranslationHandlerLog = function () { - return this.useMissingTranslationHandler('$translateMissingTranslationHandlerLog'); - }; - - /** - * @ngdoc function - * @name pascalprecht.translate.$translateProvider#useMissingTranslationHandler - * @methodOf pascalprecht.translate.$translateProvider - * - * @description - * Expects a factory name which later gets instantiated with `$injector`. - * This method can be used to tell angular-translate to use a custom - * missingTranslationHandler. Just build a factory which returns a function - * and expects a translation id as argument. - * - * Example: - *
    -   *  app.config(function ($translateProvider) {
    -   *    $translateProvider.useMissingTranslationHandler('customHandler');
    -   *  });
    -   *
    -   *  app.factory('customHandler', function (dep1, dep2) {
    -   *    return function (translationId) {
    -   *      // something with translationId and dep1 and dep2
    -   *    };
    -   *  });
    -   * 
    - * - * @param {string} factory Factory name - */ - this.useMissingTranslationHandler = function (factory) { - $missingTranslationHandlerFactory = factory; - return this; - }; - - /** - * @ngdoc function - * @name pascalprecht.translate.$translateProvider#usePostCompiling - * @methodOf pascalprecht.translate.$translateProvider - * - * @description - * If post compiling is enabled, all translated values will be processed - * again with AngularJS' $compile. - * - * Example: - *
    -   *  app.config(function ($translateProvider) {
    -   *    $translateProvider.usePostCompiling(true);
    -   *  });
    -   * 
    - * - * @param {string} factory Factory name - */ - this.usePostCompiling = function (value) { - $postCompilingEnabled = !(!value); - return this; - }; - - /** - * @ngdoc function - * @name pascalprecht.translate.$translateProvider#forceAsyncReload - * @methodOf pascalprecht.translate.$translateProvider - * - * @description - * If force async reload is enabled, async loader will always be called - * even if $translationTable already contains the language key, adding - * possible new entries to the $translationTable. - * - * Example: - *
    -   *  app.config(function ($translateProvider) {
    -   *    $translateProvider.forceAsyncReload(true);
    -   *  });
    -   * 
    - * - * @param {boolean} value - valid values are true or false - */ - this.forceAsyncReload = function (value) { - $forceAsyncReloadEnabled = !(!value); - return this; - }; - - /** - * @ngdoc function - * @name pascalprecht.translate.$translateProvider#uniformLanguageTag - * @methodOf pascalprecht.translate.$translateProvider - * - * @description - * Tells angular-translate which language tag should be used as a result when determining - * the current browser language. - * - * This setting must be set before invoking {@link pascalprecht.translate.$translateProvider#methods_determinePreferredLanguage determinePreferredLanguage()}. - * - *
    -   * $translateProvider
    -   *   .uniformLanguageTag('bcp47')
    -   *   .determinePreferredLanguage()
    -   * 
    - * - * The resolver currently supports: - * * default - * (traditionally: hyphens will be converted into underscores, i.e. en-US => en_US) - * en-US => en_US - * en_US => en_US - * en-us => en_us - * * java - * like default, but the second part will be always in uppercase - * en-US => en_US - * en_US => en_US - * en-us => en_US - * * BCP 47 (RFC 4646 & 4647) - * en-US => en-US - * en_US => en-US - * en-us => en-US - * - * See also: - * * http://en.wikipedia.org/wiki/IETF_language_tag - * * http://www.w3.org/International/core/langtags/ - * * http://tools.ietf.org/html/bcp47 - * - * @param {string|object} options - options (or standard) - * @param {string} options.standard - valid values are 'default', 'bcp47', 'java' - */ - this.uniformLanguageTag = function (options) { - - if (!options) { - options = {}; - } else if (angular.isString(options)) { - options = { - standard: options - }; - } - - uniformLanguageTagResolver = options.standard; - - return this; - }; - - /** - * @ngdoc function - * @name pascalprecht.translate.$translateProvider#determinePreferredLanguage - * @methodOf pascalprecht.translate.$translateProvider - * - * @description - * Tells angular-translate to try to determine on its own which language key - * to set as preferred language. When `fn` is given, angular-translate uses it - * to determine a language key, otherwise it uses the built-in `getLocale()` - * method. - * - * The `getLocale()` returns a language key in the format `[lang]_[country]` or - * `[lang]` depending on what the browser provides. - * - * Use this method at your own risk, since not all browsers return a valid - * locale (see {@link pascalprecht.translate.$translateProvider#methods_uniformLanguageTag uniformLanguageTag()}). - * - * @param {Function=} fn Function to determine a browser's locale - */ - this.determinePreferredLanguage = function (fn) { - - var locale = (fn && angular.isFunction(fn)) ? fn() : getLocale(); - - if (!$availableLanguageKeys.length) { - $preferredLanguage = locale; - } else { - $preferredLanguage = negotiateLocale(locale); - } - - return this; - }; - - /** - * @ngdoc function - * @name pascalprecht.translate.$translateProvider#registerAvailableLanguageKeys - * @methodOf pascalprecht.translate.$translateProvider - * - * @description - * Registers a set of language keys the app will work with. Use this method in - * combination with - * {@link pascalprecht.translate.$translateProvider#determinePreferredLanguage determinePreferredLanguage}. - * When available languages keys are registered, angular-translate - * tries to find the best fitting language key depending on the browsers locale, - * considering your language key convention. - * - * @param {object} languageKeys Array of language keys the your app will use - * @param {object=} aliases Alias map. - */ - this.registerAvailableLanguageKeys = function (languageKeys, aliases) { - if (languageKeys) { - $availableLanguageKeys = languageKeys; - if (aliases) { - $languageKeyAliases = aliases; - } - return this; - } - return $availableLanguageKeys; - }; - - /** - * @ngdoc function - * @name pascalprecht.translate.$translateProvider#useLoaderCache - * @methodOf pascalprecht.translate.$translateProvider - * - * @description - * Registers a cache for internal $http based loaders. - * {@link pascalprecht.translate.$translateProvider#determinePreferredLanguage determinePreferredLanguage}. - * When false the cache will be disabled (default). When true or undefined - * the cache will be a default (see $cacheFactory). When an object it will - * be treat as a cache object itself: the usage is $http({cache: cache}) - * - * @param {object} cache boolean, string or cache-object - */ - this.useLoaderCache = function (cache) { - if (cache === false) { - // disable cache - loaderCache = undefined; - } else if (cache === true) { - // enable cache using AJS defaults - loaderCache = true; - } else if (typeof(cache) === 'undefined') { - // enable cache using default - loaderCache = '$translationCache'; - } else if (cache) { - // enable cache using given one (see $cacheFactory) - loaderCache = cache; - } - return this; - }; - - /** - * @ngdoc function - * @name pascalprecht.translate.$translateProvider#directivePriority - * @methodOf pascalprecht.translate.$translateProvider - * - * @description - * Sets the default priority of the translate directive. The standard value is `0`. - * Calling this function without an argument will return the current value. - * - * @param {number} priority for the translate-directive - */ - this.directivePriority = function (priority) { - if (priority === undefined) { - // getter - return directivePriority; - } else { - // setter with chaining - directivePriority = priority; - return this; - } - }; - - /** - * @ngdoc function - * @name pascalprecht.translate.$translateProvider#statefulFilter - * @methodOf pascalprecht.translate.$translateProvider - * - * @description - * Since AngularJS 1.3, filters which are not stateless (depending at the scope) - * have to explicit define this behavior. - * Sets whether the translate filter should be stateful or stateless. The standard value is `true` - * meaning being stateful. - * Calling this function without an argument will return the current value. - * - * @param {boolean} state - defines the state of the filter - */ - this.statefulFilter = function (state) { - if (state === undefined) { - // getter - return statefulFilter; - } else { - // setter with chaining - statefulFilter = state; - return this; - } - }; - - /** - * @ngdoc object - * @name pascalprecht.translate.$translate - * @requires $interpolate - * @requires $log - * @requires $rootScope - * @requires $q - * - * @description - * The `$translate` service is the actual core of angular-translate. It expects a translation id - * and optional interpolate parameters to translate contents. - * - *
    -   *  $translate('HEADLINE_TEXT').then(function (translation) {
    -   *    $scope.translatedText = translation;
    -   *  });
    -   * 
    - * - * @param {string|array} translationId A token which represents a translation id - * This can be optionally an array of translation ids which - * results that the function returns an object where each key - * is the translation id and the value the translation. - * @param {object=} interpolateParams An object hash for dynamic values - * @param {string} interpolationId The id of the interpolation to use - * @returns {object} promise - */ - this.$get = [ - '$log', - '$injector', - '$rootScope', - '$q', - function ($log, $injector, $rootScope, $q) { - - var Storage, - defaultInterpolator = $injector.get($interpolationFactory || '$translateDefaultInterpolation'), - pendingLoader = false, - interpolatorHashMap = {}, - langPromises = {}, - fallbackIndex, - startFallbackIteration; - - var $translate = function (translationId, interpolateParams, interpolationId, defaultTranslationText) { - - // Duck detection: If the first argument is an array, a bunch of translations was requested. - // The result is an object. - if (angular.isArray(translationId)) { - // Inspired by Q.allSettled by Kris Kowal - // https://github.com/kriskowal/q/blob/b0fa72980717dc202ffc3cbf03b936e10ebbb9d7/q.js#L1553-1563 - // This transforms all promises regardless resolved or rejected - var translateAll = function (translationIds) { - var results = {}; // storing the actual results - var promises = []; // promises to wait for - // Wraps the promise a) being always resolved and b) storing the link id->value - var translate = function (translationId) { - var deferred = $q.defer(); - var regardless = function (value) { - results[translationId] = value; - deferred.resolve([translationId, value]); - }; - // we don't care whether the promise was resolved or rejected; just store the values - $translate(translationId, interpolateParams, interpolationId, defaultTranslationText).then(regardless, regardless); - return deferred.promise; - }; - for (var i = 0, c = translationIds.length; i < c; i++) { - promises.push(translate(translationIds[i])); - } - // wait for all (including storing to results) - return $q.all(promises).then(function () { - // return the results - return results; - }); - }; - return translateAll(translationId); - } - - var deferred = $q.defer(); - - // trim off any whitespace - if (translationId) { - translationId = trim.apply(translationId); - } - - var promiseToWaitFor = (function () { - var promise = $preferredLanguage ? - langPromises[$preferredLanguage] : - langPromises[$uses]; - - fallbackIndex = 0; - - if ($storageFactory && !promise) { - // looks like there's no pending promise for $preferredLanguage or - // $uses. Maybe there's one pending for a language that comes from - // storage. - var langKey = Storage.get($storageKey); - promise = langPromises[langKey]; - - if ($fallbackLanguage && $fallbackLanguage.length) { - var index = indexOf($fallbackLanguage, langKey); - // maybe the language from storage is also defined as fallback language - // we increase the fallback language index to not search in that language - // as fallback, since it's probably the first used language - // in that case the index starts after the first element - fallbackIndex = (index === 0) ? 1 : 0; - - // but we can make sure to ALWAYS fallback to preferred language at least - if (indexOf($fallbackLanguage, $preferredLanguage) < 0) { - $fallbackLanguage.push($preferredLanguage); - } - } - } - return promise; - }()); - - if (!promiseToWaitFor) { - // no promise to wait for? okay. Then there's no loader registered - // nor is a one pending for language that comes from storage. - // We can just translate. - determineTranslation(translationId, interpolateParams, interpolationId, defaultTranslationText).then(deferred.resolve, deferred.reject); - } else { - var promiseResolved = function () { - determineTranslation(translationId, interpolateParams, interpolationId, defaultTranslationText).then(deferred.resolve, deferred.reject); - }; - promiseResolved.displayName = 'promiseResolved'; - - promiseToWaitFor['finally'](promiseResolved, deferred.reject); - } - return deferred.promise; - }; - - /** - * @name applyNotFoundIndicators - * @private - * - * @description - * Applies not fount indicators to given translation id, if needed. - * This function gets only executed, if a translation id doesn't exist, - * which is why a translation id is expected as argument. - * - * @param {string} translationId Translation id. - * @returns {string} Same as given translation id but applied with not found - * indicators. - */ - var applyNotFoundIndicators = function (translationId) { - // applying notFoundIndicators - if ($notFoundIndicatorLeft) { - translationId = [$notFoundIndicatorLeft, translationId].join(' '); - } - if ($notFoundIndicatorRight) { - translationId = [translationId, $notFoundIndicatorRight].join(' '); - } - return translationId; - }; - - /** - * @name useLanguage - * @private - * - * @description - * Makes actual use of a language by setting a given language key as used - * language and informs registered interpolators to also use the given - * key as locale. - * - * @param {key} Locale key. - */ - var useLanguage = function (key) { - $uses = key; - $rootScope.$emit('$translateChangeSuccess', {language: key}); - - if ($storageFactory) { - Storage.put($translate.storageKey(), $uses); - } - // inform default interpolator - defaultInterpolator.setLocale($uses); - - var eachInterpolator = function (interpolator, id) { - interpolatorHashMap[id].setLocale($uses); - }; - eachInterpolator.displayName = 'eachInterpolatorLocaleSetter'; - - // inform all others too! - angular.forEach(interpolatorHashMap, eachInterpolator); - $rootScope.$emit('$translateChangeEnd', {language: key}); - }; - - /** - * @name loadAsync - * @private - * - * @description - * Kicks of registered async loader using `$injector` and applies existing - * loader options. When resolved, it updates translation tables accordingly - * or rejects with given language key. - * - * @param {string} key Language key. - * @return {Promise} A promise. - */ - var loadAsync = function (key) { - if (!key) { - throw 'No language key specified for loading.'; - } - - var deferred = $q.defer(); - - $rootScope.$emit('$translateLoadingStart', {language: key}); - pendingLoader = true; - - var cache = loaderCache; - if (typeof(cache) === 'string') { - // getting on-demand instance of loader - cache = $injector.get(cache); - } - - var loaderOptions = angular.extend({}, $loaderOptions, { - key: key, - $http: angular.extend({}, { - cache: cache - }, $loaderOptions.$http) - }); - - var onLoaderSuccess = function (data) { - var translationTable = {}; - $rootScope.$emit('$translateLoadingSuccess', {language: key}); - - if (angular.isArray(data)) { - angular.forEach(data, function (table) { - angular.extend(translationTable, flatObject(table)); - }); - } else { - angular.extend(translationTable, flatObject(data)); - } - pendingLoader = false; - deferred.resolve({ - key: key, - table: translationTable - }); - $rootScope.$emit('$translateLoadingEnd', {language: key}); - }; - onLoaderSuccess.displayName = 'onLoaderSuccess'; - - var onLoaderError = function (key) { - $rootScope.$emit('$translateLoadingError', {language: key}); - deferred.reject(key); - $rootScope.$emit('$translateLoadingEnd', {language: key}); - }; - onLoaderError.displayName = 'onLoaderError'; - - $injector.get($loaderFactory)(loaderOptions) - .then(onLoaderSuccess, onLoaderError); - - return deferred.promise; - }; - - if ($storageFactory) { - Storage = $injector.get($storageFactory); - - if (!Storage.get || !Storage.put) { - throw new Error('Couldn\'t use storage \'' + $storageFactory + '\', missing get() or put() method!'); - } - } - - // if we have additional interpolations that were added via - // $translateProvider.addInterpolation(), we have to map'em - if ($interpolatorFactories.length) { - var eachInterpolationFactory = function (interpolatorFactory) { - var interpolator = $injector.get(interpolatorFactory); - // setting initial locale for each interpolation service - interpolator.setLocale($preferredLanguage || $uses); - // make'em recognizable through id - interpolatorHashMap[interpolator.getInterpolationIdentifier()] = interpolator; - }; - eachInterpolationFactory.displayName = 'interpolationFactoryAdder'; - - angular.forEach($interpolatorFactories, eachInterpolationFactory); - } - - /** - * @name getTranslationTable - * @private - * - * @description - * Returns a promise that resolves to the translation table - * or is rejected if an error occurred. - * - * @param langKey - * @returns {Q.promise} - */ - var getTranslationTable = function (langKey) { - var deferred = $q.defer(); - if (Object.prototype.hasOwnProperty.call($translationTable, langKey)) { - deferred.resolve($translationTable[langKey]); - } else if (langPromises[langKey]) { - var onResolve = function (data) { - translations(data.key, data.table); - deferred.resolve(data.table); - }; - onResolve.displayName = 'translationTableResolver'; - langPromises[langKey].then(onResolve, deferred.reject); - } else { - deferred.reject(); - } - return deferred.promise; - }; - - /** - * @name getFallbackTranslation - * @private - * - * @description - * Returns a promise that will resolve to the translation - * or be rejected if no translation was found for the language. - * This function is currently only used for fallback language translation. - * - * @param langKey The language to translate to. - * @param translationId - * @param interpolateParams - * @param Interpolator - * @returns {Q.promise} - */ - var getFallbackTranslation = function (langKey, translationId, interpolateParams, Interpolator) { - var deferred = $q.defer(); - - var onResolve = function (translationTable) { - if (Object.prototype.hasOwnProperty.call(translationTable, translationId)) { - Interpolator.setLocale(langKey); - var translation = translationTable[translationId]; - if (translation.substr(0, 2) === '@:') { - getFallbackTranslation(langKey, translation.substr(2), interpolateParams, Interpolator) - .then(deferred.resolve, deferred.reject); - } else { - deferred.resolve(Interpolator.interpolate(translationTable[translationId], interpolateParams)); - } - Interpolator.setLocale($uses); - } else { - deferred.reject(); - } - }; - onResolve.displayName = 'fallbackTranslationResolver'; - - getTranslationTable(langKey).then(onResolve, deferred.reject); - - return deferred.promise; - }; - - /** - * @name getFallbackTranslationInstant - * @private - * - * @description - * Returns a translation - * This function is currently only used for fallback language translation. - * - * @param langKey The language to translate to. - * @param translationId - * @param interpolateParams - * @param Interpolator - * @returns {string} translation - */ - var getFallbackTranslationInstant = function (langKey, translationId, interpolateParams, Interpolator) { - var result, translationTable = $translationTable[langKey]; - - if (translationTable && Object.prototype.hasOwnProperty.call(translationTable, translationId)) { - Interpolator.setLocale(langKey); - result = Interpolator.interpolate(translationTable[translationId], interpolateParams); - if (result.substr(0, 2) === '@:') { - return getFallbackTranslationInstant(langKey, result.substr(2), interpolateParams, Interpolator); - } - Interpolator.setLocale($uses); - } - - return result; - }; - - - /** - * @name translateByHandler - * @private - * - * Translate by missing translation handler. - * - * @param translationId - * @returns translation created by $missingTranslationHandler or translationId is $missingTranslationHandler is - * absent - */ - var translateByHandler = function (translationId, interpolateParams) { - // If we have a handler factory - we might also call it here to determine if it provides - // a default text for a translationid that can't be found anywhere in our tables - if ($missingTranslationHandlerFactory) { - var resultString = $injector.get($missingTranslationHandlerFactory)(translationId, $uses, interpolateParams); - if (resultString !== undefined) { - return resultString; - } else { - return translationId; - } - } else { - return translationId; - } - }; - - /** - * @name resolveForFallbackLanguage - * @private - * - * Recursive helper function for fallbackTranslation that will sequentially look - * for a translation in the fallbackLanguages starting with fallbackLanguageIndex. - * - * @param fallbackLanguageIndex - * @param translationId - * @param interpolateParams - * @param Interpolator - * @returns {Q.promise} Promise that will resolve to the translation. - */ - var resolveForFallbackLanguage = function (fallbackLanguageIndex, translationId, interpolateParams, Interpolator, defaultTranslationText) { - var deferred = $q.defer(); - - if (fallbackLanguageIndex < $fallbackLanguage.length) { - var langKey = $fallbackLanguage[fallbackLanguageIndex]; - getFallbackTranslation(langKey, translationId, interpolateParams, Interpolator).then( - deferred.resolve, - function () { - // Look in the next fallback language for a translation. - // It delays the resolving by passing another promise to resolve. - resolveForFallbackLanguage(fallbackLanguageIndex + 1, translationId, interpolateParams, Interpolator, defaultTranslationText).then(deferred.resolve); - } - ); - } else { - // No translation found in any fallback language - // if a default translation text is set in the directive, then return this as a result - if (defaultTranslationText) { - deferred.resolve(defaultTranslationText); - } else { - // if no default translation is set and an error handler is defined, send it to the handler - // and then return the result - deferred.resolve(translateByHandler(translationId, interpolateParams)); - } - } - return deferred.promise; - }; - - /** - * @name resolveForFallbackLanguageInstant - * @private - * - * Recursive helper function for fallbackTranslation that will sequentially look - * for a translation in the fallbackLanguages starting with fallbackLanguageIndex. - * - * @param fallbackLanguageIndex - * @param translationId - * @param interpolateParams - * @param Interpolator - * @returns {string} translation - */ - var resolveForFallbackLanguageInstant = function (fallbackLanguageIndex, translationId, interpolateParams, Interpolator) { - var result; - - if (fallbackLanguageIndex < $fallbackLanguage.length) { - var langKey = $fallbackLanguage[fallbackLanguageIndex]; - result = getFallbackTranslationInstant(langKey, translationId, interpolateParams, Interpolator); - if (!result) { - result = resolveForFallbackLanguageInstant(fallbackLanguageIndex + 1, translationId, interpolateParams, Interpolator); - } - } - return result; - }; - - /** - * Translates with the usage of the fallback languages. - * - * @param translationId - * @param interpolateParams - * @param Interpolator - * @returns {Q.promise} Promise, that resolves to the translation. - */ - var fallbackTranslation = function (translationId, interpolateParams, Interpolator, defaultTranslationText) { - // Start with the fallbackLanguage with index 0 - return resolveForFallbackLanguage((startFallbackIteration>0 ? startFallbackIteration : fallbackIndex), translationId, interpolateParams, Interpolator, defaultTranslationText); - }; - - /** - * Translates with the usage of the fallback languages. - * - * @param translationId - * @param interpolateParams - * @param Interpolator - * @returns {String} translation - */ - var fallbackTranslationInstant = function (translationId, interpolateParams, Interpolator) { - // Start with the fallbackLanguage with index 0 - return resolveForFallbackLanguageInstant((startFallbackIteration>0 ? startFallbackIteration : fallbackIndex), translationId, interpolateParams, Interpolator); - }; - - var determineTranslation = function (translationId, interpolateParams, interpolationId, defaultTranslationText) { - - var deferred = $q.defer(); - - var table = $uses ? $translationTable[$uses] : $translationTable, - Interpolator = (interpolationId) ? interpolatorHashMap[interpolationId] : defaultInterpolator; - - // if the translation id exists, we can just interpolate it - if (table && Object.prototype.hasOwnProperty.call(table, translationId)) { - var translation = table[translationId]; - - // If using link, rerun $translate with linked translationId and return it - if (translation.substr(0, 2) === '@:') { - - $translate(translation.substr(2), interpolateParams, interpolationId, defaultTranslationText) - .then(deferred.resolve, deferred.reject); - } else { - deferred.resolve(Interpolator.interpolate(translation, interpolateParams)); - } - } else { - var missingTranslationHandlerTranslation; - // for logging purposes only (as in $translateMissingTranslationHandlerLog), value is not returned to promise - if ($missingTranslationHandlerFactory && !pendingLoader) { - missingTranslationHandlerTranslation = translateByHandler(translationId, interpolateParams); - } - - // since we couldn't translate the inital requested translation id, - // we try it now with one or more fallback languages, if fallback language(s) is - // configured. - if ($uses && $fallbackLanguage && $fallbackLanguage.length) { - fallbackTranslation(translationId, interpolateParams, Interpolator, defaultTranslationText) - .then(function (translation) { - deferred.resolve(translation); - }, function (_translationId) { - deferred.reject(applyNotFoundIndicators(_translationId)); - }); - } else if ($missingTranslationHandlerFactory && !pendingLoader && missingTranslationHandlerTranslation) { - // looks like the requested translation id doesn't exists. - // Now, if there is a registered handler for missing translations and no - // asyncLoader is pending, we execute the handler - if (defaultTranslationText) { - deferred.resolve(defaultTranslationText); - } else { - deferred.resolve(missingTranslationHandlerTranslation); - } - } else { - if (defaultTranslationText) { - deferred.resolve(defaultTranslationText); - } else { - deferred.reject(applyNotFoundIndicators(translationId)); - } - } - } - return deferred.promise; - }; - - var determineTranslationInstant = function (translationId, interpolateParams, interpolationId) { - - var result, table = $uses ? $translationTable[$uses] : $translationTable, - Interpolator = defaultInterpolator; - - // if the interpolation id exists use custom interpolator - if (interpolatorHashMap && Object.prototype.hasOwnProperty.call(interpolatorHashMap, interpolationId)) { - Interpolator = interpolatorHashMap[interpolationId]; - } - - // if the translation id exists, we can just interpolate it - if (table && Object.prototype.hasOwnProperty.call(table, translationId)) { - var translation = table[translationId]; - - // If using link, rerun $translate with linked translationId and return it - if (translation.substr(0, 2) === '@:') { - result = determineTranslationInstant(translation.substr(2), interpolateParams, interpolationId); - } else { - result = Interpolator.interpolate(translation, interpolateParams); - } - } else { - var missingTranslationHandlerTranslation; - // for logging purposes only (as in $translateMissingTranslationHandlerLog), value is not returned to promise - if ($missingTranslationHandlerFactory && !pendingLoader) { - missingTranslationHandlerTranslation = translateByHandler(translationId, interpolateParams); - } - - // since we couldn't translate the inital requested translation id, - // we try it now with one or more fallback languages, if fallback language(s) is - // configured. - if ($uses && $fallbackLanguage && $fallbackLanguage.length) { - fallbackIndex = 0; - result = fallbackTranslationInstant(translationId, interpolateParams, Interpolator); - } else if ($missingTranslationHandlerFactory && !pendingLoader && missingTranslationHandlerTranslation) { - // looks like the requested translation id doesn't exists. - // Now, if there is a registered handler for missing translations and no - // asyncLoader is pending, we execute the handler - result = missingTranslationHandlerTranslation; - } else { - result = applyNotFoundIndicators(translationId); - } - } - - return result; - }; - - var clearNextLangAndPromise = function(key) { - if ($nextLang === key) { - $nextLang = undefined; - } - langPromises[key] = undefined; - }; - - /** - * @ngdoc function - * @name pascalprecht.translate.$translate#preferredLanguage - * @methodOf pascalprecht.translate.$translate - * - * @description - * Returns the language key for the preferred language. - * - * @param {string} langKey language String or Array to be used as preferredLanguage (changing at runtime) - * - * @return {string} preferred language key - */ - $translate.preferredLanguage = function (langKey) { - if(langKey) { - setupPreferredLanguage(langKey); - } - return $preferredLanguage; - }; - - /** - * @ngdoc function - * @name pascalprecht.translate.$translate#cloakClassName - * @methodOf pascalprecht.translate.$translate - * - * @description - * Returns the configured class name for `translate-cloak` directive. - * - * @return {string} cloakClassName - */ - $translate.cloakClassName = function () { - return $cloakClassName; - }; - - /** - * @ngdoc function - * @name pascalprecht.translate.$translate#fallbackLanguage - * @methodOf pascalprecht.translate.$translate - * - * @description - * Returns the language key for the fallback languages or sets a new fallback stack. - * - * @param {string=} langKey language String or Array of fallback languages to be used (to change stack at runtime) - * - * @return {string||array} fallback language key - */ - $translate.fallbackLanguage = function (langKey) { - if (langKey !== undefined && langKey !== null) { - fallbackStack(langKey); - - // as we might have an async loader initiated and a new translation language might have been defined - // we need to add the promise to the stack also. So - iterate. - if ($loaderFactory) { - if ($fallbackLanguage && $fallbackLanguage.length) { - for (var i = 0, len = $fallbackLanguage.length; i < len; i++) { - if (!langPromises[$fallbackLanguage[i]]) { - langPromises[$fallbackLanguage[i]] = loadAsync($fallbackLanguage[i]); - } - } - } - } - $translate.use($translate.use()); - } - if ($fallbackWasString) { - return $fallbackLanguage[0]; - } else { - return $fallbackLanguage; - } - - }; - - /** - * @ngdoc function - * @name pascalprecht.translate.$translate#useFallbackLanguage - * @methodOf pascalprecht.translate.$translate - * - * @description - * Sets the first key of the fallback language stack to be used for translation. - * Therefore all languages in the fallback array BEFORE this key will be skipped! - * - * @param {string=} langKey Contains the langKey the iteration shall start with. Set to false if you want to - * get back to the whole stack - */ - $translate.useFallbackLanguage = function (langKey) { - if (langKey !== undefined && langKey !== null) { - if (!langKey) { - startFallbackIteration = 0; - } else { - var langKeyPosition = indexOf($fallbackLanguage, langKey); - if (langKeyPosition > -1) { - startFallbackIteration = langKeyPosition; - } - } - - } - - }; - - /** - * @ngdoc function - * @name pascalprecht.translate.$translate#proposedLanguage - * @methodOf pascalprecht.translate.$translate - * - * @description - * Returns the language key of language that is currently loaded asynchronously. - * - * @return {string} language key - */ - $translate.proposedLanguage = function () { - return $nextLang; - }; - - /** - * @ngdoc function - * @name pascalprecht.translate.$translate#storage - * @methodOf pascalprecht.translate.$translate - * - * @description - * Returns registered storage. - * - * @return {object} Storage - */ - $translate.storage = function () { - return Storage; - }; - - /** - * @ngdoc function - * @name pascalprecht.translate.$translate#use - * @methodOf pascalprecht.translate.$translate - * - * @description - * Tells angular-translate which language to use by given language key. This method is - * used to change language at runtime. It also takes care of storing the language - * key in a configured store to let your app remember the choosed language. - * - * When trying to 'use' a language which isn't available it tries to load it - * asynchronously with registered loaders. - * - * Returns promise object with loaded language file data - * @example - * $translate.use("en_US").then(function(data){ - * $scope.text = $translate("HELLO"); - * }); - * - * @param {string} key Language key - * @return {string} Language key - */ - $translate.use = function (key) { - if (!key) { - return $uses; - } - - var deferred = $q.defer(); - - $rootScope.$emit('$translateChangeStart', {language: key}); - - // Try to get the aliased language key - var aliasedKey = negotiateLocale(key); - if (aliasedKey) { - key = aliasedKey; - } - - // if there isn't a translation table for the language we've requested, - // we load it asynchronously - if (($forceAsyncReloadEnabled || !$translationTable[key]) && $loaderFactory && !langPromises[key]) { - $nextLang = key; - langPromises[key] = loadAsync(key).then(function (translation) { - translations(translation.key, translation.table); - deferred.resolve(translation.key); - useLanguage(translation.key); - return translation; - }, function (key) { - $rootScope.$emit('$translateChangeError', {language: key}); - deferred.reject(key); - $rootScope.$emit('$translateChangeEnd', {language: key}); - return $q.reject(key); - }); - langPromises[key]['finally'](function () { - clearNextLangAndPromise(key); - }); - } else if ($nextLang === key && langPromises[key]) { - // we are already loading this asynchronously - // resolve our new deferred when the old langPromise is resolved - langPromises[key].then(function (translation) { - deferred.resolve(translation.key); - return translation; - }, function (key) { - deferred.reject(key); - return $q.reject(key); - }); - } else { - deferred.resolve(key); - useLanguage(key); - } - - return deferred.promise; - }; - - /** - * @ngdoc function - * @name pascalprecht.translate.$translate#storageKey - * @methodOf pascalprecht.translate.$translate - * - * @description - * Returns the key for the storage. - * - * @return {string} storage key - */ - $translate.storageKey = function () { - return storageKey(); - }; - - /** - * @ngdoc function - * @name pascalprecht.translate.$translate#isPostCompilingEnabled - * @methodOf pascalprecht.translate.$translate - * - * @description - * Returns whether post compiling is enabled or not - * - * @return {bool} storage key - */ - $translate.isPostCompilingEnabled = function () { - return $postCompilingEnabled; - }; - - /** - * @ngdoc function - * @name pascalprecht.translate.$translate#isForceAsyncReloadEnabled - * @methodOf pascalprecht.translate.$translate - * - * @description - * Returns whether force async reload is enabled or not - * - * @return {boolean} forceAsyncReload value - */ - $translate.isForceAsyncReloadEnabled = function () { - return $forceAsyncReloadEnabled; - }; - - /** - * @ngdoc function - * @name pascalprecht.translate.$translate#refresh - * @methodOf pascalprecht.translate.$translate - * - * @description - * Refreshes a translation table pointed by the given langKey. If langKey is not specified, - * the module will drop all existent translation tables and load new version of those which - * are currently in use. - * - * Refresh means that the module will drop target translation table and try to load it again. - * - * In case there are no loaders registered the refresh() method will throw an Error. - * - * If the module is able to refresh translation tables refresh() method will broadcast - * $translateRefreshStart and $translateRefreshEnd events. - * - * @example - * // this will drop all currently existent translation tables and reload those which are - * // currently in use - * $translate.refresh(); - * // this will refresh a translation table for the en_US language - * $translate.refresh('en_US'); - * - * @param {string} langKey A language key of the table, which has to be refreshed - * - * @return {promise} Promise, which will be resolved in case a translation tables refreshing - * process is finished successfully, and reject if not. - */ - $translate.refresh = function (langKey) { - if (!$loaderFactory) { - throw new Error('Couldn\'t refresh translation table, no loader registered!'); - } - - var deferred = $q.defer(); - - function resolve() { - deferred.resolve(); - $rootScope.$emit('$translateRefreshEnd', {language: langKey}); - } - - function reject() { - deferred.reject(); - $rootScope.$emit('$translateRefreshEnd', {language: langKey}); - } - - $rootScope.$emit('$translateRefreshStart', {language: langKey}); - - if (!langKey) { - // if there's no language key specified we refresh ALL THE THINGS! - var tables = [], loadingKeys = {}; - - // reload registered fallback languages - if ($fallbackLanguage && $fallbackLanguage.length) { - for (var i = 0, len = $fallbackLanguage.length; i < len; i++) { - tables.push(loadAsync($fallbackLanguage[i])); - loadingKeys[$fallbackLanguage[i]] = true; - } - } - - // reload currently used language - if ($uses && !loadingKeys[$uses]) { - tables.push(loadAsync($uses)); - } - - var allTranslationsLoaded = function (tableData) { - $translationTable = {}; - angular.forEach(tableData, function (data) { - translations(data.key, data.table); - }); - if ($uses) { - useLanguage($uses); - } - resolve(); - }; - allTranslationsLoaded.displayName = 'refreshPostProcessor'; - - $q.all(tables).then(allTranslationsLoaded, reject); - - } else if ($translationTable[langKey]) { - - var oneTranslationsLoaded = function (data) { - translations(data.key, data.table); - if (langKey === $uses) { - useLanguage($uses); - } - resolve(); - }; - oneTranslationsLoaded.displayName = 'refreshPostProcessor'; - - loadAsync(langKey).then(oneTranslationsLoaded, reject); - - } else { - reject(); - } - return deferred.promise; - }; - - /** - * @ngdoc function - * @name pascalprecht.translate.$translate#instant - * @methodOf pascalprecht.translate.$translate - * - * @description - * Returns a translation instantly from the internal state of loaded translation. All rules - * regarding the current language, the preferred language of even fallback languages will be - * used except any promise handling. If a language was not found, an asynchronous loading - * will be invoked in the background. - * - * @param {string|array} translationId A token which represents a translation id - * This can be optionally an array of translation ids which - * results that the function's promise returns an object where - * each key is the translation id and the value the translation. - * @param {object} interpolateParams Params - * @param {string} interpolationId The id of the interpolation to use - * - * @return {string|object} translation - */ - $translate.instant = function (translationId, interpolateParams, interpolationId) { - - // Detect undefined and null values to shorten the execution and prevent exceptions - if (translationId === null || angular.isUndefined(translationId)) { - return translationId; - } - - // Duck detection: If the first argument is an array, a bunch of translations was requested. - // The result is an object. - if (angular.isArray(translationId)) { - var results = {}; - for (var i = 0, c = translationId.length; i < c; i++) { - results[translationId[i]] = $translate.instant(translationId[i], interpolateParams, interpolationId); - } - return results; - } - - // We discarded unacceptable values. So we just need to verify if translationId is empty String - if (angular.isString(translationId) && translationId.length < 1) { - return translationId; - } - - // trim off any whitespace - if (translationId) { - translationId = trim.apply(translationId); - } - - var result, possibleLangKeys = []; - if ($preferredLanguage) { - possibleLangKeys.push($preferredLanguage); - } - if ($uses) { - possibleLangKeys.push($uses); - } - if ($fallbackLanguage && $fallbackLanguage.length) { - possibleLangKeys = possibleLangKeys.concat($fallbackLanguage); - } - for (var j = 0, d = possibleLangKeys.length; j < d; j++) { - var possibleLangKey = possibleLangKeys[j]; - if ($translationTable[possibleLangKey]) { - if (typeof $translationTable[possibleLangKey][translationId] !== 'undefined') { - result = determineTranslationInstant(translationId, interpolateParams, interpolationId); - } else if ($notFoundIndicatorLeft || $notFoundIndicatorRight) { - result = applyNotFoundIndicators(translationId); - } - } - if (typeof result !== 'undefined') { - break; - } - } - - if (!result && result !== '') { - // Return translation of default interpolator if not found anything. - result = defaultInterpolator.interpolate(translationId, interpolateParams); - if ($missingTranslationHandlerFactory && !pendingLoader) { - result = translateByHandler(translationId, interpolateParams); - } - } - - return result; - }; - - /** - * @ngdoc function - * @name pascalprecht.translate.$translate#versionInfo - * @methodOf pascalprecht.translate.$translate - * - * @description - * Returns the current version information for the angular-translate library - * - * @return {string} angular-translate version - */ - $translate.versionInfo = function () { - return version; - }; - - /** - * @ngdoc function - * @name pascalprecht.translate.$translate#loaderCache - * @methodOf pascalprecht.translate.$translate - * - * @description - * Returns the defined loaderCache. - * - * @return {boolean|string|object} current value of loaderCache - */ - $translate.loaderCache = function () { - return loaderCache; - }; - - // internal purpose only - $translate.directivePriority = function () { - return directivePriority; - }; - - // internal purpose only - $translate.statefulFilter = function () { - return statefulFilter; - }; - - if ($loaderFactory) { - - // If at least one async loader is defined and there are no - // (default) translations available we should try to load them. - if (angular.equals($translationTable, {})) { - $translate.use($translate.use()); - } - - // Also, if there are any fallback language registered, we start - // loading them asynchronously as soon as we can. - if ($fallbackLanguage && $fallbackLanguage.length) { - var processAsyncResult = function (translation) { - translations(translation.key, translation.table); - $rootScope.$emit('$translateChangeEnd', { language: translation.key }); - return translation; - }; - for (var i = 0, len = $fallbackLanguage.length; i < len; i++) { - var fallbackLanguageId = $fallbackLanguage[i]; - if ($forceAsyncReloadEnabled || !$translationTable[fallbackLanguageId]) { - langPromises[fallbackLanguageId] = loadAsync(fallbackLanguageId).then(processAsyncResult); - } - } - } - } - - return $translate; - } - ]; -} -$translate.$inject = ['$STORAGE_KEY', '$windowProvider', '$translateSanitizationProvider', 'pascalprechtTranslateOverrider']; - -$translate.displayName = 'displayName'; - -/** - * @ngdoc object - * @name pascalprecht.translate.$translateDefaultInterpolation - * @requires $interpolate - * - * @description - * Uses angular's `$interpolate` services to interpolate strings against some values. - * - * Be aware to configure a proper sanitization strategy. - * - * See also: - * * {@link pascalprecht.translate.$translateSanitization} - * - * @return {object} $translateDefaultInterpolation Interpolator service - */ -angular.module('pascalprecht.translate').factory('$translateDefaultInterpolation', $translateDefaultInterpolation); - -function $translateDefaultInterpolation ($interpolate, $translateSanitization) { - - 'use strict'; - - var $translateInterpolator = {}, - $locale, - $identifier = 'default'; - - /** - * @ngdoc function - * @name pascalprecht.translate.$translateDefaultInterpolation#setLocale - * @methodOf pascalprecht.translate.$translateDefaultInterpolation - * - * @description - * Sets current locale (this is currently not use in this interpolation). - * - * @param {string} locale Language key or locale. - */ - $translateInterpolator.setLocale = function (locale) { - $locale = locale; - }; - - /** - * @ngdoc function - * @name pascalprecht.translate.$translateDefaultInterpolation#getInterpolationIdentifier - * @methodOf pascalprecht.translate.$translateDefaultInterpolation - * - * @description - * Returns an identifier for this interpolation service. - * - * @returns {string} $identifier - */ - $translateInterpolator.getInterpolationIdentifier = function () { - return $identifier; - }; - - /** - * @deprecated will be removed in 3.0 - * @see {@link pascalprecht.translate.$translateSanitization} - */ - $translateInterpolator.useSanitizeValueStrategy = function (value) { - $translateSanitization.useStrategy(value); - return this; - }; - - /** - * @ngdoc function - * @name pascalprecht.translate.$translateDefaultInterpolation#interpolate - * @methodOf pascalprecht.translate.$translateDefaultInterpolation - * - * @description - * Interpolates given string agains given interpolate params using angulars - * `$interpolate` service. - * - * @returns {string} interpolated string. - */ - $translateInterpolator.interpolate = function (string, interpolationParams) { - interpolationParams = interpolationParams || {}; - interpolationParams = $translateSanitization.sanitize(interpolationParams, 'params'); - - var interpolatedText = $interpolate(string)(interpolationParams); - interpolatedText = $translateSanitization.sanitize(interpolatedText, 'text'); - - return interpolatedText; - }; - - return $translateInterpolator; -} -$translateDefaultInterpolation.$inject = ['$interpolate', '$translateSanitization']; - -$translateDefaultInterpolation.displayName = '$translateDefaultInterpolation'; - -angular.module('pascalprecht.translate').constant('$STORAGE_KEY', 'NG_TRANSLATE_LANG_KEY'); - -angular.module('pascalprecht.translate') -/** - * @ngdoc directive - * @name pascalprecht.translate.directive:translate - * @requires $compile - * @requires $filter - * @requires $interpolate - * @restrict A - * - * @description - * Translates given translation id either through attribute or DOM content. - * Internally it uses `translate` filter to translate translation id. It possible to - * pass an optional `translate-values` object literal as string into translation id. - * - * @param {string=} translate Translation id which could be either string or interpolated string. - * @param {string=} translate-values Values to pass into translation id. Can be passed as object literal string or interpolated object. - * @param {string=} translate-attr-ATTR translate Translation id and put it into ATTR attribute. - * @param {string=} translate-default will be used unless translation was successful - * @param {boolean=} translate-compile (default true if present) defines locally activation of {@link pascalprecht.translate.$translateProvider#methods_usePostCompiling} - * - * @example - - -
    - -
    
    -        
    TRANSLATION_ID
    -
    
    -        
    
    -        
    {{translationId}}
    -
    
    -        
    WITH_VALUES
    -
    
    -        
    WITH_VALUES
    -
    
    -
    -      
    -
    - - angular.module('ngView', ['pascalprecht.translate']) - - .config(function ($translateProvider) { - - $translateProvider.translations('en',{ - 'TRANSLATION_ID': 'Hello there!', - 'WITH_VALUES': 'The following value is dynamic: {{value}}' - }).preferredLanguage('en'); - - }); - - angular.module('ngView').controller('TranslateCtrl', function ($scope) { - $scope.translationId = 'TRANSLATION_ID'; - - $scope.values = { - value: 78 - }; - }); - - - it('should translate', function () { - inject(function ($rootScope, $compile) { - $rootScope.translationId = 'TRANSLATION_ID'; - - element = $compile('

    ')($rootScope); - $rootScope.$digest(); - expect(element.text()).toBe('Hello there!'); - - element = $compile('

    ')($rootScope); - $rootScope.$digest(); - expect(element.text()).toBe('Hello there!'); - - element = $compile('

    TRANSLATION_ID

    ')($rootScope); - $rootScope.$digest(); - expect(element.text()).toBe('Hello there!'); - - element = $compile('

    {{translationId}}

    ')($rootScope); - $rootScope.$digest(); - expect(element.text()).toBe('Hello there!'); - - element = $compile('

    ')($rootScope); - $rootScope.$digest(); - expect(element.attr('title')).toBe('Hello there!'); - }); - }); -
    -
    - */ -.directive('translate', translateDirective); -function translateDirective($translate, $q, $interpolate, $compile, $parse, $rootScope) { - - 'use strict'; - - /** - * @name trim - * @private - * - * @description - * trim polyfill - * - * @returns {string} The string stripped of whitespace from both ends - */ - var trim = function() { - return this.toString().replace(/^\s+|\s+$/g, ''); - }; - - return { - restrict: 'AE', - scope: true, - priority: $translate.directivePriority(), - compile: function (tElement, tAttr) { - - var translateValuesExist = (tAttr.translateValues) ? - tAttr.translateValues : undefined; - - var translateInterpolation = (tAttr.translateInterpolation) ? - tAttr.translateInterpolation : undefined; - - var translateValueExist = tElement[0].outerHTML.match(/translate-value-+/i); - - var interpolateRegExp = '^(.*)(' + $interpolate.startSymbol() + '.*' + $interpolate.endSymbol() + ')(.*)', - watcherRegExp = '^(.*)' + $interpolate.startSymbol() + '(.*)' + $interpolate.endSymbol() + '(.*)'; - - return function linkFn(scope, iElement, iAttr) { - - scope.interpolateParams = {}; - scope.preText = ''; - scope.postText = ''; - var translationIds = {}; - - var initInterpolationParams = function (interpolateParams, iAttr, tAttr) { - // initial setup - if (iAttr.translateValues) { - angular.extend(interpolateParams, $parse(iAttr.translateValues)(scope.$parent)); - } - // initially fetch all attributes if existing and fill the params - if (translateValueExist) { - for (var attr in tAttr) { - if (Object.prototype.hasOwnProperty.call(iAttr, attr) && attr.substr(0, 14) === 'translateValue' && attr !== 'translateValues') { - var attributeName = angular.lowercase(attr.substr(14, 1)) + attr.substr(15); - interpolateParams[attributeName] = tAttr[attr]; - } - } - } - }; - - // Ensures any change of the attribute "translate" containing the id will - // be re-stored to the scope's "translationId". - // If the attribute has no content, the element's text value (white spaces trimmed off) will be used. - var observeElementTranslation = function (translationId) { - - // Remove any old watcher - if (angular.isFunction(observeElementTranslation._unwatchOld)) { - observeElementTranslation._unwatchOld(); - observeElementTranslation._unwatchOld = undefined; - } - - if (angular.equals(translationId , '') || !angular.isDefined(translationId)) { - // Resolve translation id by inner html if required - var interpolateMatches = trim.apply(iElement.text()).match(interpolateRegExp); - // Interpolate translation id if required - if (angular.isArray(interpolateMatches)) { - scope.preText = interpolateMatches[1]; - scope.postText = interpolateMatches[3]; - translationIds.translate = $interpolate(interpolateMatches[2])(scope.$parent); - var watcherMatches = iElement.text().match(watcherRegExp); - if (angular.isArray(watcherMatches) && watcherMatches[2] && watcherMatches[2].length) { - observeElementTranslation._unwatchOld = scope.$watch(watcherMatches[2], function (newValue) { - translationIds.translate = newValue; - updateTranslations(); - }); - } - } else { - translationIds.translate = iElement.text().replace(/^\s+|\s+$/g,''); - } - } else { - translationIds.translate = translationId; - } - updateTranslations(); - }; - - var observeAttributeTranslation = function (translateAttr) { - iAttr.$observe(translateAttr, function (translationId) { - translationIds[translateAttr] = translationId; - updateTranslations(); - }); - }; - - // initial setup with values - initInterpolationParams(scope.interpolateParams, iAttr, tAttr); - - var firstAttributeChangedEvent = true; - iAttr.$observe('translate', function (translationId) { - if (typeof translationId === 'undefined') { - // case of element "xyz" - observeElementTranslation(''); - } else { - // case of regular attribute - if (translationId !== '' || !firstAttributeChangedEvent) { - translationIds.translate = translationId; - updateTranslations(); - } - } - firstAttributeChangedEvent = false; - }); - - for (var translateAttr in iAttr) { - if (iAttr.hasOwnProperty(translateAttr) && translateAttr.substr(0, 13) === 'translateAttr') { - observeAttributeTranslation(translateAttr); - } - } - - iAttr.$observe('translateDefault', function (value) { - scope.defaultText = value; - }); - - if (translateValuesExist) { - iAttr.$observe('translateValues', function (interpolateParams) { - if (interpolateParams) { - scope.$parent.$watch(function () { - angular.extend(scope.interpolateParams, $parse(interpolateParams)(scope.$parent)); - }); - } - }); - } - - if (translateValueExist) { - var observeValueAttribute = function (attrName) { - iAttr.$observe(attrName, function (value) { - var attributeName = angular.lowercase(attrName.substr(14, 1)) + attrName.substr(15); - scope.interpolateParams[attributeName] = value; - }); - }; - for (var attr in iAttr) { - if (Object.prototype.hasOwnProperty.call(iAttr, attr) && attr.substr(0, 14) === 'translateValue' && attr !== 'translateValues') { - observeValueAttribute(attr); - } - } - } - - // Master update function - var updateTranslations = function () { - for (var key in translationIds) { - - if (translationIds.hasOwnProperty(key) && translationIds[key] !== undefined) { - updateTranslation(key, translationIds[key], scope, scope.interpolateParams, scope.defaultText); - } - } - }; - - // Put translation processing function outside loop - var updateTranslation = function(translateAttr, translationId, scope, interpolateParams, defaultTranslationText) { - if (translationId) { - $translate(translationId, interpolateParams, translateInterpolation, defaultTranslationText) - .then(function (translation) { - applyTranslation(translation, scope, true, translateAttr); - }, function (translationId) { - applyTranslation(translationId, scope, false, translateAttr); - }); - } else { - // as an empty string cannot be translated, we can solve this using successful=false - applyTranslation(translationId, scope, false, translateAttr); - } - }; - - var applyTranslation = function (value, scope, successful, translateAttr) { - if (translateAttr === 'translate') { - // default translate into innerHTML - if (!successful && typeof scope.defaultText !== 'undefined') { - value = scope.defaultText; - } - iElement.html(scope.preText + value + scope.postText); - var globallyEnabled = $translate.isPostCompilingEnabled(); - var locallyDefined = typeof tAttr.translateCompile !== 'undefined'; - var locallyEnabled = locallyDefined && tAttr.translateCompile !== 'false'; - if ((globallyEnabled && !locallyDefined) || locallyEnabled) { - $compile(iElement.contents())(scope); - } - } else { - // translate attribute - if (!successful && typeof scope.defaultText !== 'undefined') { - value = scope.defaultText; - } - var attributeName = iAttr.$attr[translateAttr]; - if (attributeName.substr(0, 5) === 'data-') { - // ensure html5 data prefix is stripped - attributeName = attributeName.substr(5); - } - attributeName = attributeName.substr(15); - iElement.attr(attributeName, value); - } - }; - - if (translateValuesExist || translateValueExist || iAttr.translateDefault) { - scope.$watch('interpolateParams', updateTranslations, true); - } - - // Ensures the text will be refreshed after the current language was changed - // w/ $translate.use(...) - var unbind = $rootScope.$on('$translateChangeSuccess', updateTranslations); - - // ensure translation will be looked up at least one - if (iElement.text().length) { - if (iAttr.translate) { - observeElementTranslation(iAttr.translate); - } else { - observeElementTranslation(''); - } - } else if (iAttr.translate) { - // ensure attribute will be not skipped - observeElementTranslation(iAttr.translate); - } - updateTranslations(); - scope.$on('$destroy', unbind); - }; - } - }; -} -translateDirective.$inject = ['$translate', '$q', '$interpolate', '$compile', '$parse', '$rootScope']; - -translateDirective.displayName = 'translateDirective'; - -angular.module('pascalprecht.translate') -/** - * @ngdoc directive - * @name pascalprecht.translate.directive:translateCloak - * @requires $rootScope - * @requires $translate - * @restrict A - * - * $description - * Adds a `translate-cloak` class name to the given element where this directive - * is applied initially and removes it, once a loader has finished loading. - * - * This directive can be used to prevent initial flickering when loading translation - * data asynchronously. - * - * The class name is defined in - * {@link pascalprecht.translate.$translateProvider#cloakClassName $translate.cloakClassName()}. - * - * @param {string=} translate-cloak If a translationId is provided, it will be used for showing - * or hiding the cloak. Basically it relies on the translation - * resolve. - */ -.directive('translateCloak', translateCloakDirective); - -function translateCloakDirective($rootScope, $translate) { - - 'use strict'; - - return { - compile: function (tElement) { - var applyCloak = function () { - tElement.addClass($translate.cloakClassName()); - }, - removeCloak = function () { - tElement.removeClass($translate.cloakClassName()); - }, - removeListener = $rootScope.$on('$translateChangeEnd', function () { - removeCloak(); - removeListener(); - removeListener = null; - }); - applyCloak(); - - return function linkFn(scope, iElement, iAttr) { - // Register a watcher for the defined translation allowing a fine tuned cloak - if (iAttr.translateCloak && iAttr.translateCloak.length) { - iAttr.$observe('translateCloak', function (translationId) { - $translate(translationId).then(removeCloak, applyCloak); - }); - } - }; - } - }; -} -translateCloakDirective.$inject = ['$rootScope', '$translate']; - -translateCloakDirective.displayName = 'translateCloakDirective'; - -angular.module('pascalprecht.translate') -/** - * @ngdoc filter - * @name pascalprecht.translate.filter:translate - * @requires $parse - * @requires pascalprecht.translate.$translate - * @function - * - * @description - * Uses `$translate` service to translate contents. Accepts interpolate parameters - * to pass dynamized values though translation. - * - * @param {string} translationId A translation id to be translated. - * @param {*=} interpolateParams Optional object literal (as hash or string) to pass values into translation. - * - * @returns {string} Translated text. - * - * @example - - -
    - -
    {{ 'TRANSLATION_ID' | translate }}
    -
    {{ translationId | translate }}
    -
    {{ 'WITH_VALUES' | translate:'{value: 5}' }}
    -
    {{ 'WITH_VALUES' | translate:values }}
    - -
    -
    - - angular.module('ngView', ['pascalprecht.translate']) - - .config(function ($translateProvider) { - - $translateProvider.translations('en', { - 'TRANSLATION_ID': 'Hello there!', - 'WITH_VALUES': 'The following value is dynamic: {{value}}' - }); - $translateProvider.preferredLanguage('en'); - - }); - - angular.module('ngView').controller('TranslateCtrl', function ($scope) { - $scope.translationId = 'TRANSLATION_ID'; - - $scope.values = { - value: 78 - }; - }); - -
    - */ -.filter('translate', translateFilterFactory); - -function translateFilterFactory($parse, $translate) { - - 'use strict'; - - var translateFilter = function (translationId, interpolateParams, interpolation) { - - if (!angular.isObject(interpolateParams)) { - interpolateParams = $parse(interpolateParams)(this); - } - - return $translate.instant(translationId, interpolateParams, interpolation); - }; - - if ($translate.statefulFilter()) { - translateFilter.$stateful = true; - } - - return translateFilter; -} -translateFilterFactory.$inject = ['$parse', '$translate']; - -translateFilterFactory.displayName = 'translateFilterFactory'; - -angular.module('pascalprecht.translate') - -/** - * @ngdoc object - * @name pascalprecht.translate.$translationCache - * @requires $cacheFactory - * - * @description - * The first time a translation table is used, it is loaded in the translation cache for quick retrieval. You - * can load translation tables directly into the cache by consuming the - * `$translationCache` service directly. - * - * @return {object} $cacheFactory object. - */ - .factory('$translationCache', $translationCache); - -function $translationCache($cacheFactory) { - - 'use strict'; - - return $cacheFactory('translations'); -} -$translationCache.$inject = ['$cacheFactory']; - -$translationCache.displayName = '$translationCache'; -return 'pascalprecht.translate'; - -})); diff --git a/themes/src/main/resources/theme/keycloak/common/resources/lib/angular/angular.js b/themes/src/main/resources/theme/keycloak/common/resources/lib/angular/angular.js deleted file mode 100644 index d8b681dd3e..0000000000 --- a/themes/src/main/resources/theme/keycloak/common/resources/lib/angular/angular.js +++ /dev/null @@ -1,28604 +0,0 @@ -/** - * @license AngularJS v1.4.4 - * (c) 2010-2015 Google, Inc. http://angularjs.org - * License: MIT - */ -(function(window, document, undefined) {'use strict'; - -/** - * @description - * - * This object provides a utility for producing rich Error messages within - * Angular. It can be called as follows: - * - * var exampleMinErr = minErr('example'); - * throw exampleMinErr('one', 'This {0} is {1}', foo, bar); - * - * The above creates an instance of minErr in the example namespace. The - * resulting error will have a namespaced error code of example.one. The - * resulting error will replace {0} with the value of foo, and {1} with the - * value of bar. The object is not restricted in the number of arguments it can - * take. - * - * If fewer arguments are specified than necessary for interpolation, the extra - * interpolation markers will be preserved in the final string. - * - * Since data will be parsed statically during a build step, some restrictions - * are applied with respect to how minErr instances are created and called. - * Instances should have names of the form namespaceMinErr for a minErr created - * using minErr('namespace') . Error codes, namespaces and template strings - * should all be static strings, not variables or general expressions. - * - * @param {string} module The namespace to use for the new minErr instance. - * @param {function} ErrorConstructor Custom error constructor to be instantiated when returning - * error from returned function, for cases when a particular type of error is useful. - * @returns {function(code:string, template:string, ...templateArgs): Error} minErr instance - */ - -function minErr(module, ErrorConstructor) { - ErrorConstructor = ErrorConstructor || Error; - return function() { - var SKIP_INDEXES = 2; - - var templateArgs = arguments, - code = templateArgs[0], - message = '[' + (module ? module + ':' : '') + code + '] ', - template = templateArgs[1], - paramPrefix, i; - - message += template.replace(/\{\d+\}/g, function(match) { - var index = +match.slice(1, -1), - shiftedIndex = index + SKIP_INDEXES; - - if (shiftedIndex < templateArgs.length) { - return toDebugString(templateArgs[shiftedIndex]); - } - - return match; - }); - - message += '\nhttp://errors.angularjs.org/1.4.4/' + - (module ? module + '/' : '') + code; - - for (i = SKIP_INDEXES, paramPrefix = '?'; i < templateArgs.length; i++, paramPrefix = '&') { - message += paramPrefix + 'p' + (i - SKIP_INDEXES) + '=' + - encodeURIComponent(toDebugString(templateArgs[i])); - } - - return new ErrorConstructor(message); - }; -} - -/* We need to tell jshint what variables are being exported */ -/* global angular: true, - msie: true, - jqLite: true, - jQuery: true, - slice: true, - splice: true, - push: true, - toString: true, - ngMinErr: true, - angularModule: true, - uid: true, - REGEX_STRING_REGEXP: true, - VALIDITY_STATE_PROPERTY: true, - - lowercase: true, - uppercase: true, - manualLowercase: true, - manualUppercase: true, - nodeName_: true, - isArrayLike: true, - forEach: true, - forEachSorted: true, - reverseParams: true, - nextUid: true, - setHashKey: true, - extend: true, - toInt: true, - inherit: true, - merge: true, - noop: true, - identity: true, - valueFn: true, - isUndefined: true, - isDefined: true, - isObject: true, - isBlankObject: true, - isString: true, - isNumber: true, - isDate: true, - isArray: true, - isFunction: true, - isRegExp: true, - isWindow: true, - isScope: true, - isFile: true, - isFormData: true, - isBlob: true, - isBoolean: true, - isPromiseLike: true, - trim: true, - escapeForRegexp: true, - isElement: true, - makeMap: true, - includes: true, - arrayRemove: true, - copy: true, - shallowCopy: true, - equals: true, - csp: true, - jq: true, - concat: true, - sliceArgs: true, - bind: true, - toJsonReplacer: true, - toJson: true, - fromJson: true, - convertTimezoneToLocal: true, - timezoneToOffset: true, - startingTag: true, - tryDecodeURIComponent: true, - parseKeyValue: true, - toKeyValue: true, - encodeUriSegment: true, - encodeUriQuery: true, - angularInit: true, - bootstrap: true, - getTestability: true, - snake_case: true, - bindJQuery: true, - assertArg: true, - assertArgFn: true, - assertNotHasOwnProperty: true, - getter: true, - getBlockNodes: true, - hasOwnProperty: true, - createMap: true, - - NODE_TYPE_ELEMENT: true, - NODE_TYPE_ATTRIBUTE: true, - NODE_TYPE_TEXT: true, - NODE_TYPE_COMMENT: true, - NODE_TYPE_DOCUMENT: true, - NODE_TYPE_DOCUMENT_FRAGMENT: true, -*/ - -//////////////////////////////////// - -/** - * @ngdoc module - * @name ng - * @module ng - * @description - * - * # ng (core module) - * The ng module is loaded by default when an AngularJS application is started. The module itself - * contains the essential components for an AngularJS application to function. The table below - * lists a high level breakdown of each of the services/factories, filters, directives and testing - * components available within this core module. - * - *
    - */ - -var REGEX_STRING_REGEXP = /^\/(.+)\/([a-z]*)$/; - -// The name of a form control's ValidityState property. -// This is used so that it's possible for internal tests to create mock ValidityStates. -var VALIDITY_STATE_PROPERTY = 'validity'; - -/** - * @ngdoc function - * @name angular.lowercase - * @module ng - * @kind function - * - * @description Converts the specified string to lowercase. - * @param {string} string String to be converted to lowercase. - * @returns {string} Lowercased string. - */ -var lowercase = function(string) {return isString(string) ? string.toLowerCase() : string;}; -var hasOwnProperty = Object.prototype.hasOwnProperty; - -/** - * @ngdoc function - * @name angular.uppercase - * @module ng - * @kind function - * - * @description Converts the specified string to uppercase. - * @param {string} string String to be converted to uppercase. - * @returns {string} Uppercased string. - */ -var uppercase = function(string) {return isString(string) ? string.toUpperCase() : string;}; - - -var manualLowercase = function(s) { - /* jshint bitwise: false */ - return isString(s) - ? s.replace(/[A-Z]/g, function(ch) {return String.fromCharCode(ch.charCodeAt(0) | 32);}) - : s; -}; -var manualUppercase = function(s) { - /* jshint bitwise: false */ - return isString(s) - ? s.replace(/[a-z]/g, function(ch) {return String.fromCharCode(ch.charCodeAt(0) & ~32);}) - : s; -}; - - -// String#toLowerCase and String#toUpperCase don't produce correct results in browsers with Turkish -// locale, for this reason we need to detect this case and redefine lowercase/uppercase methods -// with correct but slower alternatives. -if ('i' !== 'I'.toLowerCase()) { - lowercase = manualLowercase; - uppercase = manualUppercase; -} - - -var - msie, // holds major version number for IE, or NaN if UA is not IE. - jqLite, // delay binding since jQuery could be loaded after us. - jQuery, // delay binding - slice = [].slice, - splice = [].splice, - push = [].push, - toString = Object.prototype.toString, - getPrototypeOf = Object.getPrototypeOf, - ngMinErr = minErr('ng'), - - /** @name angular */ - angular = window.angular || (window.angular = {}), - angularModule, - uid = 0; - -/** - * documentMode is an IE-only property - * http://msdn.microsoft.com/en-us/library/ie/cc196988(v=vs.85).aspx - */ -msie = document.documentMode; - - -/** - * @private - * @param {*} obj - * @return {boolean} Returns true if `obj` is an array or array-like object (NodeList, Arguments, - * String ...) - */ -function isArrayLike(obj) { - if (obj == null || isWindow(obj)) { - return false; - } - - // Support: iOS 8.2 (not reproducible in simulator) - // "length" in obj used to prevent JIT error (gh-11508) - var length = "length" in Object(obj) && obj.length; - - if (obj.nodeType === NODE_TYPE_ELEMENT && length) { - return true; - } - - return isString(obj) || isArray(obj) || length === 0 || - typeof length === 'number' && length > 0 && (length - 1) in obj; -} - -/** - * @ngdoc function - * @name angular.forEach - * @module ng - * @kind function - * - * @description - * Invokes the `iterator` function once for each item in `obj` collection, which can be either an - * object or an array. The `iterator` function is invoked with `iterator(value, key, obj)`, where `value` - * is the value of an object property or an array element, `key` is the object property key or - * array element index and obj is the `obj` itself. Specifying a `context` for the function is optional. - * - * It is worth noting that `.forEach` does not iterate over inherited properties because it filters - * using the `hasOwnProperty` method. - * - * Unlike ES262's - * [Array.prototype.forEach](http://www.ecma-international.org/ecma-262/5.1/#sec-15.4.4.18), - * Providing 'undefined' or 'null' values for `obj` will not throw a TypeError, but rather just - * return the value provided. - * - ```js - var values = {name: 'misko', gender: 'male'}; - var log = []; - angular.forEach(values, function(value, key) { - this.push(key + ': ' + value); - }, log); - expect(log).toEqual(['name: misko', 'gender: male']); - ``` - * - * @param {Object|Array} obj Object to iterate over. - * @param {Function} iterator Iterator function. - * @param {Object=} context Object to become context (`this`) for the iterator function. - * @returns {Object|Array} Reference to `obj`. - */ - -function forEach(obj, iterator, context) { - var key, length; - if (obj) { - if (isFunction(obj)) { - for (key in obj) { - // Need to check if hasOwnProperty exists, - // as on IE8 the result of querySelectorAll is an object without a hasOwnProperty function - if (key != 'prototype' && key != 'length' && key != 'name' && (!obj.hasOwnProperty || obj.hasOwnProperty(key))) { - iterator.call(context, obj[key], key, obj); - } - } - } else if (isArray(obj) || isArrayLike(obj)) { - var isPrimitive = typeof obj !== 'object'; - for (key = 0, length = obj.length; key < length; key++) { - if (isPrimitive || key in obj) { - iterator.call(context, obj[key], key, obj); - } - } - } else if (obj.forEach && obj.forEach !== forEach) { - obj.forEach(iterator, context, obj); - } else if (isBlankObject(obj)) { - // createMap() fast path --- Safe to avoid hasOwnProperty check because prototype chain is empty - for (key in obj) { - iterator.call(context, obj[key], key, obj); - } - } else if (typeof obj.hasOwnProperty === 'function') { - // Slow path for objects inheriting Object.prototype, hasOwnProperty check needed - for (key in obj) { - if (obj.hasOwnProperty(key)) { - iterator.call(context, obj[key], key, obj); - } - } - } else { - // Slow path for objects which do not have a method `hasOwnProperty` - for (key in obj) { - if (hasOwnProperty.call(obj, key)) { - iterator.call(context, obj[key], key, obj); - } - } - } - } - return obj; -} - -function forEachSorted(obj, iterator, context) { - var keys = Object.keys(obj).sort(); - for (var i = 0; i < keys.length; i++) { - iterator.call(context, obj[keys[i]], keys[i]); - } - return keys; -} - - -/** - * when using forEach the params are value, key, but it is often useful to have key, value. - * @param {function(string, *)} iteratorFn - * @returns {function(*, string)} - */ -function reverseParams(iteratorFn) { - return function(value, key) { iteratorFn(key, value); }; -} - -/** - * A consistent way of creating unique IDs in angular. - * - * Using simple numbers allows us to generate 28.6 million unique ids per second for 10 years before - * we hit number precision issues in JavaScript. - * - * Math.pow(2,53) / 60 / 60 / 24 / 365 / 10 = 28.6M - * - * @returns {number} an unique alpha-numeric string - */ -function nextUid() { - return ++uid; -} - - -/** - * Set or clear the hashkey for an object. - * @param obj object - * @param h the hashkey (!truthy to delete the hashkey) - */ -function setHashKey(obj, h) { - if (h) { - obj.$$hashKey = h; - } else { - delete obj.$$hashKey; - } -} - - -function baseExtend(dst, objs, deep) { - var h = dst.$$hashKey; - - for (var i = 0, ii = objs.length; i < ii; ++i) { - var obj = objs[i]; - if (!isObject(obj) && !isFunction(obj)) continue; - var keys = Object.keys(obj); - for (var j = 0, jj = keys.length; j < jj; j++) { - var key = keys[j]; - var src = obj[key]; - - if (deep && isObject(src)) { - if (isDate(src)) { - dst[key] = new Date(src.valueOf()); - } else if (isRegExp(src)) { - dst[key] = new RegExp(src); - } else { - if (!isObject(dst[key])) dst[key] = isArray(src) ? [] : {}; - baseExtend(dst[key], [src], true); - } - } else { - dst[key] = src; - } - } - } - - setHashKey(dst, h); - return dst; -} - -/** - * @ngdoc function - * @name angular.extend - * @module ng - * @kind function - * - * @description - * Extends the destination object `dst` by copying own enumerable properties from the `src` object(s) - * to `dst`. You can specify multiple `src` objects. If you want to preserve original objects, you can do so - * by passing an empty object as the target: `var object = angular.extend({}, object1, object2)`. - * - * **Note:** Keep in mind that `angular.extend` does not support recursive merge (deep copy). Use - * {@link angular.merge} for this. - * - * @param {Object} dst Destination object. - * @param {...Object} src Source object(s). - * @returns {Object} Reference to `dst`. - */ -function extend(dst) { - return baseExtend(dst, slice.call(arguments, 1), false); -} - - -/** -* @ngdoc function -* @name angular.merge -* @module ng -* @kind function -* -* @description -* Deeply extends the destination object `dst` by copying own enumerable properties from the `src` object(s) -* to `dst`. You can specify multiple `src` objects. If you want to preserve original objects, you can do so -* by passing an empty object as the target: `var object = angular.merge({}, object1, object2)`. -* -* Unlike {@link angular.extend extend()}, `merge()` recursively descends into object properties of source -* objects, performing a deep copy. -* -* @param {Object} dst Destination object. -* @param {...Object} src Source object(s). -* @returns {Object} Reference to `dst`. -*/ -function merge(dst) { - return baseExtend(dst, slice.call(arguments, 1), true); -} - - - -function toInt(str) { - return parseInt(str, 10); -} - - -function inherit(parent, extra) { - return extend(Object.create(parent), extra); -} - -/** - * @ngdoc function - * @name angular.noop - * @module ng - * @kind function - * - * @description - * A function that performs no operations. This function can be useful when writing code in the - * functional style. - ```js - function foo(callback) { - var result = calculateResult(); - (callback || angular.noop)(result); - } - ``` - */ -function noop() {} -noop.$inject = []; - - -/** - * @ngdoc function - * @name angular.identity - * @module ng - * @kind function - * - * @description - * A function that returns its first argument. This function is useful when writing code in the - * functional style. - * - ```js - function transformer(transformationFn, value) { - return (transformationFn || angular.identity)(value); - }; - ``` - * @param {*} value to be returned. - * @returns {*} the value passed in. - */ -function identity($) {return $;} -identity.$inject = []; - - -function valueFn(value) {return function() {return value;};} - -function hasCustomToString(obj) { - return isFunction(obj.toString) && obj.toString !== Object.prototype.toString; -} - - -/** - * @ngdoc function - * @name angular.isUndefined - * @module ng - * @kind function - * - * @description - * Determines if a reference is undefined. - * - * @param {*} value Reference to check. - * @returns {boolean} True if `value` is undefined. - */ -function isUndefined(value) {return typeof value === 'undefined';} - - -/** - * @ngdoc function - * @name angular.isDefined - * @module ng - * @kind function - * - * @description - * Determines if a reference is defined. - * - * @param {*} value Reference to check. - * @returns {boolean} True if `value` is defined. - */ -function isDefined(value) {return typeof value !== 'undefined';} - - -/** - * @ngdoc function - * @name angular.isObject - * @module ng - * @kind function - * - * @description - * Determines if a reference is an `Object`. Unlike `typeof` in JavaScript, `null`s are not - * considered to be objects. Note that JavaScript arrays are objects. - * - * @param {*} value Reference to check. - * @returns {boolean} True if `value` is an `Object` but not `null`. - */ -function isObject(value) { - // http://jsperf.com/isobject4 - return value !== null && typeof value === 'object'; -} - - -/** - * Determine if a value is an object with a null prototype - * - * @returns {boolean} True if `value` is an `Object` with a null prototype - */ -function isBlankObject(value) { - return value !== null && typeof value === 'object' && !getPrototypeOf(value); -} - - -/** - * @ngdoc function - * @name angular.isString - * @module ng - * @kind function - * - * @description - * Determines if a reference is a `String`. - * - * @param {*} value Reference to check. - * @returns {boolean} True if `value` is a `String`. - */ -function isString(value) {return typeof value === 'string';} - - -/** - * @ngdoc function - * @name angular.isNumber - * @module ng - * @kind function - * - * @description - * Determines if a reference is a `Number`. - * - * This includes the "special" numbers `NaN`, `+Infinity` and `-Infinity`. - * - * If you wish to exclude these then you can use the native - * [`isFinite'](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/isFinite) - * method. - * - * @param {*} value Reference to check. - * @returns {boolean} True if `value` is a `Number`. - */ -function isNumber(value) {return typeof value === 'number';} - - -/** - * @ngdoc function - * @name angular.isDate - * @module ng - * @kind function - * - * @description - * Determines if a value is a date. - * - * @param {*} value Reference to check. - * @returns {boolean} True if `value` is a `Date`. - */ -function isDate(value) { - return toString.call(value) === '[object Date]'; -} - - -/** - * @ngdoc function - * @name angular.isArray - * @module ng - * @kind function - * - * @description - * Determines if a reference is an `Array`. - * - * @param {*} value Reference to check. - * @returns {boolean} True if `value` is an `Array`. - */ -var isArray = Array.isArray; - -/** - * @ngdoc function - * @name angular.isFunction - * @module ng - * @kind function - * - * @description - * Determines if a reference is a `Function`. - * - * @param {*} value Reference to check. - * @returns {boolean} True if `value` is a `Function`. - */ -function isFunction(value) {return typeof value === 'function';} - - -/** - * Determines if a value is a regular expression object. - * - * @private - * @param {*} value Reference to check. - * @returns {boolean} True if `value` is a `RegExp`. - */ -function isRegExp(value) { - return toString.call(value) === '[object RegExp]'; -} - - -/** - * Checks if `obj` is a window object. - * - * @private - * @param {*} obj Object to check - * @returns {boolean} True if `obj` is a window obj. - */ -function isWindow(obj) { - return obj && obj.window === obj; -} - - -function isScope(obj) { - return obj && obj.$evalAsync && obj.$watch; -} - - -function isFile(obj) { - return toString.call(obj) === '[object File]'; -} - - -function isFormData(obj) { - return toString.call(obj) === '[object FormData]'; -} - - -function isBlob(obj) { - return toString.call(obj) === '[object Blob]'; -} - - -function isBoolean(value) { - return typeof value === 'boolean'; -} - - -function isPromiseLike(obj) { - return obj && isFunction(obj.then); -} - - -var TYPED_ARRAY_REGEXP = /^\[object (Uint8(Clamped)?)|(Uint16)|(Uint32)|(Int8)|(Int16)|(Int32)|(Float(32)|(64))Array\]$/; -function isTypedArray(value) { - return TYPED_ARRAY_REGEXP.test(toString.call(value)); -} - - -var trim = function(value) { - return isString(value) ? value.trim() : value; -}; - -// Copied from: -// http://docs.closure-library.googlecode.com/git/local_closure_goog_string_string.js.source.html#line1021 -// Prereq: s is a string. -var escapeForRegexp = function(s) { - return s.replace(/([-()\[\]{}+?*.$\^|,:#= 0) { - array.splice(index, 1); - } - return index; -} - -/** - * @ngdoc function - * @name angular.copy - * @module ng - * @kind function - * - * @description - * Creates a deep copy of `source`, which should be an object or an array. - * - * * If no destination is supplied, a copy of the object or array is created. - * * If a destination is provided, all of its elements (for arrays) or properties (for objects) - * are deleted and then all elements/properties from the source are copied to it. - * * If `source` is not an object or array (inc. `null` and `undefined`), `source` is returned. - * * If `source` is identical to 'destination' an exception will be thrown. - * - * @param {*} source The source that will be used to make a copy. - * Can be any type, including primitives, `null`, and `undefined`. - * @param {(Object|Array)=} destination Destination into which the source is copied. If - * provided, must be of the same type as `source`. - * @returns {*} The copy or updated `destination`, if `destination` was specified. - * - * @example - - -
    -
    - Name:
    - E-mail:
    - Gender: male - female
    - - -
    -
    form = {{user | json}}
    -
    master = {{master | json}}
    -
    - - -
    -
    - */ -function copy(source, destination, stackSource, stackDest) { - if (isWindow(source) || isScope(source)) { - throw ngMinErr('cpws', - "Can't copy! Making copies of Window or Scope instances is not supported."); - } - if (isTypedArray(destination)) { - throw ngMinErr('cpta', - "Can't copy! TypedArray destination cannot be mutated."); - } - - if (!destination) { - destination = source; - if (isObject(source)) { - var index; - if (stackSource && (index = stackSource.indexOf(source)) !== -1) { - return stackDest[index]; - } - - // TypedArray, Date and RegExp have specific copy functionality and must be - // pushed onto the stack before returning. - // Array and other objects create the base object and recurse to copy child - // objects. The array/object will be pushed onto the stack when recursed. - if (isArray(source)) { - return copy(source, [], stackSource, stackDest); - } else if (isTypedArray(source)) { - destination = new source.constructor(source); - } else if (isDate(source)) { - destination = new Date(source.getTime()); - } else if (isRegExp(source)) { - destination = new RegExp(source.source, source.toString().match(/[^\/]*$/)[0]); - destination.lastIndex = source.lastIndex; - } else { - var emptyObject = Object.create(getPrototypeOf(source)); - return copy(source, emptyObject, stackSource, stackDest); - } - - if (stackDest) { - stackSource.push(source); - stackDest.push(destination); - } - } - } else { - if (source === destination) throw ngMinErr('cpi', - "Can't copy! Source and destination are identical."); - - stackSource = stackSource || []; - stackDest = stackDest || []; - - if (isObject(source)) { - stackSource.push(source); - stackDest.push(destination); - } - - var result, key; - if (isArray(source)) { - destination.length = 0; - for (var i = 0; i < source.length; i++) { - destination.push(copy(source[i], null, stackSource, stackDest)); - } - } else { - var h = destination.$$hashKey; - if (isArray(destination)) { - destination.length = 0; - } else { - forEach(destination, function(value, key) { - delete destination[key]; - }); - } - if (isBlankObject(source)) { - // createMap() fast path --- Safe to avoid hasOwnProperty check because prototype chain is empty - for (key in source) { - destination[key] = copy(source[key], null, stackSource, stackDest); - } - } else if (source && typeof source.hasOwnProperty === 'function') { - // Slow path, which must rely on hasOwnProperty - for (key in source) { - if (source.hasOwnProperty(key)) { - destination[key] = copy(source[key], null, stackSource, stackDest); - } - } - } else { - // Slowest path --- hasOwnProperty can't be called as a method - for (key in source) { - if (hasOwnProperty.call(source, key)) { - destination[key] = copy(source[key], null, stackSource, stackDest); - } - } - } - setHashKey(destination,h); - } - } - return destination; -} - -/** - * Creates a shallow copy of an object, an array or a primitive. - * - * Assumes that there are no proto properties for objects. - */ -function shallowCopy(src, dst) { - if (isArray(src)) { - dst = dst || []; - - for (var i = 0, ii = src.length; i < ii; i++) { - dst[i] = src[i]; - } - } else if (isObject(src)) { - dst = dst || {}; - - for (var key in src) { - if (!(key.charAt(0) === '$' && key.charAt(1) === '$')) { - dst[key] = src[key]; - } - } - } - - return dst || src; -} - - -/** - * @ngdoc function - * @name angular.equals - * @module ng - * @kind function - * - * @description - * Determines if two objects or two values are equivalent. Supports value types, regular - * expressions, arrays and objects. - * - * Two objects or values are considered equivalent if at least one of the following is true: - * - * * Both objects or values pass `===` comparison. - * * Both objects or values are of the same type and all of their properties are equal by - * comparing them with `angular.equals`. - * * Both values are NaN. (In JavaScript, NaN == NaN => false. But we consider two NaN as equal) - * * Both values represent the same regular expression (In JavaScript, - * /abc/ == /abc/ => false. But we consider two regular expressions as equal when their textual - * representation matches). - * - * During a property comparison, properties of `function` type and properties with names - * that begin with `$` are ignored. - * - * Scope and DOMWindow objects are being compared only by identify (`===`). - * - * @param {*} o1 Object or value to compare. - * @param {*} o2 Object or value to compare. - * @returns {boolean} True if arguments are equal. - */ -function equals(o1, o2) { - if (o1 === o2) return true; - if (o1 === null || o2 === null) return false; - if (o1 !== o1 && o2 !== o2) return true; // NaN === NaN - var t1 = typeof o1, t2 = typeof o2, length, key, keySet; - if (t1 == t2) { - if (t1 == 'object') { - if (isArray(o1)) { - if (!isArray(o2)) return false; - if ((length = o1.length) == o2.length) { - for (key = 0; key < length; key++) { - if (!equals(o1[key], o2[key])) return false; - } - return true; - } - } else if (isDate(o1)) { - if (!isDate(o2)) return false; - return equals(o1.getTime(), o2.getTime()); - } else if (isRegExp(o1)) { - return isRegExp(o2) ? o1.toString() == o2.toString() : false; - } else { - if (isScope(o1) || isScope(o2) || isWindow(o1) || isWindow(o2) || - isArray(o2) || isDate(o2) || isRegExp(o2)) return false; - keySet = createMap(); - for (key in o1) { - if (key.charAt(0) === '$' || isFunction(o1[key])) continue; - if (!equals(o1[key], o2[key])) return false; - keySet[key] = true; - } - for (key in o2) { - if (!(key in keySet) && - key.charAt(0) !== '$' && - o2[key] !== undefined && - !isFunction(o2[key])) return false; - } - return true; - } - } - } - return false; -} - -var csp = function() { - if (!isDefined(csp.rules)) { - - - var ngCspElement = (document.querySelector('[ng-csp]') || - document.querySelector('[data-ng-csp]')); - - if (ngCspElement) { - var ngCspAttribute = ngCspElement.getAttribute('ng-csp') || - ngCspElement.getAttribute('data-ng-csp'); - csp.rules = { - noUnsafeEval: !ngCspAttribute || (ngCspAttribute.indexOf('no-unsafe-eval') !== -1), - noInlineStyle: !ngCspAttribute || (ngCspAttribute.indexOf('no-inline-style') !== -1) - }; - } else { - csp.rules = { - noUnsafeEval: noUnsafeEval(), - noInlineStyle: false - }; - } - } - - return csp.rules; - - function noUnsafeEval() { - try { - /* jshint -W031, -W054 */ - new Function(''); - /* jshint +W031, +W054 */ - return false; - } catch (e) { - return true; - } - } -}; - -/** - * @ngdoc directive - * @module ng - * @name ngJq - * - * @element ANY - * @param {string=} ngJq the name of the library available under `window` - * to be used for angular.element - * @description - * Use this directive to force the angular.element library. This should be - * used to force either jqLite by leaving ng-jq blank or setting the name of - * the jquery variable under window (eg. jQuery). - * - * Since angular looks for this directive when it is loaded (doesn't wait for the - * DOMContentLoaded event), it must be placed on an element that comes before the script - * which loads angular. Also, only the first instance of `ng-jq` will be used and all - * others ignored. - * - * @example - * This example shows how to force jqLite using the `ngJq` directive to the `html` tag. - ```html - - - ... - ... - - ``` - * @example - * This example shows how to use a jQuery based library of a different name. - * The library name must be available at the top most 'window'. - ```html - - - ... - ... - - ``` - */ -var jq = function() { - if (isDefined(jq.name_)) return jq.name_; - var el; - var i, ii = ngAttrPrefixes.length, prefix, name; - for (i = 0; i < ii; ++i) { - prefix = ngAttrPrefixes[i]; - if (el = document.querySelector('[' + prefix.replace(':', '\\:') + 'jq]')) { - name = el.getAttribute(prefix + 'jq'); - break; - } - } - - return (jq.name_ = name); -}; - -function concat(array1, array2, index) { - return array1.concat(slice.call(array2, index)); -} - -function sliceArgs(args, startIndex) { - return slice.call(args, startIndex || 0); -} - - -/* jshint -W101 */ -/** - * @ngdoc function - * @name angular.bind - * @module ng - * @kind function - * - * @description - * Returns a function which calls function `fn` bound to `self` (`self` becomes the `this` for - * `fn`). You can supply optional `args` that are prebound to the function. This feature is also - * known as [partial application](http://en.wikipedia.org/wiki/Partial_application), as - * distinguished from [function currying](http://en.wikipedia.org/wiki/Currying#Contrast_with_partial_function_application). - * - * @param {Object} self Context which `fn` should be evaluated in. - * @param {function()} fn Function to be bound. - * @param {...*} args Optional arguments to be prebound to the `fn` function call. - * @returns {function()} Function that wraps the `fn` with all the specified bindings. - */ -/* jshint +W101 */ -function bind(self, fn) { - var curryArgs = arguments.length > 2 ? sliceArgs(arguments, 2) : []; - if (isFunction(fn) && !(fn instanceof RegExp)) { - return curryArgs.length - ? function() { - return arguments.length - ? fn.apply(self, concat(curryArgs, arguments, 0)) - : fn.apply(self, curryArgs); - } - : function() { - return arguments.length - ? fn.apply(self, arguments) - : fn.call(self); - }; - } else { - // in IE, native methods are not functions so they cannot be bound (note: they don't need to be) - return fn; - } -} - - -function toJsonReplacer(key, value) { - var val = value; - - if (typeof key === 'string' && key.charAt(0) === '$' && key.charAt(1) === '$') { - val = undefined; - } else if (isWindow(value)) { - val = '$WINDOW'; - } else if (value && document === value) { - val = '$DOCUMENT'; - } else if (isScope(value)) { - val = '$SCOPE'; - } - - return val; -} - - -/** - * @ngdoc function - * @name angular.toJson - * @module ng - * @kind function - * - * @description - * Serializes input into a JSON-formatted string. Properties with leading $$ characters will be - * stripped since angular uses this notation internally. - * - * @param {Object|Array|Date|string|number} obj Input to be serialized into JSON. - * @param {boolean|number} [pretty=2] If set to true, the JSON output will contain newlines and whitespace. - * If set to an integer, the JSON output will contain that many spaces per indentation. - * @returns {string|undefined} JSON-ified string representing `obj`. - */ -function toJson(obj, pretty) { - if (typeof obj === 'undefined') return undefined; - if (!isNumber(pretty)) { - pretty = pretty ? 2 : null; - } - return JSON.stringify(obj, toJsonReplacer, pretty); -} - - -/** - * @ngdoc function - * @name angular.fromJson - * @module ng - * @kind function - * - * @description - * Deserializes a JSON string. - * - * @param {string} json JSON string to deserialize. - * @returns {Object|Array|string|number} Deserialized JSON string. - */ -function fromJson(json) { - return isString(json) - ? JSON.parse(json) - : json; -} - - -function timezoneToOffset(timezone, fallback) { - var requestedTimezoneOffset = Date.parse('Jan 01, 1970 00:00:00 ' + timezone) / 60000; - return isNaN(requestedTimezoneOffset) ? fallback : requestedTimezoneOffset; -} - - -function addDateMinutes(date, minutes) { - date = new Date(date.getTime()); - date.setMinutes(date.getMinutes() + minutes); - return date; -} - - -function convertTimezoneToLocal(date, timezone, reverse) { - reverse = reverse ? -1 : 1; - var timezoneOffset = timezoneToOffset(timezone, date.getTimezoneOffset()); - return addDateMinutes(date, reverse * (timezoneOffset - date.getTimezoneOffset())); -} - - -/** - * @returns {string} Returns the string representation of the element. - */ -function startingTag(element) { - element = jqLite(element).clone(); - try { - // turns out IE does not let you set .html() on elements which - // are not allowed to have children. So we just ignore it. - element.empty(); - } catch (e) {} - var elemHtml = jqLite('
    ').append(element).html(); - try { - return element[0].nodeType === NODE_TYPE_TEXT ? lowercase(elemHtml) : - elemHtml. - match(/^(<[^>]+>)/)[1]. - replace(/^<([\w\-]+)/, function(match, nodeName) { return '<' + lowercase(nodeName); }); - } catch (e) { - return lowercase(elemHtml); - } - -} - - -///////////////////////////////////////////////// - -/** - * Tries to decode the URI component without throwing an exception. - * - * @private - * @param str value potential URI component to check. - * @returns {boolean} True if `value` can be decoded - * with the decodeURIComponent function. - */ -function tryDecodeURIComponent(value) { - try { - return decodeURIComponent(value); - } catch (e) { - // Ignore any invalid uri component - } -} - - -/** - * Parses an escaped url query string into key-value pairs. - * @returns {Object.} - */ -function parseKeyValue(/**string*/keyValue) { - var obj = {}; - forEach((keyValue || "").split('&'), function(keyValue) { - var splitPoint, key, val; - if (keyValue) { - key = keyValue = keyValue.replace(/\+/g,'%20'); - splitPoint = keyValue.indexOf('='); - if (splitPoint !== -1) { - key = keyValue.substring(0, splitPoint); - val = keyValue.substring(splitPoint + 1); - } - key = tryDecodeURIComponent(key); - if (isDefined(key)) { - val = isDefined(val) ? tryDecodeURIComponent(val) : true; - if (!hasOwnProperty.call(obj, key)) { - obj[key] = val; - } else if (isArray(obj[key])) { - obj[key].push(val); - } else { - obj[key] = [obj[key],val]; - } - } - } - }); - return obj; -} - -function toKeyValue(obj) { - var parts = []; - forEach(obj, function(value, key) { - if (isArray(value)) { - forEach(value, function(arrayValue) { - parts.push(encodeUriQuery(key, true) + - (arrayValue === true ? '' : '=' + encodeUriQuery(arrayValue, true))); - }); - } else { - parts.push(encodeUriQuery(key, true) + - (value === true ? '' : '=' + encodeUriQuery(value, true))); - } - }); - return parts.length ? parts.join('&') : ''; -} - - -/** - * We need our custom method because encodeURIComponent is too aggressive and doesn't follow - * http://www.ietf.org/rfc/rfc3986.txt with regards to the character set (pchar) allowed in path - * segments: - * segment = *pchar - * pchar = unreserved / pct-encoded / sub-delims / ":" / "@" - * pct-encoded = "%" HEXDIG HEXDIG - * unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" - * sub-delims = "!" / "$" / "&" / "'" / "(" / ")" - * / "*" / "+" / "," / ";" / "=" - */ -function encodeUriSegment(val) { - return encodeUriQuery(val, true). - replace(/%26/gi, '&'). - replace(/%3D/gi, '='). - replace(/%2B/gi, '+'); -} - - -/** - * This method is intended for encoding *key* or *value* parts of query component. We need a custom - * method because encodeURIComponent is too aggressive and encodes stuff that doesn't have to be - * encoded per http://tools.ietf.org/html/rfc3986: - * query = *( pchar / "/" / "?" ) - * pchar = unreserved / pct-encoded / sub-delims / ":" / "@" - * unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" - * pct-encoded = "%" HEXDIG HEXDIG - * sub-delims = "!" / "$" / "&" / "'" / "(" / ")" - * / "*" / "+" / "," / ";" / "=" - */ -function encodeUriQuery(val, pctEncodeSpaces) { - return encodeURIComponent(val). - replace(/%40/gi, '@'). - replace(/%3A/gi, ':'). - replace(/%24/g, '$'). - replace(/%2C/gi, ','). - replace(/%3B/gi, ';'). - replace(/%20/g, (pctEncodeSpaces ? '%20' : '+')); -} - -var ngAttrPrefixes = ['ng-', 'data-ng-', 'ng:', 'x-ng-']; - -function getNgAttribute(element, ngAttr) { - var attr, i, ii = ngAttrPrefixes.length; - for (i = 0; i < ii; ++i) { - attr = ngAttrPrefixes[i] + ngAttr; - if (isString(attr = element.getAttribute(attr))) { - return attr; - } - } - return null; -} - -/** - * @ngdoc directive - * @name ngApp - * @module ng - * - * @element ANY - * @param {angular.Module} ngApp an optional application - * {@link angular.module module} name to load. - * @param {boolean=} ngStrictDi if this attribute is present on the app element, the injector will be - * created in "strict-di" mode. This means that the application will fail to invoke functions which - * do not use explicit function annotation (and are thus unsuitable for minification), as described - * in {@link guide/di the Dependency Injection guide}, and useful debugging info will assist in - * tracking down the root of these bugs. - * - * @description - * - * Use this directive to **auto-bootstrap** an AngularJS application. The `ngApp` directive - * designates the **root element** of the application and is typically placed near the root element - * of the page - e.g. on the `` or `` tags. - * - * Only one AngularJS application can be auto-bootstrapped per HTML document. The first `ngApp` - * found in the document will be used to define the root element to auto-bootstrap as an - * application. To run multiple applications in an HTML document you must manually bootstrap them using - * {@link angular.bootstrap} instead. AngularJS applications cannot be nested within each other. - * - * You can specify an **AngularJS module** to be used as the root module for the application. This - * module will be loaded into the {@link auto.$injector} when the application is bootstrapped. It - * should contain the application code needed or have dependencies on other modules that will - * contain the code. See {@link angular.module} for more information. - * - * In the example below if the `ngApp` directive were not placed on the `html` element then the - * document would not be compiled, the `AppController` would not be instantiated and the `{{ a+b }}` - * would not be resolved to `3`. - * - * `ngApp` is the easiest, and most common way to bootstrap an application. - * - - -
    - I can add: {{a}} + {{b}} = {{ a+b }} -
    -
    - - angular.module('ngAppDemo', []).controller('ngAppDemoController', function($scope) { - $scope.a = 1; - $scope.b = 2; - }); - -
    - * - * Using `ngStrictDi`, you would see something like this: - * - - -
    -
    - I can add: {{a}} + {{b}} = {{ a+b }} - -

    This renders because the controller does not fail to - instantiate, by using explicit annotation style (see - script.js for details) -

    -
    - -
    - Name:
    - Hello, {{name}}! - -

    This renders because the controller does not fail to - instantiate, by using explicit annotation style - (see script.js for details) -

    -
    - -
    - I can add: {{a}} + {{b}} = {{ a+b }} - -

    The controller could not be instantiated, due to relying - on automatic function annotations (which are disabled in - strict mode). As such, the content of this section is not - interpolated, and there should be an error in your web console. -

    -
    -
    -
    - - angular.module('ngAppStrictDemo', []) - // BadController will fail to instantiate, due to relying on automatic function annotation, - // rather than an explicit annotation - .controller('BadController', function($scope) { - $scope.a = 1; - $scope.b = 2; - }) - // Unlike BadController, GoodController1 and GoodController2 will not fail to be instantiated, - // due to using explicit annotations using the array style and $inject property, respectively. - .controller('GoodController1', ['$scope', function($scope) { - $scope.a = 1; - $scope.b = 2; - }]) - .controller('GoodController2', GoodController2); - function GoodController2($scope) { - $scope.name = "World"; - } - GoodController2.$inject = ['$scope']; - - - div[ng-controller] { - margin-bottom: 1em; - -webkit-border-radius: 4px; - border-radius: 4px; - border: 1px solid; - padding: .5em; - } - div[ng-controller^=Good] { - border-color: #d6e9c6; - background-color: #dff0d8; - color: #3c763d; - } - div[ng-controller^=Bad] { - border-color: #ebccd1; - background-color: #f2dede; - color: #a94442; - margin-bottom: 0; - } - -
    - */ -function angularInit(element, bootstrap) { - var appElement, - module, - config = {}; - - // The element `element` has priority over any other element - forEach(ngAttrPrefixes, function(prefix) { - var name = prefix + 'app'; - - if (!appElement && element.hasAttribute && element.hasAttribute(name)) { - appElement = element; - module = element.getAttribute(name); - } - }); - forEach(ngAttrPrefixes, function(prefix) { - var name = prefix + 'app'; - var candidate; - - if (!appElement && (candidate = element.querySelector('[' + name.replace(':', '\\:') + ']'))) { - appElement = candidate; - module = candidate.getAttribute(name); - } - }); - if (appElement) { - config.strictDi = getNgAttribute(appElement, "strict-di") !== null; - bootstrap(appElement, module ? [module] : [], config); - } -} - -/** - * @ngdoc function - * @name angular.bootstrap - * @module ng - * @description - * Use this function to manually start up angular application. - * - * See: {@link guide/bootstrap Bootstrap} - * - * Note that Protractor based end-to-end tests cannot use this function to bootstrap manually. - * They must use {@link ng.directive:ngApp ngApp}. - * - * Angular will detect if it has been loaded into the browser more than once and only allow the - * first loaded script to be bootstrapped and will report a warning to the browser console for - * each of the subsequent scripts. This prevents strange results in applications, where otherwise - * multiple instances of Angular try to work on the DOM. - * - * ```html - * - * - * - *
    - * {{greeting}} - *
    - * - * - * - * - * - * ``` - * - * @param {DOMElement} element DOM element which is the root of angular application. - * @param {Array=} modules an array of modules to load into the application. - * Each item in the array should be the name of a predefined module or a (DI annotated) - * function that will be invoked by the injector as a `config` block. - * See: {@link angular.module modules} - * @param {Object=} config an object for defining configuration options for the application. The - * following keys are supported: - * - * * `strictDi` - disable automatic function annotation for the application. This is meant to - * assist in finding bugs which break minified code. Defaults to `false`. - * - * @returns {auto.$injector} Returns the newly created injector for this app. - */ -function bootstrap(element, modules, config) { - if (!isObject(config)) config = {}; - var defaultConfig = { - strictDi: false - }; - config = extend(defaultConfig, config); - var doBootstrap = function() { - element = jqLite(element); - - if (element.injector()) { - var tag = (element[0] === document) ? 'document' : startingTag(element); - //Encode angle brackets to prevent input from being sanitized to empty string #8683 - throw ngMinErr( - 'btstrpd', - "App Already Bootstrapped with this Element '{0}'", - tag.replace(//,'>')); - } - - modules = modules || []; - modules.unshift(['$provide', function($provide) { - $provide.value('$rootElement', element); - }]); - - if (config.debugInfoEnabled) { - // Pushing so that this overrides `debugInfoEnabled` setting defined in user's `modules`. - modules.push(['$compileProvider', function($compileProvider) { - $compileProvider.debugInfoEnabled(true); - }]); - } - - modules.unshift('ng'); - var injector = createInjector(modules, config.strictDi); - injector.invoke(['$rootScope', '$rootElement', '$compile', '$injector', - function bootstrapApply(scope, element, compile, injector) { - scope.$apply(function() { - element.data('$injector', injector); - compile(element)(scope); - }); - }] - ); - return injector; - }; - - var NG_ENABLE_DEBUG_INFO = /^NG_ENABLE_DEBUG_INFO!/; - var NG_DEFER_BOOTSTRAP = /^NG_DEFER_BOOTSTRAP!/; - - if (window && NG_ENABLE_DEBUG_INFO.test(window.name)) { - config.debugInfoEnabled = true; - window.name = window.name.replace(NG_ENABLE_DEBUG_INFO, ''); - } - - if (window && !NG_DEFER_BOOTSTRAP.test(window.name)) { - return doBootstrap(); - } - - window.name = window.name.replace(NG_DEFER_BOOTSTRAP, ''); - angular.resumeBootstrap = function(extraModules) { - forEach(extraModules, function(module) { - modules.push(module); - }); - return doBootstrap(); - }; - - if (isFunction(angular.resumeDeferredBootstrap)) { - angular.resumeDeferredBootstrap(); - } -} - -/** - * @ngdoc function - * @name angular.reloadWithDebugInfo - * @module ng - * @description - * Use this function to reload the current application with debug information turned on. - * This takes precedence over a call to `$compileProvider.debugInfoEnabled(false)`. - * - * See {@link ng.$compileProvider#debugInfoEnabled} for more. - */ -function reloadWithDebugInfo() { - window.name = 'NG_ENABLE_DEBUG_INFO!' + window.name; - window.location.reload(); -} - -/** - * @name angular.getTestability - * @module ng - * @description - * Get the testability service for the instance of Angular on the given - * element. - * @param {DOMElement} element DOM element which is the root of angular application. - */ -function getTestability(rootElement) { - var injector = angular.element(rootElement).injector(); - if (!injector) { - throw ngMinErr('test', - 'no injector found for element argument to getTestability'); - } - return injector.get('$$testability'); -} - -var SNAKE_CASE_REGEXP = /[A-Z]/g; -function snake_case(name, separator) { - separator = separator || '_'; - return name.replace(SNAKE_CASE_REGEXP, function(letter, pos) { - return (pos ? separator : '') + letter.toLowerCase(); - }); -} - -var bindJQueryFired = false; -var skipDestroyOnNextJQueryCleanData; -function bindJQuery() { - var originalCleanData; - - if (bindJQueryFired) { - return; - } - - // bind to jQuery if present; - var jqName = jq(); - jQuery = window.jQuery; // use default jQuery. - if (isDefined(jqName)) { // `ngJq` present - jQuery = jqName === null ? undefined : window[jqName]; // if empty; use jqLite. if not empty, use jQuery specified by `ngJq`. - } - - // Use jQuery if it exists with proper functionality, otherwise default to us. - // Angular 1.2+ requires jQuery 1.7+ for on()/off() support. - // Angular 1.3+ technically requires at least jQuery 2.1+ but it may work with older - // versions. It will not work for sure with jQuery <1.7, though. - if (jQuery && jQuery.fn.on) { - jqLite = jQuery; - extend(jQuery.fn, { - scope: JQLitePrototype.scope, - isolateScope: JQLitePrototype.isolateScope, - controller: JQLitePrototype.controller, - injector: JQLitePrototype.injector, - inheritedData: JQLitePrototype.inheritedData - }); - - // All nodes removed from the DOM via various jQuery APIs like .remove() - // are passed through jQuery.cleanData. Monkey-patch this method to fire - // the $destroy event on all removed nodes. - originalCleanData = jQuery.cleanData; - jQuery.cleanData = function(elems) { - var events; - if (!skipDestroyOnNextJQueryCleanData) { - for (var i = 0, elem; (elem = elems[i]) != null; i++) { - events = jQuery._data(elem, "events"); - if (events && events.$destroy) { - jQuery(elem).triggerHandler('$destroy'); - } - } - } else { - skipDestroyOnNextJQueryCleanData = false; - } - originalCleanData(elems); - }; - } else { - jqLite = JQLite; - } - - angular.element = jqLite; - - // Prevent double-proxying. - bindJQueryFired = true; -} - -/** - * throw error if the argument is falsy. - */ -function assertArg(arg, name, reason) { - if (!arg) { - throw ngMinErr('areq', "Argument '{0}' is {1}", (name || '?'), (reason || "required")); - } - return arg; -} - -function assertArgFn(arg, name, acceptArrayAnnotation) { - if (acceptArrayAnnotation && isArray(arg)) { - arg = arg[arg.length - 1]; - } - - assertArg(isFunction(arg), name, 'not a function, got ' + - (arg && typeof arg === 'object' ? arg.constructor.name || 'Object' : typeof arg)); - return arg; -} - -/** - * throw error if the name given is hasOwnProperty - * @param {String} name the name to test - * @param {String} context the context in which the name is used, such as module or directive - */ -function assertNotHasOwnProperty(name, context) { - if (name === 'hasOwnProperty') { - throw ngMinErr('badname', "hasOwnProperty is not a valid {0} name", context); - } -} - -/** - * Return the value accessible from the object by path. Any undefined traversals are ignored - * @param {Object} obj starting object - * @param {String} path path to traverse - * @param {boolean} [bindFnToScope=true] - * @returns {Object} value as accessible by path - */ -//TODO(misko): this function needs to be removed -function getter(obj, path, bindFnToScope) { - if (!path) return obj; - var keys = path.split('.'); - var key; - var lastInstance = obj; - var len = keys.length; - - for (var i = 0; i < len; i++) { - key = keys[i]; - if (obj) { - obj = (lastInstance = obj)[key]; - } - } - if (!bindFnToScope && isFunction(obj)) { - return bind(lastInstance, obj); - } - return obj; -} - -/** - * Return the DOM siblings between the first and last node in the given array. - * @param {Array} array like object - * @returns {jqLite} jqLite collection containing the nodes - */ -function getBlockNodes(nodes) { - // TODO(perf): just check if all items in `nodes` are siblings and if they are return the original - // collection, otherwise update the original collection. - var node = nodes[0]; - var endNode = nodes[nodes.length - 1]; - var blockNodes = [node]; - - do { - node = node.nextSibling; - if (!node) break; - blockNodes.push(node); - } while (node !== endNode); - - return jqLite(blockNodes); -} - - -/** - * Creates a new object without a prototype. This object is useful for lookup without having to - * guard against prototypically inherited properties via hasOwnProperty. - * - * Related micro-benchmarks: - * - http://jsperf.com/object-create2 - * - http://jsperf.com/proto-map-lookup/2 - * - http://jsperf.com/for-in-vs-object-keys2 - * - * @returns {Object} - */ -function createMap() { - return Object.create(null); -} - -var NODE_TYPE_ELEMENT = 1; -var NODE_TYPE_ATTRIBUTE = 2; -var NODE_TYPE_TEXT = 3; -var NODE_TYPE_COMMENT = 8; -var NODE_TYPE_DOCUMENT = 9; -var NODE_TYPE_DOCUMENT_FRAGMENT = 11; - -/** - * @ngdoc type - * @name angular.Module - * @module ng - * @description - * - * Interface for configuring angular {@link angular.module modules}. - */ - -function setupModuleLoader(window) { - - var $injectorMinErr = minErr('$injector'); - var ngMinErr = minErr('ng'); - - function ensure(obj, name, factory) { - return obj[name] || (obj[name] = factory()); - } - - var angular = ensure(window, 'angular', Object); - - // We need to expose `angular.$$minErr` to modules such as `ngResource` that reference it during bootstrap - angular.$$minErr = angular.$$minErr || minErr; - - return ensure(angular, 'module', function() { - /** @type {Object.} */ - var modules = {}; - - /** - * @ngdoc function - * @name angular.module - * @module ng - * @description - * - * The `angular.module` is a global place for creating, registering and retrieving Angular - * modules. - * All modules (angular core or 3rd party) that should be available to an application must be - * registered using this mechanism. - * - * Passing one argument retrieves an existing {@link angular.Module}, - * whereas passing more than one argument creates a new {@link angular.Module} - * - * - * # Module - * - * A module is a collection of services, directives, controllers, filters, and configuration information. - * `angular.module` is used to configure the {@link auto.$injector $injector}. - * - * ```js - * // Create a new module - * var myModule = angular.module('myModule', []); - * - * // register a new service - * myModule.value('appName', 'MyCoolApp'); - * - * // configure existing services inside initialization blocks. - * myModule.config(['$locationProvider', function($locationProvider) { - * // Configure existing providers - * $locationProvider.hashPrefix('!'); - * }]); - * ``` - * - * Then you can create an injector and load your modules like this: - * - * ```js - * var injector = angular.injector(['ng', 'myModule']) - * ``` - * - * However it's more likely that you'll just use - * {@link ng.directive:ngApp ngApp} or - * {@link angular.bootstrap} to simplify this process for you. - * - * @param {!string} name The name of the module to create or retrieve. - * @param {!Array.=} requires If specified then new module is being created. If - * unspecified then the module is being retrieved for further configuration. - * @param {Function=} configFn Optional configuration function for the module. Same as - * {@link angular.Module#config Module#config()}. - * @returns {module} new module with the {@link angular.Module} api. - */ - return function module(name, requires, configFn) { - var assertNotHasOwnProperty = function(name, context) { - if (name === 'hasOwnProperty') { - throw ngMinErr('badname', 'hasOwnProperty is not a valid {0} name', context); - } - }; - - assertNotHasOwnProperty(name, 'module'); - if (requires && modules.hasOwnProperty(name)) { - modules[name] = null; - } - return ensure(modules, name, function() { - if (!requires) { - throw $injectorMinErr('nomod', "Module '{0}' is not available! You either misspelled " + - "the module name or forgot to load it. If registering a module ensure that you " + - "specify the dependencies as the second argument.", name); - } - - /** @type {!Array.>} */ - var invokeQueue = []; - - /** @type {!Array.} */ - var configBlocks = []; - - /** @type {!Array.} */ - var runBlocks = []; - - var config = invokeLater('$injector', 'invoke', 'push', configBlocks); - - /** @type {angular.Module} */ - var moduleInstance = { - // Private state - _invokeQueue: invokeQueue, - _configBlocks: configBlocks, - _runBlocks: runBlocks, - - /** - * @ngdoc property - * @name angular.Module#requires - * @module ng - * - * @description - * Holds the list of modules which the injector will load before the current module is - * loaded. - */ - requires: requires, - - /** - * @ngdoc property - * @name angular.Module#name - * @module ng - * - * @description - * Name of the module. - */ - name: name, - - - /** - * @ngdoc method - * @name angular.Module#provider - * @module ng - * @param {string} name service name - * @param {Function} providerType Construction function for creating new instance of the - * service. - * @description - * See {@link auto.$provide#provider $provide.provider()}. - */ - provider: invokeLaterAndSetModuleName('$provide', 'provider'), - - /** - * @ngdoc method - * @name angular.Module#factory - * @module ng - * @param {string} name service name - * @param {Function} providerFunction Function for creating new instance of the service. - * @description - * See {@link auto.$provide#factory $provide.factory()}. - */ - factory: invokeLaterAndSetModuleName('$provide', 'factory'), - - /** - * @ngdoc method - * @name angular.Module#service - * @module ng - * @param {string} name service name - * @param {Function} constructor A constructor function that will be instantiated. - * @description - * See {@link auto.$provide#service $provide.service()}. - */ - service: invokeLaterAndSetModuleName('$provide', 'service'), - - /** - * @ngdoc method - * @name angular.Module#value - * @module ng - * @param {string} name service name - * @param {*} object Service instance object. - * @description - * See {@link auto.$provide#value $provide.value()}. - */ - value: invokeLater('$provide', 'value'), - - /** - * @ngdoc method - * @name angular.Module#constant - * @module ng - * @param {string} name constant name - * @param {*} object Constant value. - * @description - * Because the constant are fixed, they get applied before other provide methods. - * See {@link auto.$provide#constant $provide.constant()}. - */ - constant: invokeLater('$provide', 'constant', 'unshift'), - - /** - * @ngdoc method - * @name angular.Module#decorator - * @module ng - * @param {string} The name of the service to decorate. - * @param {Function} This function will be invoked when the service needs to be - * instantiated and should return the decorated service instance. - * @description - * See {@link auto.$provide#decorator $provide.decorator()}. - */ - decorator: invokeLaterAndSetModuleName('$provide', 'decorator'), - - /** - * @ngdoc method - * @name angular.Module#animation - * @module ng - * @param {string} name animation name - * @param {Function} animationFactory Factory function for creating new instance of an - * animation. - * @description - * - * **NOTE**: animations take effect only if the **ngAnimate** module is loaded. - * - * - * Defines an animation hook that can be later used with - * {@link $animate $animate} service and directives that use this service. - * - * ```js - * module.animation('.animation-name', function($inject1, $inject2) { - * return { - * eventName : function(element, done) { - * //code to run the animation - * //once complete, then run done() - * return function cancellationFunction(element) { - * //code to cancel the animation - * } - * } - * } - * }) - * ``` - * - * See {@link ng.$animateProvider#register $animateProvider.register()} and - * {@link ngAnimate ngAnimate module} for more information. - */ - animation: invokeLaterAndSetModuleName('$animateProvider', 'register'), - - /** - * @ngdoc method - * @name angular.Module#filter - * @module ng - * @param {string} name Filter name - this must be a valid angular expression identifier - * @param {Function} filterFactory Factory function for creating new instance of filter. - * @description - * See {@link ng.$filterProvider#register $filterProvider.register()}. - * - *
    - * **Note:** Filter names must be valid angular {@link expression} identifiers, such as `uppercase` or `orderBy`. - * Names with special characters, such as hyphens and dots, are not allowed. If you wish to namespace - * your filters, then you can use capitalization (`myappSubsectionFilterx`) or underscores - * (`myapp_subsection_filterx`). - *
    - */ - filter: invokeLaterAndSetModuleName('$filterProvider', 'register'), - - /** - * @ngdoc method - * @name angular.Module#controller - * @module ng - * @param {string|Object} name Controller name, or an object map of controllers where the - * keys are the names and the values are the constructors. - * @param {Function} constructor Controller constructor function. - * @description - * See {@link ng.$controllerProvider#register $controllerProvider.register()}. - */ - controller: invokeLaterAndSetModuleName('$controllerProvider', 'register'), - - /** - * @ngdoc method - * @name angular.Module#directive - * @module ng - * @param {string|Object} name Directive name, or an object map of directives where the - * keys are the names and the values are the factories. - * @param {Function} directiveFactory Factory function for creating new instance of - * directives. - * @description - * See {@link ng.$compileProvider#directive $compileProvider.directive()}. - */ - directive: invokeLaterAndSetModuleName('$compileProvider', 'directive'), - - /** - * @ngdoc method - * @name angular.Module#config - * @module ng - * @param {Function} configFn Execute this function on module load. Useful for service - * configuration. - * @description - * Use this method to register work which needs to be performed on module loading. - * For more about how to configure services, see - * {@link providers#provider-recipe Provider Recipe}. - */ - config: config, - - /** - * @ngdoc method - * @name angular.Module#run - * @module ng - * @param {Function} initializationFn Execute this function after injector creation. - * Useful for application initialization. - * @description - * Use this method to register work which should be performed when the injector is done - * loading all modules. - */ - run: function(block) { - runBlocks.push(block); - return this; - } - }; - - if (configFn) { - config(configFn); - } - - return moduleInstance; - - /** - * @param {string} provider - * @param {string} method - * @param {String=} insertMethod - * @returns {angular.Module} - */ - function invokeLater(provider, method, insertMethod, queue) { - if (!queue) queue = invokeQueue; - return function() { - queue[insertMethod || 'push']([provider, method, arguments]); - return moduleInstance; - }; - } - - /** - * @param {string} provider - * @param {string} method - * @returns {angular.Module} - */ - function invokeLaterAndSetModuleName(provider, method) { - return function(recipeName, factoryFunction) { - if (factoryFunction && isFunction(factoryFunction)) factoryFunction.$$moduleName = name; - invokeQueue.push([provider, method, arguments]); - return moduleInstance; - }; - } - }); - }; - }); - -} - -/* global: toDebugString: true */ - -function serializeObject(obj) { - var seen = []; - - return JSON.stringify(obj, function(key, val) { - val = toJsonReplacer(key, val); - if (isObject(val)) { - - if (seen.indexOf(val) >= 0) return '<>'; - - seen.push(val); - } - return val; - }); -} - -function toDebugString(obj) { - if (typeof obj === 'function') { - return obj.toString().replace(/ \{[\s\S]*$/, ''); - } else if (typeof obj === 'undefined') { - return 'undefined'; - } else if (typeof obj !== 'string') { - return serializeObject(obj); - } - return obj; -} - -/* global angularModule: true, - version: true, - - $CompileProvider, - - htmlAnchorDirective, - inputDirective, - inputDirective, - formDirective, - scriptDirective, - selectDirective, - styleDirective, - optionDirective, - ngBindDirective, - ngBindHtmlDirective, - ngBindTemplateDirective, - ngClassDirective, - ngClassEvenDirective, - ngClassOddDirective, - ngCloakDirective, - ngControllerDirective, - ngFormDirective, - ngHideDirective, - ngIfDirective, - ngIncludeDirective, - ngIncludeFillContentDirective, - ngInitDirective, - ngNonBindableDirective, - ngPluralizeDirective, - ngRepeatDirective, - ngShowDirective, - ngStyleDirective, - ngSwitchDirective, - ngSwitchWhenDirective, - ngSwitchDefaultDirective, - ngOptionsDirective, - ngTranscludeDirective, - ngModelDirective, - ngListDirective, - ngChangeDirective, - patternDirective, - patternDirective, - requiredDirective, - requiredDirective, - minlengthDirective, - minlengthDirective, - maxlengthDirective, - maxlengthDirective, - ngValueDirective, - ngModelOptionsDirective, - ngAttributeAliasDirectives, - ngEventDirectives, - - $AnchorScrollProvider, - $AnimateProvider, - $CoreAnimateCssProvider, - $$CoreAnimateQueueProvider, - $$CoreAnimateRunnerProvider, - $BrowserProvider, - $CacheFactoryProvider, - $ControllerProvider, - $DocumentProvider, - $ExceptionHandlerProvider, - $FilterProvider, - $$ForceReflowProvider, - $InterpolateProvider, - $IntervalProvider, - $$HashMapProvider, - $HttpProvider, - $HttpParamSerializerProvider, - $HttpParamSerializerJQLikeProvider, - $HttpBackendProvider, - $LocationProvider, - $LogProvider, - $ParseProvider, - $RootScopeProvider, - $QProvider, - $$QProvider, - $$SanitizeUriProvider, - $SceProvider, - $SceDelegateProvider, - $SnifferProvider, - $TemplateCacheProvider, - $TemplateRequestProvider, - $$TestabilityProvider, - $TimeoutProvider, - $$RAFProvider, - $WindowProvider, - $$jqLiteProvider, - $$CookieReaderProvider -*/ - - -/** - * @ngdoc object - * @name angular.version - * @module ng - * @description - * An object that contains information about the current AngularJS version. This object has the - * following properties: - * - * - `full` – `{string}` – Full version string, such as "0.9.18". - * - `major` – `{number}` – Major version number, such as "0". - * - `minor` – `{number}` – Minor version number, such as "9". - * - `dot` – `{number}` – Dot version number, such as "18". - * - `codeName` – `{string}` – Code name of the release, such as "jiggling-armfat". - */ -var version = { - full: '1.4.4', // all of these placeholder strings will be replaced by grunt's - major: 1, // package task - minor: 4, - dot: 4, - codeName: 'pylon-requirement' -}; - - -function publishExternalAPI(angular) { - extend(angular, { - 'bootstrap': bootstrap, - 'copy': copy, - 'extend': extend, - 'merge': merge, - 'equals': equals, - 'element': jqLite, - 'forEach': forEach, - 'injector': createInjector, - 'noop': noop, - 'bind': bind, - 'toJson': toJson, - 'fromJson': fromJson, - 'identity': identity, - 'isUndefined': isUndefined, - 'isDefined': isDefined, - 'isString': isString, - 'isFunction': isFunction, - 'isObject': isObject, - 'isNumber': isNumber, - 'isElement': isElement, - 'isArray': isArray, - 'version': version, - 'isDate': isDate, - 'lowercase': lowercase, - 'uppercase': uppercase, - 'callbacks': {counter: 0}, - 'getTestability': getTestability, - '$$minErr': minErr, - '$$csp': csp, - 'reloadWithDebugInfo': reloadWithDebugInfo - }); - - angularModule = setupModuleLoader(window); - - angularModule('ng', ['ngLocale'], ['$provide', - function ngModule($provide) { - // $$sanitizeUriProvider needs to be before $compileProvider as it is used by it. - $provide.provider({ - $$sanitizeUri: $$SanitizeUriProvider - }); - $provide.provider('$compile', $CompileProvider). - directive({ - a: htmlAnchorDirective, - input: inputDirective, - textarea: inputDirective, - form: formDirective, - script: scriptDirective, - select: selectDirective, - style: styleDirective, - option: optionDirective, - ngBind: ngBindDirective, - ngBindHtml: ngBindHtmlDirective, - ngBindTemplate: ngBindTemplateDirective, - ngClass: ngClassDirective, - ngClassEven: ngClassEvenDirective, - ngClassOdd: ngClassOddDirective, - ngCloak: ngCloakDirective, - ngController: ngControllerDirective, - ngForm: ngFormDirective, - ngHide: ngHideDirective, - ngIf: ngIfDirective, - ngInclude: ngIncludeDirective, - ngInit: ngInitDirective, - ngNonBindable: ngNonBindableDirective, - ngPluralize: ngPluralizeDirective, - ngRepeat: ngRepeatDirective, - ngShow: ngShowDirective, - ngStyle: ngStyleDirective, - ngSwitch: ngSwitchDirective, - ngSwitchWhen: ngSwitchWhenDirective, - ngSwitchDefault: ngSwitchDefaultDirective, - ngOptions: ngOptionsDirective, - ngTransclude: ngTranscludeDirective, - ngModel: ngModelDirective, - ngList: ngListDirective, - ngChange: ngChangeDirective, - pattern: patternDirective, - ngPattern: patternDirective, - required: requiredDirective, - ngRequired: requiredDirective, - minlength: minlengthDirective, - ngMinlength: minlengthDirective, - maxlength: maxlengthDirective, - ngMaxlength: maxlengthDirective, - ngValue: ngValueDirective, - ngModelOptions: ngModelOptionsDirective - }). - directive({ - ngInclude: ngIncludeFillContentDirective - }). - directive(ngAttributeAliasDirectives). - directive(ngEventDirectives); - $provide.provider({ - $anchorScroll: $AnchorScrollProvider, - $animate: $AnimateProvider, - $animateCss: $CoreAnimateCssProvider, - $$animateQueue: $$CoreAnimateQueueProvider, - $$AnimateRunner: $$CoreAnimateRunnerProvider, - $browser: $BrowserProvider, - $cacheFactory: $CacheFactoryProvider, - $controller: $ControllerProvider, - $document: $DocumentProvider, - $exceptionHandler: $ExceptionHandlerProvider, - $filter: $FilterProvider, - $$forceReflow: $$ForceReflowProvider, - $interpolate: $InterpolateProvider, - $interval: $IntervalProvider, - $http: $HttpProvider, - $httpParamSerializer: $HttpParamSerializerProvider, - $httpParamSerializerJQLike: $HttpParamSerializerJQLikeProvider, - $httpBackend: $HttpBackendProvider, - $location: $LocationProvider, - $log: $LogProvider, - $parse: $ParseProvider, - $rootScope: $RootScopeProvider, - $q: $QProvider, - $$q: $$QProvider, - $sce: $SceProvider, - $sceDelegate: $SceDelegateProvider, - $sniffer: $SnifferProvider, - $templateCache: $TemplateCacheProvider, - $templateRequest: $TemplateRequestProvider, - $$testability: $$TestabilityProvider, - $timeout: $TimeoutProvider, - $window: $WindowProvider, - $$rAF: $$RAFProvider, - $$jqLite: $$jqLiteProvider, - $$HashMap: $$HashMapProvider, - $$cookieReader: $$CookieReaderProvider - }); - } - ]); -} - -/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * - * Any commits to this file should be reviewed with security in mind. * - * Changes to this file can potentially create security vulnerabilities. * - * An approval from 2 Core members with history of modifying * - * this file is required. * - * * - * Does the change somehow allow for arbitrary javascript to be executed? * - * Or allows for someone to change the prototype of built-in objects? * - * Or gives undesired access to variables likes document or window? * - * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ - -/* global JQLitePrototype: true, - addEventListenerFn: true, - removeEventListenerFn: true, - BOOLEAN_ATTR: true, - ALIASED_ATTR: true, -*/ - -////////////////////////////////// -//JQLite -////////////////////////////////// - -/** - * @ngdoc function - * @name angular.element - * @module ng - * @kind function - * - * @description - * Wraps a raw DOM element or HTML string as a [jQuery](http://jquery.com) element. - * - * If jQuery is available, `angular.element` is an alias for the - * [jQuery](http://api.jquery.com/jQuery/) function. If jQuery is not available, `angular.element` - * delegates to Angular's built-in subset of jQuery, called "jQuery lite" or "jqLite." - * - *
    jqLite is a tiny, API-compatible subset of jQuery that allows - * Angular to manipulate the DOM in a cross-browser compatible way. **jqLite** implements only the most - * commonly needed functionality with the goal of having a very small footprint.
    - * - * To use `jQuery`, simply ensure it is loaded before the `angular.js` file. - * - *
    **Note:** all element references in Angular are always wrapped with jQuery or - * jqLite; they are never raw DOM references.
    - * - * ## Angular's jqLite - * jqLite provides only the following jQuery methods: - * - * - [`addClass()`](http://api.jquery.com/addClass/) - * - [`after()`](http://api.jquery.com/after/) - * - [`append()`](http://api.jquery.com/append/) - * - [`attr()`](http://api.jquery.com/attr/) - Does not support functions as parameters - * - [`bind()`](http://api.jquery.com/bind/) - Does not support namespaces, selectors or eventData - * - [`children()`](http://api.jquery.com/children/) - Does not support selectors - * - [`clone()`](http://api.jquery.com/clone/) - * - [`contents()`](http://api.jquery.com/contents/) - * - [`css()`](http://api.jquery.com/css/) - Only retrieves inline-styles, does not call `getComputedStyle()`. As a setter, does not convert numbers to strings or append 'px'. - * - [`data()`](http://api.jquery.com/data/) - * - [`detach()`](http://api.jquery.com/detach/) - * - [`empty()`](http://api.jquery.com/empty/) - * - [`eq()`](http://api.jquery.com/eq/) - * - [`find()`](http://api.jquery.com/find/) - Limited to lookups by tag name - * - [`hasClass()`](http://api.jquery.com/hasClass/) - * - [`html()`](http://api.jquery.com/html/) - * - [`next()`](http://api.jquery.com/next/) - Does not support selectors - * - [`on()`](http://api.jquery.com/on/) - Does not support namespaces, selectors or eventData - * - [`off()`](http://api.jquery.com/off/) - Does not support namespaces or selectors - * - [`one()`](http://api.jquery.com/one/) - Does not support namespaces or selectors - * - [`parent()`](http://api.jquery.com/parent/) - Does not support selectors - * - [`prepend()`](http://api.jquery.com/prepend/) - * - [`prop()`](http://api.jquery.com/prop/) - * - [`ready()`](http://api.jquery.com/ready/) - * - [`remove()`](http://api.jquery.com/remove/) - * - [`removeAttr()`](http://api.jquery.com/removeAttr/) - * - [`removeClass()`](http://api.jquery.com/removeClass/) - * - [`removeData()`](http://api.jquery.com/removeData/) - * - [`replaceWith()`](http://api.jquery.com/replaceWith/) - * - [`text()`](http://api.jquery.com/text/) - * - [`toggleClass()`](http://api.jquery.com/toggleClass/) - * - [`triggerHandler()`](http://api.jquery.com/triggerHandler/) - Passes a dummy event object to handlers. - * - [`unbind()`](http://api.jquery.com/unbind/) - Does not support namespaces - * - [`val()`](http://api.jquery.com/val/) - * - [`wrap()`](http://api.jquery.com/wrap/) - * - * ## jQuery/jqLite Extras - * Angular also provides the following additional methods and events to both jQuery and jqLite: - * - * ### Events - * - `$destroy` - AngularJS intercepts all jqLite/jQuery's DOM destruction apis and fires this event - * on all DOM nodes being removed. This can be used to clean up any 3rd party bindings to the DOM - * element before it is removed. - * - * ### Methods - * - `controller(name)` - retrieves the controller of the current element or its parent. By default - * retrieves controller associated with the `ngController` directive. If `name` is provided as - * camelCase directive name, then the controller for this directive will be retrieved (e.g. - * `'ngModel'`). - * - `injector()` - retrieves the injector of the current element or its parent. - * - `scope()` - retrieves the {@link ng.$rootScope.Scope scope} of the current - * element or its parent. Requires {@link guide/production#disabling-debug-data Debug Data} to - * be enabled. - * - `isolateScope()` - retrieves an isolate {@link ng.$rootScope.Scope scope} if one is attached directly to the - * current element. This getter should be used only on elements that contain a directive which starts a new isolate - * scope. Calling `scope()` on this element always returns the original non-isolate scope. - * Requires {@link guide/production#disabling-debug-data Debug Data} to be enabled. - * - `inheritedData()` - same as `data()`, but walks up the DOM until a value is found or the top - * parent element is reached. - * - * @param {string|DOMElement} element HTML string or DOMElement to be wrapped into jQuery. - * @returns {Object} jQuery object. - */ - -JQLite.expando = 'ng339'; - -var jqCache = JQLite.cache = {}, - jqId = 1, - addEventListenerFn = function(element, type, fn) { - element.addEventListener(type, fn, false); - }, - removeEventListenerFn = function(element, type, fn) { - element.removeEventListener(type, fn, false); - }; - -/* - * !!! This is an undocumented "private" function !!! - */ -JQLite._data = function(node) { - //jQuery always returns an object on cache miss - return this.cache[node[this.expando]] || {}; -}; - -function jqNextId() { return ++jqId; } - - -var SPECIAL_CHARS_REGEXP = /([\:\-\_]+(.))/g; -var MOZ_HACK_REGEXP = /^moz([A-Z])/; -var MOUSE_EVENT_MAP= { mouseleave: "mouseout", mouseenter: "mouseover"}; -var jqLiteMinErr = minErr('jqLite'); - -/** - * Converts snake_case to camelCase. - * Also there is special case for Moz prefix starting with upper case letter. - * @param name Name to normalize - */ -function camelCase(name) { - return name. - replace(SPECIAL_CHARS_REGEXP, function(_, separator, letter, offset) { - return offset ? letter.toUpperCase() : letter; - }). - replace(MOZ_HACK_REGEXP, 'Moz$1'); -} - -var SINGLE_TAG_REGEXP = /^<(\w+)\s*\/?>(?:<\/\1>|)$/; -var HTML_REGEXP = /<|&#?\w+;/; -var TAG_NAME_REGEXP = /<([\w:]+)/; -var XHTML_TAG_REGEXP = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi; - -var wrapMap = { - 'option': [1, ''], - - 'thead': [1, '', '
    '], - 'col': [2, '', '
    '], - 'tr': [2, '', '
    '], - 'td': [3, '', '
    '], - '_default': [0, "", ""] -}; - -wrapMap.optgroup = wrapMap.option; -wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead; -wrapMap.th = wrapMap.td; - - -function jqLiteIsTextNode(html) { - return !HTML_REGEXP.test(html); -} - -function jqLiteAcceptsData(node) { - // The window object can accept data but has no nodeType - // Otherwise we are only interested in elements (1) and documents (9) - var nodeType = node.nodeType; - return nodeType === NODE_TYPE_ELEMENT || !nodeType || nodeType === NODE_TYPE_DOCUMENT; -} - -function jqLiteHasData(node) { - for (var key in jqCache[node.ng339]) { - return true; - } - return false; -} - -function jqLiteBuildFragment(html, context) { - var tmp, tag, wrap, - fragment = context.createDocumentFragment(), - nodes = [], i; - - if (jqLiteIsTextNode(html)) { - // Convert non-html into a text node - nodes.push(context.createTextNode(html)); - } else { - // Convert html into DOM nodes - tmp = tmp || fragment.appendChild(context.createElement("div")); - tag = (TAG_NAME_REGEXP.exec(html) || ["", ""])[1].toLowerCase(); - wrap = wrapMap[tag] || wrapMap._default; - tmp.innerHTML = wrap[1] + html.replace(XHTML_TAG_REGEXP, "<$1>") + wrap[2]; - - // Descend through wrappers to the right content - i = wrap[0]; - while (i--) { - tmp = tmp.lastChild; - } - - nodes = concat(nodes, tmp.childNodes); - - tmp = fragment.firstChild; - tmp.textContent = ""; - } - - // Remove wrapper from fragment - fragment.textContent = ""; - fragment.innerHTML = ""; // Clear inner HTML - forEach(nodes, function(node) { - fragment.appendChild(node); - }); - - return fragment; -} - -function jqLiteParseHTML(html, context) { - context = context || document; - var parsed; - - if ((parsed = SINGLE_TAG_REGEXP.exec(html))) { - return [context.createElement(parsed[1])]; - } - - if ((parsed = jqLiteBuildFragment(html, context))) { - return parsed.childNodes; - } - - return []; -} - -///////////////////////////////////////////// -function JQLite(element) { - if (element instanceof JQLite) { - return element; - } - - var argIsString; - - if (isString(element)) { - element = trim(element); - argIsString = true; - } - if (!(this instanceof JQLite)) { - if (argIsString && element.charAt(0) != '<') { - throw jqLiteMinErr('nosel', 'Looking up elements via selectors is not supported by jqLite! See: http://docs.angularjs.org/api/angular.element'); - } - return new JQLite(element); - } - - if (argIsString) { - jqLiteAddNodes(this, jqLiteParseHTML(element)); - } else { - jqLiteAddNodes(this, element); - } -} - -function jqLiteClone(element) { - return element.cloneNode(true); -} - -function jqLiteDealoc(element, onlyDescendants) { - if (!onlyDescendants) jqLiteRemoveData(element); - - if (element.querySelectorAll) { - var descendants = element.querySelectorAll('*'); - for (var i = 0, l = descendants.length; i < l; i++) { - jqLiteRemoveData(descendants[i]); - } - } -} - -function jqLiteOff(element, type, fn, unsupported) { - if (isDefined(unsupported)) throw jqLiteMinErr('offargs', 'jqLite#off() does not support the `selector` argument'); - - var expandoStore = jqLiteExpandoStore(element); - var events = expandoStore && expandoStore.events; - var handle = expandoStore && expandoStore.handle; - - if (!handle) return; //no listeners registered - - if (!type) { - for (type in events) { - if (type !== '$destroy') { - removeEventListenerFn(element, type, handle); - } - delete events[type]; - } - } else { - forEach(type.split(' '), function(type) { - if (isDefined(fn)) { - var listenerFns = events[type]; - arrayRemove(listenerFns || [], fn); - if (listenerFns && listenerFns.length > 0) { - return; - } - } - - removeEventListenerFn(element, type, handle); - delete events[type]; - }); - } -} - -function jqLiteRemoveData(element, name) { - var expandoId = element.ng339; - var expandoStore = expandoId && jqCache[expandoId]; - - if (expandoStore) { - if (name) { - delete expandoStore.data[name]; - return; - } - - if (expandoStore.handle) { - if (expandoStore.events.$destroy) { - expandoStore.handle({}, '$destroy'); - } - jqLiteOff(element); - } - delete jqCache[expandoId]; - element.ng339 = undefined; // don't delete DOM expandos. IE and Chrome don't like it - } -} - - -function jqLiteExpandoStore(element, createIfNecessary) { - var expandoId = element.ng339, - expandoStore = expandoId && jqCache[expandoId]; - - if (createIfNecessary && !expandoStore) { - element.ng339 = expandoId = jqNextId(); - expandoStore = jqCache[expandoId] = {events: {}, data: {}, handle: undefined}; - } - - return expandoStore; -} - - -function jqLiteData(element, key, value) { - if (jqLiteAcceptsData(element)) { - - var isSimpleSetter = isDefined(value); - var isSimpleGetter = !isSimpleSetter && key && !isObject(key); - var massGetter = !key; - var expandoStore = jqLiteExpandoStore(element, !isSimpleGetter); - var data = expandoStore && expandoStore.data; - - if (isSimpleSetter) { // data('key', value) - data[key] = value; - } else { - if (massGetter) { // data() - return data; - } else { - if (isSimpleGetter) { // data('key') - // don't force creation of expandoStore if it doesn't exist yet - return data && data[key]; - } else { // mass-setter: data({key1: val1, key2: val2}) - extend(data, key); - } - } - } - } -} - -function jqLiteHasClass(element, selector) { - if (!element.getAttribute) return false; - return ((" " + (element.getAttribute('class') || '') + " ").replace(/[\n\t]/g, " "). - indexOf(" " + selector + " ") > -1); -} - -function jqLiteRemoveClass(element, cssClasses) { - if (cssClasses && element.setAttribute) { - forEach(cssClasses.split(' '), function(cssClass) { - element.setAttribute('class', trim( - (" " + (element.getAttribute('class') || '') + " ") - .replace(/[\n\t]/g, " ") - .replace(" " + trim(cssClass) + " ", " ")) - ); - }); - } -} - -function jqLiteAddClass(element, cssClasses) { - if (cssClasses && element.setAttribute) { - var existingClasses = (' ' + (element.getAttribute('class') || '') + ' ') - .replace(/[\n\t]/g, " "); - - forEach(cssClasses.split(' '), function(cssClass) { - cssClass = trim(cssClass); - if (existingClasses.indexOf(' ' + cssClass + ' ') === -1) { - existingClasses += cssClass + ' '; - } - }); - - element.setAttribute('class', trim(existingClasses)); - } -} - - -function jqLiteAddNodes(root, elements) { - // THIS CODE IS VERY HOT. Don't make changes without benchmarking. - - if (elements) { - - // if a Node (the most common case) - if (elements.nodeType) { - root[root.length++] = elements; - } else { - var length = elements.length; - - // if an Array or NodeList and not a Window - if (typeof length === 'number' && elements.window !== elements) { - if (length) { - for (var i = 0; i < length; i++) { - root[root.length++] = elements[i]; - } - } - } else { - root[root.length++] = elements; - } - } - } -} - - -function jqLiteController(element, name) { - return jqLiteInheritedData(element, '$' + (name || 'ngController') + 'Controller'); -} - -function jqLiteInheritedData(element, name, value) { - // if element is the document object work with the html element instead - // this makes $(document).scope() possible - if (element.nodeType == NODE_TYPE_DOCUMENT) { - element = element.documentElement; - } - var names = isArray(name) ? name : [name]; - - while (element) { - for (var i = 0, ii = names.length; i < ii; i++) { - if ((value = jqLite.data(element, names[i])) !== undefined) return value; - } - - // If dealing with a document fragment node with a host element, and no parent, use the host - // element as the parent. This enables directives within a Shadow DOM or polyfilled Shadow DOM - // to lookup parent controllers. - element = element.parentNode || (element.nodeType === NODE_TYPE_DOCUMENT_FRAGMENT && element.host); - } -} - -function jqLiteEmpty(element) { - jqLiteDealoc(element, true); - while (element.firstChild) { - element.removeChild(element.firstChild); - } -} - -function jqLiteRemove(element, keepData) { - if (!keepData) jqLiteDealoc(element); - var parent = element.parentNode; - if (parent) parent.removeChild(element); -} - - -function jqLiteDocumentLoaded(action, win) { - win = win || window; - if (win.document.readyState === 'complete') { - // Force the action to be run async for consistent behaviour - // from the action's point of view - // i.e. it will definitely not be in a $apply - win.setTimeout(action); - } else { - // No need to unbind this handler as load is only ever called once - jqLite(win).on('load', action); - } -} - -////////////////////////////////////////// -// Functions which are declared directly. -////////////////////////////////////////// -var JQLitePrototype = JQLite.prototype = { - ready: function(fn) { - var fired = false; - - function trigger() { - if (fired) return; - fired = true; - fn(); - } - - // check if document is already loaded - if (document.readyState === 'complete') { - setTimeout(trigger); - } else { - this.on('DOMContentLoaded', trigger); // works for modern browsers and IE9 - // we can not use jqLite since we are not done loading and jQuery could be loaded later. - // jshint -W064 - JQLite(window).on('load', trigger); // fallback to window.onload for others - // jshint +W064 - } - }, - toString: function() { - var value = []; - forEach(this, function(e) { value.push('' + e);}); - return '[' + value.join(', ') + ']'; - }, - - eq: function(index) { - return (index >= 0) ? jqLite(this[index]) : jqLite(this[this.length + index]); - }, - - length: 0, - push: push, - sort: [].sort, - splice: [].splice -}; - -////////////////////////////////////////// -// Functions iterating getter/setters. -// these functions return self on setter and -// value on get. -////////////////////////////////////////// -var BOOLEAN_ATTR = {}; -forEach('multiple,selected,checked,disabled,readOnly,required,open'.split(','), function(value) { - BOOLEAN_ATTR[lowercase(value)] = value; -}); -var BOOLEAN_ELEMENTS = {}; -forEach('input,select,option,textarea,button,form,details'.split(','), function(value) { - BOOLEAN_ELEMENTS[value] = true; -}); -var ALIASED_ATTR = { - 'ngMinlength': 'minlength', - 'ngMaxlength': 'maxlength', - 'ngMin': 'min', - 'ngMax': 'max', - 'ngPattern': 'pattern' -}; - -function getBooleanAttrName(element, name) { - // check dom last since we will most likely fail on name - var booleanAttr = BOOLEAN_ATTR[name.toLowerCase()]; - - // booleanAttr is here twice to minimize DOM access - return booleanAttr && BOOLEAN_ELEMENTS[nodeName_(element)] && booleanAttr; -} - -function getAliasedAttrName(element, name) { - var nodeName = element.nodeName; - return (nodeName === 'INPUT' || nodeName === 'TEXTAREA') && ALIASED_ATTR[name]; -} - -forEach({ - data: jqLiteData, - removeData: jqLiteRemoveData, - hasData: jqLiteHasData -}, function(fn, name) { - JQLite[name] = fn; -}); - -forEach({ - data: jqLiteData, - inheritedData: jqLiteInheritedData, - - scope: function(element) { - // Can't use jqLiteData here directly so we stay compatible with jQuery! - return jqLite.data(element, '$scope') || jqLiteInheritedData(element.parentNode || element, ['$isolateScope', '$scope']); - }, - - isolateScope: function(element) { - // Can't use jqLiteData here directly so we stay compatible with jQuery! - return jqLite.data(element, '$isolateScope') || jqLite.data(element, '$isolateScopeNoTemplate'); - }, - - controller: jqLiteController, - - injector: function(element) { - return jqLiteInheritedData(element, '$injector'); - }, - - removeAttr: function(element, name) { - element.removeAttribute(name); - }, - - hasClass: jqLiteHasClass, - - css: function(element, name, value) { - name = camelCase(name); - - if (isDefined(value)) { - element.style[name] = value; - } else { - return element.style[name]; - } - }, - - attr: function(element, name, value) { - var nodeType = element.nodeType; - if (nodeType === NODE_TYPE_TEXT || nodeType === NODE_TYPE_ATTRIBUTE || nodeType === NODE_TYPE_COMMENT) { - return; - } - var lowercasedName = lowercase(name); - if (BOOLEAN_ATTR[lowercasedName]) { - if (isDefined(value)) { - if (!!value) { - element[name] = true; - element.setAttribute(name, lowercasedName); - } else { - element[name] = false; - element.removeAttribute(lowercasedName); - } - } else { - return (element[name] || - (element.attributes.getNamedItem(name) || noop).specified) - ? lowercasedName - : undefined; - } - } else if (isDefined(value)) { - element.setAttribute(name, value); - } else if (element.getAttribute) { - // the extra argument "2" is to get the right thing for a.href in IE, see jQuery code - // some elements (e.g. Document) don't have get attribute, so return undefined - var ret = element.getAttribute(name, 2); - // normalize non-existing attributes to undefined (as jQuery) - return ret === null ? undefined : ret; - } - }, - - prop: function(element, name, value) { - if (isDefined(value)) { - element[name] = value; - } else { - return element[name]; - } - }, - - text: (function() { - getText.$dv = ''; - return getText; - - function getText(element, value) { - if (isUndefined(value)) { - var nodeType = element.nodeType; - return (nodeType === NODE_TYPE_ELEMENT || nodeType === NODE_TYPE_TEXT) ? element.textContent : ''; - } - element.textContent = value; - } - })(), - - val: function(element, value) { - if (isUndefined(value)) { - if (element.multiple && nodeName_(element) === 'select') { - var result = []; - forEach(element.options, function(option) { - if (option.selected) { - result.push(option.value || option.text); - } - }); - return result.length === 0 ? null : result; - } - return element.value; - } - element.value = value; - }, - - html: function(element, value) { - if (isUndefined(value)) { - return element.innerHTML; - } - jqLiteDealoc(element, true); - element.innerHTML = value; - }, - - empty: jqLiteEmpty -}, function(fn, name) { - /** - * Properties: writes return selection, reads return first value - */ - JQLite.prototype[name] = function(arg1, arg2) { - var i, key; - var nodeCount = this.length; - - // jqLiteHasClass has only two arguments, but is a getter-only fn, so we need to special-case it - // in a way that survives minification. - // jqLiteEmpty takes no arguments but is a setter. - if (fn !== jqLiteEmpty && - (((fn.length == 2 && (fn !== jqLiteHasClass && fn !== jqLiteController)) ? arg1 : arg2) === undefined)) { - if (isObject(arg1)) { - - // we are a write, but the object properties are the key/values - for (i = 0; i < nodeCount; i++) { - if (fn === jqLiteData) { - // data() takes the whole object in jQuery - fn(this[i], arg1); - } else { - for (key in arg1) { - fn(this[i], key, arg1[key]); - } - } - } - // return self for chaining - return this; - } else { - // we are a read, so read the first child. - // TODO: do we still need this? - var value = fn.$dv; - // Only if we have $dv do we iterate over all, otherwise it is just the first element. - var jj = (value === undefined) ? Math.min(nodeCount, 1) : nodeCount; - for (var j = 0; j < jj; j++) { - var nodeValue = fn(this[j], arg1, arg2); - value = value ? value + nodeValue : nodeValue; - } - return value; - } - } else { - // we are a write, so apply to all children - for (i = 0; i < nodeCount; i++) { - fn(this[i], arg1, arg2); - } - // return self for chaining - return this; - } - }; -}); - -function createEventHandler(element, events) { - var eventHandler = function(event, type) { - // jQuery specific api - event.isDefaultPrevented = function() { - return event.defaultPrevented; - }; - - var eventFns = events[type || event.type]; - var eventFnsLength = eventFns ? eventFns.length : 0; - - if (!eventFnsLength) return; - - if (isUndefined(event.immediatePropagationStopped)) { - var originalStopImmediatePropagation = event.stopImmediatePropagation; - event.stopImmediatePropagation = function() { - event.immediatePropagationStopped = true; - - if (event.stopPropagation) { - event.stopPropagation(); - } - - if (originalStopImmediatePropagation) { - originalStopImmediatePropagation.call(event); - } - }; - } - - event.isImmediatePropagationStopped = function() { - return event.immediatePropagationStopped === true; - }; - - // Copy event handlers in case event handlers array is modified during execution. - if ((eventFnsLength > 1)) { - eventFns = shallowCopy(eventFns); - } - - for (var i = 0; i < eventFnsLength; i++) { - if (!event.isImmediatePropagationStopped()) { - eventFns[i].call(element, event); - } - } - }; - - // TODO: this is a hack for angularMocks/clearDataCache that makes it possible to deregister all - // events on `element` - eventHandler.elem = element; - return eventHandler; -} - -////////////////////////////////////////// -// Functions iterating traversal. -// These functions chain results into a single -// selector. -////////////////////////////////////////// -forEach({ - removeData: jqLiteRemoveData, - - on: function jqLiteOn(element, type, fn, unsupported) { - if (isDefined(unsupported)) throw jqLiteMinErr('onargs', 'jqLite#on() does not support the `selector` or `eventData` parameters'); - - // Do not add event handlers to non-elements because they will not be cleaned up. - if (!jqLiteAcceptsData(element)) { - return; - } - - var expandoStore = jqLiteExpandoStore(element, true); - var events = expandoStore.events; - var handle = expandoStore.handle; - - if (!handle) { - handle = expandoStore.handle = createEventHandler(element, events); - } - - // http://jsperf.com/string-indexof-vs-split - var types = type.indexOf(' ') >= 0 ? type.split(' ') : [type]; - var i = types.length; - - while (i--) { - type = types[i]; - var eventFns = events[type]; - - if (!eventFns) { - events[type] = []; - - if (type === 'mouseenter' || type === 'mouseleave') { - // Refer to jQuery's implementation of mouseenter & mouseleave - // Read about mouseenter and mouseleave: - // http://www.quirksmode.org/js/events_mouse.html#link8 - - jqLiteOn(element, MOUSE_EVENT_MAP[type], function(event) { - var target = this, related = event.relatedTarget; - // For mousenter/leave call the handler if related is outside the target. - // NB: No relatedTarget if the mouse left/entered the browser window - if (!related || (related !== target && !target.contains(related))) { - handle(event, type); - } - }); - - } else { - if (type !== '$destroy') { - addEventListenerFn(element, type, handle); - } - } - eventFns = events[type]; - } - eventFns.push(fn); - } - }, - - off: jqLiteOff, - - one: function(element, type, fn) { - element = jqLite(element); - - //add the listener twice so that when it is called - //you can remove the original function and still be - //able to call element.off(ev, fn) normally - element.on(type, function onFn() { - element.off(type, fn); - element.off(type, onFn); - }); - element.on(type, fn); - }, - - replaceWith: function(element, replaceNode) { - var index, parent = element.parentNode; - jqLiteDealoc(element); - forEach(new JQLite(replaceNode), function(node) { - if (index) { - parent.insertBefore(node, index.nextSibling); - } else { - parent.replaceChild(node, element); - } - index = node; - }); - }, - - children: function(element) { - var children = []; - forEach(element.childNodes, function(element) { - if (element.nodeType === NODE_TYPE_ELEMENT) { - children.push(element); - } - }); - return children; - }, - - contents: function(element) { - return element.contentDocument || element.childNodes || []; - }, - - append: function(element, node) { - var nodeType = element.nodeType; - if (nodeType !== NODE_TYPE_ELEMENT && nodeType !== NODE_TYPE_DOCUMENT_FRAGMENT) return; - - node = new JQLite(node); - - for (var i = 0, ii = node.length; i < ii; i++) { - var child = node[i]; - element.appendChild(child); - } - }, - - prepend: function(element, node) { - if (element.nodeType === NODE_TYPE_ELEMENT) { - var index = element.firstChild; - forEach(new JQLite(node), function(child) { - element.insertBefore(child, index); - }); - } - }, - - wrap: function(element, wrapNode) { - wrapNode = jqLite(wrapNode).eq(0).clone()[0]; - var parent = element.parentNode; - if (parent) { - parent.replaceChild(wrapNode, element); - } - wrapNode.appendChild(element); - }, - - remove: jqLiteRemove, - - detach: function(element) { - jqLiteRemove(element, true); - }, - - after: function(element, newElement) { - var index = element, parent = element.parentNode; - newElement = new JQLite(newElement); - - for (var i = 0, ii = newElement.length; i < ii; i++) { - var node = newElement[i]; - parent.insertBefore(node, index.nextSibling); - index = node; - } - }, - - addClass: jqLiteAddClass, - removeClass: jqLiteRemoveClass, - - toggleClass: function(element, selector, condition) { - if (selector) { - forEach(selector.split(' '), function(className) { - var classCondition = condition; - if (isUndefined(classCondition)) { - classCondition = !jqLiteHasClass(element, className); - } - (classCondition ? jqLiteAddClass : jqLiteRemoveClass)(element, className); - }); - } - }, - - parent: function(element) { - var parent = element.parentNode; - return parent && parent.nodeType !== NODE_TYPE_DOCUMENT_FRAGMENT ? parent : null; - }, - - next: function(element) { - return element.nextElementSibling; - }, - - find: function(element, selector) { - if (element.getElementsByTagName) { - return element.getElementsByTagName(selector); - } else { - return []; - } - }, - - clone: jqLiteClone, - - triggerHandler: function(element, event, extraParameters) { - - var dummyEvent, eventFnsCopy, handlerArgs; - var eventName = event.type || event; - var expandoStore = jqLiteExpandoStore(element); - var events = expandoStore && expandoStore.events; - var eventFns = events && events[eventName]; - - if (eventFns) { - // Create a dummy event to pass to the handlers - dummyEvent = { - preventDefault: function() { this.defaultPrevented = true; }, - isDefaultPrevented: function() { return this.defaultPrevented === true; }, - stopImmediatePropagation: function() { this.immediatePropagationStopped = true; }, - isImmediatePropagationStopped: function() { return this.immediatePropagationStopped === true; }, - stopPropagation: noop, - type: eventName, - target: element - }; - - // If a custom event was provided then extend our dummy event with it - if (event.type) { - dummyEvent = extend(dummyEvent, event); - } - - // Copy event handlers in case event handlers array is modified during execution. - eventFnsCopy = shallowCopy(eventFns); - handlerArgs = extraParameters ? [dummyEvent].concat(extraParameters) : [dummyEvent]; - - forEach(eventFnsCopy, function(fn) { - if (!dummyEvent.isImmediatePropagationStopped()) { - fn.apply(element, handlerArgs); - } - }); - } - } -}, function(fn, name) { - /** - * chaining functions - */ - JQLite.prototype[name] = function(arg1, arg2, arg3) { - var value; - - for (var i = 0, ii = this.length; i < ii; i++) { - if (isUndefined(value)) { - value = fn(this[i], arg1, arg2, arg3); - if (isDefined(value)) { - // any function which returns a value needs to be wrapped - value = jqLite(value); - } - } else { - jqLiteAddNodes(value, fn(this[i], arg1, arg2, arg3)); - } - } - return isDefined(value) ? value : this; - }; - - // bind legacy bind/unbind to on/off - JQLite.prototype.bind = JQLite.prototype.on; - JQLite.prototype.unbind = JQLite.prototype.off; -}); - - -// Provider for private $$jqLite service -function $$jqLiteProvider() { - this.$get = function $$jqLite() { - return extend(JQLite, { - hasClass: function(node, classes) { - if (node.attr) node = node[0]; - return jqLiteHasClass(node, classes); - }, - addClass: function(node, classes) { - if (node.attr) node = node[0]; - return jqLiteAddClass(node, classes); - }, - removeClass: function(node, classes) { - if (node.attr) node = node[0]; - return jqLiteRemoveClass(node, classes); - } - }); - }; -} - -/** - * Computes a hash of an 'obj'. - * Hash of a: - * string is string - * number is number as string - * object is either result of calling $$hashKey function on the object or uniquely generated id, - * that is also assigned to the $$hashKey property of the object. - * - * @param obj - * @returns {string} hash string such that the same input will have the same hash string. - * The resulting string key is in 'type:hashKey' format. - */ -function hashKey(obj, nextUidFn) { - var key = obj && obj.$$hashKey; - - if (key) { - if (typeof key === 'function') { - key = obj.$$hashKey(); - } - return key; - } - - var objType = typeof obj; - if (objType == 'function' || (objType == 'object' && obj !== null)) { - key = obj.$$hashKey = objType + ':' + (nextUidFn || nextUid)(); - } else { - key = objType + ':' + obj; - } - - return key; -} - -/** - * HashMap which can use objects as keys - */ -function HashMap(array, isolatedUid) { - if (isolatedUid) { - var uid = 0; - this.nextUid = function() { - return ++uid; - }; - } - forEach(array, this.put, this); -} -HashMap.prototype = { - /** - * Store key value pair - * @param key key to store can be any type - * @param value value to store can be any type - */ - put: function(key, value) { - this[hashKey(key, this.nextUid)] = value; - }, - - /** - * @param key - * @returns {Object} the value for the key - */ - get: function(key) { - return this[hashKey(key, this.nextUid)]; - }, - - /** - * Remove the key/value pair - * @param key - */ - remove: function(key) { - var value = this[key = hashKey(key, this.nextUid)]; - delete this[key]; - return value; - } -}; - -var $$HashMapProvider = [function() { - this.$get = [function() { - return HashMap; - }]; -}]; - -/** - * @ngdoc function - * @module ng - * @name angular.injector - * @kind function - * - * @description - * Creates an injector object that can be used for retrieving services as well as for - * dependency injection (see {@link guide/di dependency injection}). - * - * @param {Array.} modules A list of module functions or their aliases. See - * {@link angular.module}. The `ng` module must be explicitly added. - * @param {boolean=} [strictDi=false] Whether the injector should be in strict mode, which - * disallows argument name annotation inference. - * @returns {injector} Injector object. See {@link auto.$injector $injector}. - * - * @example - * Typical usage - * ```js - * // create an injector - * var $injector = angular.injector(['ng']); - * - * // use the injector to kick off your application - * // use the type inference to auto inject arguments, or use implicit injection - * $injector.invoke(function($rootScope, $compile, $document) { - * $compile($document)($rootScope); - * $rootScope.$digest(); - * }); - * ``` - * - * Sometimes you want to get access to the injector of a currently running Angular app - * from outside Angular. Perhaps, you want to inject and compile some markup after the - * application has been bootstrapped. You can do this using the extra `injector()` added - * to JQuery/jqLite elements. See {@link angular.element}. - * - * *This is fairly rare but could be the case if a third party library is injecting the - * markup.* - * - * In the following example a new block of HTML containing a `ng-controller` - * directive is added to the end of the document body by JQuery. We then compile and link - * it into the current AngularJS scope. - * - * ```js - * var $div = $('
    {{content.label}}
    '); - * $(document.body).append($div); - * - * angular.element(document).injector().invoke(function($compile) { - * var scope = angular.element($div).scope(); - * $compile($div)(scope); - * }); - * ``` - */ - - -/** - * @ngdoc module - * @name auto - * @description - * - * Implicit module which gets automatically added to each {@link auto.$injector $injector}. - */ - -var FN_ARGS = /^[^\(]*\(\s*([^\)]*)\)/m; -var FN_ARG_SPLIT = /,/; -var FN_ARG = /^\s*(_?)(\S+?)\1\s*$/; -var STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg; -var $injectorMinErr = minErr('$injector'); - -function anonFn(fn) { - // For anonymous functions, showing at the very least the function signature can help in - // debugging. - var fnText = fn.toString().replace(STRIP_COMMENTS, ''), - args = fnText.match(FN_ARGS); - if (args) { - return 'function(' + (args[1] || '').replace(/[\s\r\n]+/, ' ') + ')'; - } - return 'fn'; -} - -function annotate(fn, strictDi, name) { - var $inject, - fnText, - argDecl, - last; - - if (typeof fn === 'function') { - if (!($inject = fn.$inject)) { - $inject = []; - if (fn.length) { - if (strictDi) { - if (!isString(name) || !name) { - name = fn.name || anonFn(fn); - } - throw $injectorMinErr('strictdi', - '{0} is not using explicit annotation and cannot be invoked in strict mode', name); - } - fnText = fn.toString().replace(STRIP_COMMENTS, ''); - argDecl = fnText.match(FN_ARGS); - forEach(argDecl[1].split(FN_ARG_SPLIT), function(arg) { - arg.replace(FN_ARG, function(all, underscore, name) { - $inject.push(name); - }); - }); - } - fn.$inject = $inject; - } - } else if (isArray(fn)) { - last = fn.length - 1; - assertArgFn(fn[last], 'fn'); - $inject = fn.slice(0, last); - } else { - assertArgFn(fn, 'fn', true); - } - return $inject; -} - -/////////////////////////////////////// - -/** - * @ngdoc service - * @name $injector - * - * @description - * - * `$injector` is used to retrieve object instances as defined by - * {@link auto.$provide provider}, instantiate types, invoke methods, - * and load modules. - * - * The following always holds true: - * - * ```js - * var $injector = angular.injector(); - * expect($injector.get('$injector')).toBe($injector); - * expect($injector.invoke(function($injector) { - * return $injector; - * })).toBe($injector); - * ``` - * - * # Injection Function Annotation - * - * JavaScript does not have annotations, and annotations are needed for dependency injection. The - * following are all valid ways of annotating function with injection arguments and are equivalent. - * - * ```js - * // inferred (only works if code not minified/obfuscated) - * $injector.invoke(function(serviceA){}); - * - * // annotated - * function explicit(serviceA) {}; - * explicit.$inject = ['serviceA']; - * $injector.invoke(explicit); - * - * // inline - * $injector.invoke(['serviceA', function(serviceA){}]); - * ``` - * - * ## Inference - * - * In JavaScript calling `toString()` on a function returns the function definition. The definition - * can then be parsed and the function arguments can be extracted. This method of discovering - * annotations is disallowed when the injector is in strict mode. - * *NOTE:* This does not work with minification, and obfuscation tools since these tools change the - * argument names. - * - * ## `$inject` Annotation - * By adding an `$inject` property onto a function the injection parameters can be specified. - * - * ## Inline - * As an array of injection names, where the last item in the array is the function to call. - */ - -/** - * @ngdoc method - * @name $injector#get - * - * @description - * Return an instance of the service. - * - * @param {string} name The name of the instance to retrieve. - * @param {string=} caller An optional string to provide the origin of the function call for error messages. - * @return {*} The instance. - */ - -/** - * @ngdoc method - * @name $injector#invoke - * - * @description - * Invoke the method and supply the method arguments from the `$injector`. - * - * @param {Function|Array.} fn The injectable function to invoke. Function parameters are - * injected according to the {@link guide/di $inject Annotation} rules. - * @param {Object=} self The `this` for the invoked method. - * @param {Object=} locals Optional object. If preset then any argument names are read from this - * object first, before the `$injector` is consulted. - * @returns {*} the value returned by the invoked `fn` function. - */ - -/** - * @ngdoc method - * @name $injector#has - * - * @description - * Allows the user to query if the particular service exists. - * - * @param {string} name Name of the service to query. - * @returns {boolean} `true` if injector has given service. - */ - -/** - * @ngdoc method - * @name $injector#instantiate - * @description - * Create a new instance of JS type. The method takes a constructor function, invokes the new - * operator, and supplies all of the arguments to the constructor function as specified by the - * constructor annotation. - * - * @param {Function} Type Annotated constructor function. - * @param {Object=} locals Optional object. If preset then any argument names are read from this - * object first, before the `$injector` is consulted. - * @returns {Object} new instance of `Type`. - */ - -/** - * @ngdoc method - * @name $injector#annotate - * - * @description - * Returns an array of service names which the function is requesting for injection. This API is - * used by the injector to determine which services need to be injected into the function when the - * function is invoked. There are three ways in which the function can be annotated with the needed - * dependencies. - * - * # Argument names - * - * The simplest form is to extract the dependencies from the arguments of the function. This is done - * by converting the function into a string using `toString()` method and extracting the argument - * names. - * ```js - * // Given - * function MyController($scope, $route) { - * // ... - * } - * - * // Then - * expect(injector.annotate(MyController)).toEqual(['$scope', '$route']); - * ``` - * - * You can disallow this method by using strict injection mode. - * - * This method does not work with code minification / obfuscation. For this reason the following - * annotation strategies are supported. - * - * # The `$inject` property - * - * If a function has an `$inject` property and its value is an array of strings, then the strings - * represent names of services to be injected into the function. - * ```js - * // Given - * var MyController = function(obfuscatedScope, obfuscatedRoute) { - * // ... - * } - * // Define function dependencies - * MyController['$inject'] = ['$scope', '$route']; - * - * // Then - * expect(injector.annotate(MyController)).toEqual(['$scope', '$route']); - * ``` - * - * # The array notation - * - * It is often desirable to inline Injected functions and that's when setting the `$inject` property - * is very inconvenient. In these situations using the array notation to specify the dependencies in - * a way that survives minification is a better choice: - * - * ```js - * // We wish to write this (not minification / obfuscation safe) - * injector.invoke(function($compile, $rootScope) { - * // ... - * }); - * - * // We are forced to write break inlining - * var tmpFn = function(obfuscatedCompile, obfuscatedRootScope) { - * // ... - * }; - * tmpFn.$inject = ['$compile', '$rootScope']; - * injector.invoke(tmpFn); - * - * // To better support inline function the inline annotation is supported - * injector.invoke(['$compile', '$rootScope', function(obfCompile, obfRootScope) { - * // ... - * }]); - * - * // Therefore - * expect(injector.annotate( - * ['$compile', '$rootScope', function(obfus_$compile, obfus_$rootScope) {}]) - * ).toEqual(['$compile', '$rootScope']); - * ``` - * - * @param {Function|Array.} fn Function for which dependent service names need to - * be retrieved as described above. - * - * @param {boolean=} [strictDi=false] Disallow argument name annotation inference. - * - * @returns {Array.} The names of the services which the function requires. - */ - - - - -/** - * @ngdoc service - * @name $provide - * - * @description - * - * The {@link auto.$provide $provide} service has a number of methods for registering components - * with the {@link auto.$injector $injector}. Many of these functions are also exposed on - * {@link angular.Module}. - * - * An Angular **service** is a singleton object created by a **service factory**. These **service - * factories** are functions which, in turn, are created by a **service provider**. - * The **service providers** are constructor functions. When instantiated they must contain a - * property called `$get`, which holds the **service factory** function. - * - * When you request a service, the {@link auto.$injector $injector} is responsible for finding the - * correct **service provider**, instantiating it and then calling its `$get` **service factory** - * function to get the instance of the **service**. - * - * Often services have no configuration options and there is no need to add methods to the service - * provider. The provider will be no more than a constructor function with a `$get` property. For - * these cases the {@link auto.$provide $provide} service has additional helper methods to register - * services without specifying a provider. - * - * * {@link auto.$provide#provider provider(provider)} - registers a **service provider** with the - * {@link auto.$injector $injector} - * * {@link auto.$provide#constant constant(obj)} - registers a value/object that can be accessed by - * providers and services. - * * {@link auto.$provide#value value(obj)} - registers a value/object that can only be accessed by - * services, not providers. - * * {@link auto.$provide#factory factory(fn)} - registers a service **factory function**, `fn`, - * that will be wrapped in a **service provider** object, whose `$get` property will contain the - * given factory function. - * * {@link auto.$provide#service service(class)} - registers a **constructor function**, `class` - * that will be wrapped in a **service provider** object, whose `$get` property will instantiate - * a new object using the given constructor function. - * - * See the individual methods for more information and examples. - */ - -/** - * @ngdoc method - * @name $provide#provider - * @description - * - * Register a **provider function** with the {@link auto.$injector $injector}. Provider functions - * are constructor functions, whose instances are responsible for "providing" a factory for a - * service. - * - * Service provider names start with the name of the service they provide followed by `Provider`. - * For example, the {@link ng.$log $log} service has a provider called - * {@link ng.$logProvider $logProvider}. - * - * Service provider objects can have additional methods which allow configuration of the provider - * and its service. Importantly, you can configure what kind of service is created by the `$get` - * method, or how that service will act. For example, the {@link ng.$logProvider $logProvider} has a - * method {@link ng.$logProvider#debugEnabled debugEnabled} - * which lets you specify whether the {@link ng.$log $log} service will log debug messages to the - * console or not. - * - * @param {string} name The name of the instance. NOTE: the provider will be available under `name + - 'Provider'` key. - * @param {(Object|function())} provider If the provider is: - * - * - `Object`: then it should have a `$get` method. The `$get` method will be invoked using - * {@link auto.$injector#invoke $injector.invoke()} when an instance needs to be created. - * - `Constructor`: a new instance of the provider will be created using - * {@link auto.$injector#instantiate $injector.instantiate()}, then treated as `object`. - * - * @returns {Object} registered provider instance - - * @example - * - * The following example shows how to create a simple event tracking service and register it using - * {@link auto.$provide#provider $provide.provider()}. - * - * ```js - * // Define the eventTracker provider - * function EventTrackerProvider() { - * var trackingUrl = '/track'; - * - * // A provider method for configuring where the tracked events should been saved - * this.setTrackingUrl = function(url) { - * trackingUrl = url; - * }; - * - * // The service factory function - * this.$get = ['$http', function($http) { - * var trackedEvents = {}; - * return { - * // Call this to track an event - * event: function(event) { - * var count = trackedEvents[event] || 0; - * count += 1; - * trackedEvents[event] = count; - * return count; - * }, - * // Call this to save the tracked events to the trackingUrl - * save: function() { - * $http.post(trackingUrl, trackedEvents); - * } - * }; - * }]; - * } - * - * describe('eventTracker', function() { - * var postSpy; - * - * beforeEach(module(function($provide) { - * // Register the eventTracker provider - * $provide.provider('eventTracker', EventTrackerProvider); - * })); - * - * beforeEach(module(function(eventTrackerProvider) { - * // Configure eventTracker provider - * eventTrackerProvider.setTrackingUrl('/custom-track'); - * })); - * - * it('tracks events', inject(function(eventTracker) { - * expect(eventTracker.event('login')).toEqual(1); - * expect(eventTracker.event('login')).toEqual(2); - * })); - * - * it('saves to the tracking url', inject(function(eventTracker, $http) { - * postSpy = spyOn($http, 'post'); - * eventTracker.event('login'); - * eventTracker.save(); - * expect(postSpy).toHaveBeenCalled(); - * expect(postSpy.mostRecentCall.args[0]).not.toEqual('/track'); - * expect(postSpy.mostRecentCall.args[0]).toEqual('/custom-track'); - * expect(postSpy.mostRecentCall.args[1]).toEqual({ 'login': 1 }); - * })); - * }); - * ``` - */ - -/** - * @ngdoc method - * @name $provide#factory - * @description - * - * Register a **service factory**, which will be called to return the service instance. - * This is short for registering a service where its provider consists of only a `$get` property, - * which is the given service factory function. - * You should use {@link auto.$provide#factory $provide.factory(getFn)} if you do not need to - * configure your service in a provider. - * - * @param {string} name The name of the instance. - * @param {Function|Array.} $getFn The injectable $getFn for the instance creation. - * Internally this is a short hand for `$provide.provider(name, {$get: $getFn})`. - * @returns {Object} registered provider instance - * - * @example - * Here is an example of registering a service - * ```js - * $provide.factory('ping', ['$http', function($http) { - * return function ping() { - * return $http.send('/ping'); - * }; - * }]); - * ``` - * You would then inject and use this service like this: - * ```js - * someModule.controller('Ctrl', ['ping', function(ping) { - * ping(); - * }]); - * ``` - */ - - -/** - * @ngdoc method - * @name $provide#service - * @description - * - * Register a **service constructor**, which will be invoked with `new` to create the service - * instance. - * This is short for registering a service where its provider's `$get` property is the service - * constructor function that will be used to instantiate the service instance. - * - * You should use {@link auto.$provide#service $provide.service(class)} if you define your service - * as a type/class. - * - * @param {string} name The name of the instance. - * @param {Function|Array.} constructor An injectable class (constructor function) - * that will be instantiated. - * @returns {Object} registered provider instance - * - * @example - * Here is an example of registering a service using - * {@link auto.$provide#service $provide.service(class)}. - * ```js - * var Ping = function($http) { - * this.$http = $http; - * }; - * - * Ping.$inject = ['$http']; - * - * Ping.prototype.send = function() { - * return this.$http.get('/ping'); - * }; - * $provide.service('ping', Ping); - * ``` - * You would then inject and use this service like this: - * ```js - * someModule.controller('Ctrl', ['ping', function(ping) { - * ping.send(); - * }]); - * ``` - */ - - -/** - * @ngdoc method - * @name $provide#value - * @description - * - * Register a **value service** with the {@link auto.$injector $injector}, such as a string, a - * number, an array, an object or a function. This is short for registering a service where its - * provider's `$get` property is a factory function that takes no arguments and returns the **value - * service**. - * - * Value services are similar to constant services, except that they cannot be injected into a - * module configuration function (see {@link angular.Module#config}) but they can be overridden by - * an Angular - * {@link auto.$provide#decorator decorator}. - * - * @param {string} name The name of the instance. - * @param {*} value The value. - * @returns {Object} registered provider instance - * - * @example - * Here are some examples of creating value services. - * ```js - * $provide.value('ADMIN_USER', 'admin'); - * - * $provide.value('RoleLookup', { admin: 0, writer: 1, reader: 2 }); - * - * $provide.value('halfOf', function(value) { - * return value / 2; - * }); - * ``` - */ - - -/** - * @ngdoc method - * @name $provide#constant - * @description - * - * Register a **constant service**, such as a string, a number, an array, an object or a function, - * with the {@link auto.$injector $injector}. Unlike {@link auto.$provide#value value} it can be - * injected into a module configuration function (see {@link angular.Module#config}) and it cannot - * be overridden by an Angular {@link auto.$provide#decorator decorator}. - * - * @param {string} name The name of the constant. - * @param {*} value The constant value. - * @returns {Object} registered instance - * - * @example - * Here a some examples of creating constants: - * ```js - * $provide.constant('SHARD_HEIGHT', 306); - * - * $provide.constant('MY_COLOURS', ['red', 'blue', 'grey']); - * - * $provide.constant('double', function(value) { - * return value * 2; - * }); - * ``` - */ - - -/** - * @ngdoc method - * @name $provide#decorator - * @description - * - * Register a **service decorator** with the {@link auto.$injector $injector}. A service decorator - * intercepts the creation of a service, allowing it to override or modify the behaviour of the - * service. The object returned by the decorator may be the original service, or a new service - * object which replaces or wraps and delegates to the original service. - * - * @param {string} name The name of the service to decorate. - * @param {Function|Array.} decorator This function will be invoked when the service needs to be - * instantiated and should return the decorated service instance. The function is called using - * the {@link auto.$injector#invoke injector.invoke} method and is therefore fully injectable. - * Local injection arguments: - * - * * `$delegate` - The original service instance, which can be monkey patched, configured, - * decorated or delegated to. - * - * @example - * Here we decorate the {@link ng.$log $log} service to convert warnings to errors by intercepting - * calls to {@link ng.$log#error $log.warn()}. - * ```js - * $provide.decorator('$log', ['$delegate', function($delegate) { - * $delegate.warn = $delegate.error; - * return $delegate; - * }]); - * ``` - */ - - -function createInjector(modulesToLoad, strictDi) { - strictDi = (strictDi === true); - var INSTANTIATING = {}, - providerSuffix = 'Provider', - path = [], - loadedModules = new HashMap([], true), - providerCache = { - $provide: { - provider: supportObject(provider), - factory: supportObject(factory), - service: supportObject(service), - value: supportObject(value), - constant: supportObject(constant), - decorator: decorator - } - }, - providerInjector = (providerCache.$injector = - createInternalInjector(providerCache, function(serviceName, caller) { - if (angular.isString(caller)) { - path.push(caller); - } - throw $injectorMinErr('unpr', "Unknown provider: {0}", path.join(' <- ')); - })), - instanceCache = {}, - instanceInjector = (instanceCache.$injector = - createInternalInjector(instanceCache, function(serviceName, caller) { - var provider = providerInjector.get(serviceName + providerSuffix, caller); - return instanceInjector.invoke(provider.$get, provider, undefined, serviceName); - })); - - - forEach(loadModules(modulesToLoad), function(fn) { if (fn) instanceInjector.invoke(fn); }); - - return instanceInjector; - - //////////////////////////////////// - // $provider - //////////////////////////////////// - - function supportObject(delegate) { - return function(key, value) { - if (isObject(key)) { - forEach(key, reverseParams(delegate)); - } else { - return delegate(key, value); - } - }; - } - - function provider(name, provider_) { - assertNotHasOwnProperty(name, 'service'); - if (isFunction(provider_) || isArray(provider_)) { - provider_ = providerInjector.instantiate(provider_); - } - if (!provider_.$get) { - throw $injectorMinErr('pget', "Provider '{0}' must define $get factory method.", name); - } - return providerCache[name + providerSuffix] = provider_; - } - - function enforceReturnValue(name, factory) { - return function enforcedReturnValue() { - var result = instanceInjector.invoke(factory, this); - if (isUndefined(result)) { - throw $injectorMinErr('undef', "Provider '{0}' must return a value from $get factory method.", name); - } - return result; - }; - } - - function factory(name, factoryFn, enforce) { - return provider(name, { - $get: enforce !== false ? enforceReturnValue(name, factoryFn) : factoryFn - }); - } - - function service(name, constructor) { - return factory(name, ['$injector', function($injector) { - return $injector.instantiate(constructor); - }]); - } - - function value(name, val) { return factory(name, valueFn(val), false); } - - function constant(name, value) { - assertNotHasOwnProperty(name, 'constant'); - providerCache[name] = value; - instanceCache[name] = value; - } - - function decorator(serviceName, decorFn) { - var origProvider = providerInjector.get(serviceName + providerSuffix), - orig$get = origProvider.$get; - - origProvider.$get = function() { - var origInstance = instanceInjector.invoke(orig$get, origProvider); - return instanceInjector.invoke(decorFn, null, {$delegate: origInstance}); - }; - } - - //////////////////////////////////// - // Module Loading - //////////////////////////////////// - function loadModules(modulesToLoad) { - assertArg(isUndefined(modulesToLoad) || isArray(modulesToLoad), 'modulesToLoad', 'not an array'); - var runBlocks = [], moduleFn; - forEach(modulesToLoad, function(module) { - if (loadedModules.get(module)) return; - loadedModules.put(module, true); - - function runInvokeQueue(queue) { - var i, ii; - for (i = 0, ii = queue.length; i < ii; i++) { - var invokeArgs = queue[i], - provider = providerInjector.get(invokeArgs[0]); - - provider[invokeArgs[1]].apply(provider, invokeArgs[2]); - } - } - - try { - if (isString(module)) { - moduleFn = angularModule(module); - runBlocks = runBlocks.concat(loadModules(moduleFn.requires)).concat(moduleFn._runBlocks); - runInvokeQueue(moduleFn._invokeQueue); - runInvokeQueue(moduleFn._configBlocks); - } else if (isFunction(module)) { - runBlocks.push(providerInjector.invoke(module)); - } else if (isArray(module)) { - runBlocks.push(providerInjector.invoke(module)); - } else { - assertArgFn(module, 'module'); - } - } catch (e) { - if (isArray(module)) { - module = module[module.length - 1]; - } - if (e.message && e.stack && e.stack.indexOf(e.message) == -1) { - // Safari & FF's stack traces don't contain error.message content - // unlike those of Chrome and IE - // So if stack doesn't contain message, we create a new string that contains both. - // Since error.stack is read-only in Safari, I'm overriding e and not e.stack here. - /* jshint -W022 */ - e = e.message + '\n' + e.stack; - } - throw $injectorMinErr('modulerr', "Failed to instantiate module {0} due to:\n{1}", - module, e.stack || e.message || e); - } - }); - return runBlocks; - } - - //////////////////////////////////// - // internal Injector - //////////////////////////////////// - - function createInternalInjector(cache, factory) { - - function getService(serviceName, caller) { - if (cache.hasOwnProperty(serviceName)) { - if (cache[serviceName] === INSTANTIATING) { - throw $injectorMinErr('cdep', 'Circular dependency found: {0}', - serviceName + ' <- ' + path.join(' <- ')); - } - return cache[serviceName]; - } else { - try { - path.unshift(serviceName); - cache[serviceName] = INSTANTIATING; - return cache[serviceName] = factory(serviceName, caller); - } catch (err) { - if (cache[serviceName] === INSTANTIATING) { - delete cache[serviceName]; - } - throw err; - } finally { - path.shift(); - } - } - } - - function invoke(fn, self, locals, serviceName) { - if (typeof locals === 'string') { - serviceName = locals; - locals = null; - } - - var args = [], - $inject = createInjector.$$annotate(fn, strictDi, serviceName), - length, i, - key; - - for (i = 0, length = $inject.length; i < length; i++) { - key = $inject[i]; - if (typeof key !== 'string') { - throw $injectorMinErr('itkn', - 'Incorrect injection token! Expected service name as string, got {0}', key); - } - args.push( - locals && locals.hasOwnProperty(key) - ? locals[key] - : getService(key, serviceName) - ); - } - if (isArray(fn)) { - fn = fn[length]; - } - - // http://jsperf.com/angularjs-invoke-apply-vs-switch - // #5388 - return fn.apply(self, args); - } - - function instantiate(Type, locals, serviceName) { - // Check if Type is annotated and use just the given function at n-1 as parameter - // e.g. someModule.factory('greeter', ['$window', function(renamed$window) {}]); - // Object creation: http://jsperf.com/create-constructor/2 - var instance = Object.create((isArray(Type) ? Type[Type.length - 1] : Type).prototype || null); - var returnedValue = invoke(Type, instance, locals, serviceName); - - return isObject(returnedValue) || isFunction(returnedValue) ? returnedValue : instance; - } - - return { - invoke: invoke, - instantiate: instantiate, - get: getService, - annotate: createInjector.$$annotate, - has: function(name) { - return providerCache.hasOwnProperty(name + providerSuffix) || cache.hasOwnProperty(name); - } - }; - } -} - -createInjector.$$annotate = annotate; - -/** - * @ngdoc provider - * @name $anchorScrollProvider - * - * @description - * Use `$anchorScrollProvider` to disable automatic scrolling whenever - * {@link ng.$location#hash $location.hash()} changes. - */ -function $AnchorScrollProvider() { - - var autoScrollingEnabled = true; - - /** - * @ngdoc method - * @name $anchorScrollProvider#disableAutoScrolling - * - * @description - * By default, {@link ng.$anchorScroll $anchorScroll()} will automatically detect changes to - * {@link ng.$location#hash $location.hash()} and scroll to the element matching the new hash.
    - * Use this method to disable automatic scrolling. - * - * If automatic scrolling is disabled, one must explicitly call - * {@link ng.$anchorScroll $anchorScroll()} in order to scroll to the element related to the - * current hash. - */ - this.disableAutoScrolling = function() { - autoScrollingEnabled = false; - }; - - /** - * @ngdoc service - * @name $anchorScroll - * @kind function - * @requires $window - * @requires $location - * @requires $rootScope - * - * @description - * When called, it scrolls to the element related to the specified `hash` or (if omitted) to the - * current value of {@link ng.$location#hash $location.hash()}, according to the rules specified - * in the - * [HTML5 spec](http://dev.w3.org/html5/spec/Overview.html#the-indicated-part-of-the-document). - * - * It also watches the {@link ng.$location#hash $location.hash()} and automatically scrolls to - * match any anchor whenever it changes. This can be disabled by calling - * {@link ng.$anchorScrollProvider#disableAutoScrolling $anchorScrollProvider.disableAutoScrolling()}. - * - * Additionally, you can use its {@link ng.$anchorScroll#yOffset yOffset} property to specify a - * vertical scroll-offset (either fixed or dynamic). - * - * @param {string=} hash The hash specifying the element to scroll to. If omitted, the value of - * {@link ng.$location#hash $location.hash()} will be used. - * - * @property {(number|function|jqLite)} yOffset - * If set, specifies a vertical scroll-offset. This is often useful when there are fixed - * positioned elements at the top of the page, such as navbars, headers etc. - * - * `yOffset` can be specified in various ways: - * - **number**: A fixed number of pixels to be used as offset.

    - * - **function**: A getter function called everytime `$anchorScroll()` is executed. Must return - * a number representing the offset (in pixels).

    - * - **jqLite**: A jqLite/jQuery element to be used for specifying the offset. The distance from - * the top of the page to the element's bottom will be used as offset.
    - * **Note**: The element will be taken into account only as long as its `position` is set to - * `fixed`. This option is useful, when dealing with responsive navbars/headers that adjust - * their height and/or positioning according to the viewport's size. - * - *
    - *
    - * In order for `yOffset` to work properly, scrolling should take place on the document's root and - * not some child element. - *
    - * - * @example - - -
    - Go to bottom - You're at the bottom! -
    -
    - - angular.module('anchorScrollExample', []) - .controller('ScrollController', ['$scope', '$location', '$anchorScroll', - function ($scope, $location, $anchorScroll) { - $scope.gotoBottom = function() { - // set the location.hash to the id of - // the element you wish to scroll to. - $location.hash('bottom'); - - // call $anchorScroll() - $anchorScroll(); - }; - }]); - - - #scrollArea { - height: 280px; - overflow: auto; - } - - #bottom { - display: block; - margin-top: 2000px; - } - -
    - * - *
    - * The example below illustrates the use of a vertical scroll-offset (specified as a fixed value). - * See {@link ng.$anchorScroll#yOffset $anchorScroll.yOffset} for more details. - * - * @example - - - -
    - Anchor {{x}} of 5 -
    -
    - - angular.module('anchorScrollOffsetExample', []) - .run(['$anchorScroll', function($anchorScroll) { - $anchorScroll.yOffset = 50; // always scroll by 50 extra pixels - }]) - .controller('headerCtrl', ['$anchorScroll', '$location', '$scope', - function ($anchorScroll, $location, $scope) { - $scope.gotoAnchor = function(x) { - var newHash = 'anchor' + x; - if ($location.hash() !== newHash) { - // set the $location.hash to `newHash` and - // $anchorScroll will automatically scroll to it - $location.hash('anchor' + x); - } else { - // call $anchorScroll() explicitly, - // since $location.hash hasn't changed - $anchorScroll(); - } - }; - } - ]); - - - body { - padding-top: 50px; - } - - .anchor { - border: 2px dashed DarkOrchid; - padding: 10px 10px 200px 10px; - } - - .fixed-header { - background-color: rgba(0, 0, 0, 0.2); - height: 50px; - position: fixed; - top: 0; left: 0; right: 0; - } - - .fixed-header > a { - display: inline-block; - margin: 5px 15px; - } - -
    - */ - this.$get = ['$window', '$location', '$rootScope', function($window, $location, $rootScope) { - var document = $window.document; - - // Helper function to get first anchor from a NodeList - // (using `Array#some()` instead of `angular#forEach()` since it's more performant - // and working in all supported browsers.) - function getFirstAnchor(list) { - var result = null; - Array.prototype.some.call(list, function(element) { - if (nodeName_(element) === 'a') { - result = element; - return true; - } - }); - return result; - } - - function getYOffset() { - - var offset = scroll.yOffset; - - if (isFunction(offset)) { - offset = offset(); - } else if (isElement(offset)) { - var elem = offset[0]; - var style = $window.getComputedStyle(elem); - if (style.position !== 'fixed') { - offset = 0; - } else { - offset = elem.getBoundingClientRect().bottom; - } - } else if (!isNumber(offset)) { - offset = 0; - } - - return offset; - } - - function scrollTo(elem) { - if (elem) { - elem.scrollIntoView(); - - var offset = getYOffset(); - - if (offset) { - // `offset` is the number of pixels we should scroll UP in order to align `elem` properly. - // This is true ONLY if the call to `elem.scrollIntoView()` initially aligns `elem` at the - // top of the viewport. - // - // IF the number of pixels from the top of `elem` to the end of the page's content is less - // than the height of the viewport, then `elem.scrollIntoView()` will align the `elem` some - // way down the page. - // - // This is often the case for elements near the bottom of the page. - // - // In such cases we do not need to scroll the whole `offset` up, just the difference between - // the top of the element and the offset, which is enough to align the top of `elem` at the - // desired position. - var elemTop = elem.getBoundingClientRect().top; - $window.scrollBy(0, elemTop - offset); - } - } else { - $window.scrollTo(0, 0); - } - } - - function scroll(hash) { - hash = isString(hash) ? hash : $location.hash(); - var elm; - - // empty hash, scroll to the top of the page - if (!hash) scrollTo(null); - - // element with given id - else if ((elm = document.getElementById(hash))) scrollTo(elm); - - // first anchor with given name :-D - else if ((elm = getFirstAnchor(document.getElementsByName(hash)))) scrollTo(elm); - - // no element and hash == 'top', scroll to the top of the page - else if (hash === 'top') scrollTo(null); - } - - // does not scroll when user clicks on anchor link that is currently on - // (no url change, no $location.hash() change), browser native does scroll - if (autoScrollingEnabled) { - $rootScope.$watch(function autoScrollWatch() {return $location.hash();}, - function autoScrollWatchAction(newVal, oldVal) { - // skip the initial scroll if $location.hash is empty - if (newVal === oldVal && newVal === '') return; - - jqLiteDocumentLoaded(function() { - $rootScope.$evalAsync(scroll); - }); - }); - } - - return scroll; - }]; -} - -var $animateMinErr = minErr('$animate'); -var ELEMENT_NODE = 1; -var NG_ANIMATE_CLASSNAME = 'ng-animate'; - -function mergeClasses(a,b) { - if (!a && !b) return ''; - if (!a) return b; - if (!b) return a; - if (isArray(a)) a = a.join(' '); - if (isArray(b)) b = b.join(' '); - return a + ' ' + b; -} - -function extractElementNode(element) { - for (var i = 0; i < element.length; i++) { - var elm = element[i]; - if (elm.nodeType === ELEMENT_NODE) { - return elm; - } - } -} - -function splitClasses(classes) { - if (isString(classes)) { - classes = classes.split(' '); - } - - // Use createMap() to prevent class assumptions involving property names in - // Object.prototype - var obj = createMap(); - forEach(classes, function(klass) { - // sometimes the split leaves empty string values - // incase extra spaces were applied to the options - if (klass.length) { - obj[klass] = true; - } - }); - return obj; -} - -// if any other type of options value besides an Object value is -// passed into the $animate.method() animation then this helper code -// will be run which will ignore it. While this patch is not the -// greatest solution to this, a lot of existing plugins depend on -// $animate to either call the callback (< 1.2) or return a promise -// that can be changed. This helper function ensures that the options -// are wiped clean incase a callback function is provided. -function prepareAnimateOptions(options) { - return isObject(options) - ? options - : {}; -} - -var $$CoreAnimateRunnerProvider = function() { - this.$get = ['$q', '$$rAF', function($q, $$rAF) { - function AnimateRunner() {} - AnimateRunner.all = noop; - AnimateRunner.chain = noop; - AnimateRunner.prototype = { - end: noop, - cancel: noop, - resume: noop, - pause: noop, - complete: noop, - then: function(pass, fail) { - return $q(function(resolve) { - $$rAF(function() { - resolve(); - }); - }).then(pass, fail); - } - }; - return AnimateRunner; - }]; -}; - -// this is prefixed with Core since it conflicts with -// the animateQueueProvider defined in ngAnimate/animateQueue.js -var $$CoreAnimateQueueProvider = function() { - var postDigestQueue = new HashMap(); - var postDigestElements = []; - - this.$get = ['$$AnimateRunner', '$rootScope', - function($$AnimateRunner, $rootScope) { - return { - enabled: noop, - on: noop, - off: noop, - pin: noop, - - push: function(element, event, options, domOperation) { - domOperation && domOperation(); - - options = options || {}; - options.from && element.css(options.from); - options.to && element.css(options.to); - - if (options.addClass || options.removeClass) { - addRemoveClassesPostDigest(element, options.addClass, options.removeClass); - } - - return new $$AnimateRunner(); // jshint ignore:line - } - }; - - function addRemoveClassesPostDigest(element, add, remove) { - var classVal, data = postDigestQueue.get(element); - - if (!data) { - postDigestQueue.put(element, data = {}); - postDigestElements.push(element); - } - - var updateData = function(classes, value) { - var changed = false; - if (classes) { - classes = isString(classes) ? classes.split(' ') : - isArray(classes) ? classes : []; - forEach(classes, function(className) { - if (className) { - changed = true; - data[className] = value; - } - }); - } - return changed; - }; - - var classesAdded = updateData(add, true); - var classesRemoved = updateData(remove, false); - if ((!classesAdded && !classesRemoved) || postDigestElements.length > 1) return; - - $rootScope.$$postDigest(function() { - forEach(postDigestElements, function(element) { - var data = postDigestQueue.get(element); - if (data) { - var existing = splitClasses(element.attr('class')); - var toAdd = ''; - var toRemove = ''; - forEach(data, function(status, className) { - var hasClass = !!existing[className]; - if (status !== hasClass) { - if (status) { - toAdd += (toAdd.length ? ' ' : '') + className; - } else { - toRemove += (toRemove.length ? ' ' : '') + className; - } - } - }); - - forEach(element, function(elm) { - toAdd && jqLiteAddClass(elm, toAdd); - toRemove && jqLiteRemoveClass(elm, toRemove); - }); - postDigestQueue.remove(element); - } - }); - - postDigestElements.length = 0; - }); - } - }]; -}; - -/** - * @ngdoc provider - * @name $animateProvider - * - * @description - * Default implementation of $animate that doesn't perform any animations, instead just - * synchronously performs DOM updates and resolves the returned runner promise. - * - * In order to enable animations the `ngAnimate` module has to be loaded. - * - * To see the functional implementation check out `src/ngAnimate/animate.js`. - */ -var $AnimateProvider = ['$provide', function($provide) { - var provider = this; - - this.$$registeredAnimations = Object.create(null); - - /** - * @ngdoc method - * @name $animateProvider#register - * - * @description - * Registers a new injectable animation factory function. The factory function produces the - * animation object which contains callback functions for each event that is expected to be - * animated. - * - * * `eventFn`: `function(element, ... , doneFunction, options)` - * The element to animate, the `doneFunction` and the options fed into the animation. Depending - * on the type of animation additional arguments will be injected into the animation function. The - * list below explains the function signatures for the different animation methods: - * - * - setClass: function(element, addedClasses, removedClasses, doneFunction, options) - * - addClass: function(element, addedClasses, doneFunction, options) - * - removeClass: function(element, removedClasses, doneFunction, options) - * - enter, leave, move: function(element, doneFunction, options) - * - animate: function(element, fromStyles, toStyles, doneFunction, options) - * - * Make sure to trigger the `doneFunction` once the animation is fully complete. - * - * ```js - * return { - * //enter, leave, move signature - * eventFn : function(element, done, options) { - * //code to run the animation - * //once complete, then run done() - * return function endFunction(wasCancelled) { - * //code to cancel the animation - * } - * } - * } - * ``` - * - * @param {string} name The name of the animation (this is what the class-based CSS value will be compared to). - * @param {Function} factory The factory function that will be executed to return the animation - * object. - */ - this.register = function(name, factory) { - if (name && name.charAt(0) !== '.') { - throw $animateMinErr('notcsel', "Expecting class selector starting with '.' got '{0}'.", name); - } - - var key = name + '-animation'; - provider.$$registeredAnimations[name.substr(1)] = key; - $provide.factory(key, factory); - }; - - /** - * @ngdoc method - * @name $animateProvider#classNameFilter - * - * @description - * Sets and/or returns the CSS class regular expression that is checked when performing - * an animation. Upon bootstrap the classNameFilter value is not set at all and will - * therefore enable $animate to attempt to perform an animation on any element that is triggered. - * When setting the `classNameFilter` value, animations will only be performed on elements - * that successfully match the filter expression. This in turn can boost performance - * for low-powered devices as well as applications containing a lot of structural operations. - * @param {RegExp=} expression The className expression which will be checked against all animations - * @return {RegExp} The current CSS className expression value. If null then there is no expression value - */ - this.classNameFilter = function(expression) { - if (arguments.length === 1) { - this.$$classNameFilter = (expression instanceof RegExp) ? expression : null; - if (this.$$classNameFilter) { - var reservedRegex = new RegExp("(\\s+|\\/)" + NG_ANIMATE_CLASSNAME + "(\\s+|\\/)"); - if (reservedRegex.test(this.$$classNameFilter.toString())) { - throw $animateMinErr('nongcls','$animateProvider.classNameFilter(regex) prohibits accepting a regex value which matches/contains the "{0}" CSS class.', NG_ANIMATE_CLASSNAME); - - } - } - } - return this.$$classNameFilter; - }; - - this.$get = ['$$animateQueue', function($$animateQueue) { - function domInsert(element, parentElement, afterElement) { - // if for some reason the previous element was removed - // from the dom sometime before this code runs then let's - // just stick to using the parent element as the anchor - if (afterElement) { - var afterNode = extractElementNode(afterElement); - if (afterNode && !afterNode.parentNode && !afterNode.previousElementSibling) { - afterElement = null; - } - } - afterElement ? afterElement.after(element) : parentElement.prepend(element); - } - - /** - * @ngdoc service - * @name $animate - * @description The $animate service exposes a series of DOM utility methods that provide support - * for animation hooks. The default behavior is the application of DOM operations, however, - * when an animation is detected (and animations are enabled), $animate will do the heavy lifting - * to ensure that animation runs with the triggered DOM operation. - * - * By default $animate doesn't trigger an animations. This is because the `ngAnimate` module isn't - * included and only when it is active then the animation hooks that `$animate` triggers will be - * functional. Once active then all structural `ng-` directives will trigger animations as they perform - * their DOM-related operations (enter, leave and move). Other directives such as `ngClass`, - * `ngShow`, `ngHide` and `ngMessages` also provide support for animations. - * - * It is recommended that the`$animate` service is always used when executing DOM-related procedures within directives. - * - * To learn more about enabling animation support, click here to visit the - * {@link ngAnimate ngAnimate module page}. - */ - return { - // we don't call it directly since non-existant arguments may - // be interpreted as null within the sub enabled function - - /** - * - * @ngdoc method - * @name $animate#on - * @kind function - * @description Sets up an event listener to fire whenever the animation event (enter, leave, move, etc...) - * has fired on the given element or among any of its children. Once the listener is fired, the provided callback - * is fired with the following params: - * - * ```js - * $animate.on('enter', container, - * function callback(element, phase) { - * // cool we detected an enter animation within the container - * } - * ); - * ``` - * - * @param {string} event the animation event that will be captured (e.g. enter, leave, move, addClass, removeClass, etc...) - * @param {DOMElement} container the container element that will capture each of the animation events that are fired on itself - * as well as among its children - * @param {Function} callback the callback function that will be fired when the listener is triggered - * - * The arguments present in the callback function are: - * * `element` - The captured DOM element that the animation was fired on. - * * `phase` - The phase of the animation. The two possible phases are **start** (when the animation starts) and **close** (when it ends). - */ - on: $$animateQueue.on, - - /** - * - * @ngdoc method - * @name $animate#off - * @kind function - * @description Deregisters an event listener based on the event which has been associated with the provided element. This method - * can be used in three different ways depending on the arguments: - * - * ```js - * // remove all the animation event listeners listening for `enter` - * $animate.off('enter'); - * - * // remove all the animation event listeners listening for `enter` on the given element and its children - * $animate.off('enter', container); - * - * // remove the event listener function provided by `listenerFn` that is set - * // to listen for `enter` on the given `element` as well as its children - * $animate.off('enter', container, callback); - * ``` - * - * @param {string} event the animation event (e.g. enter, leave, move, addClass, removeClass, etc...) - * @param {DOMElement=} container the container element the event listener was placed on - * @param {Function=} callback the callback function that was registered as the listener - */ - off: $$animateQueue.off, - - /** - * @ngdoc method - * @name $animate#pin - * @kind function - * @description Associates the provided element with a host parent element to allow the element to be animated even if it exists - * outside of the DOM structure of the Angular application. By doing so, any animation triggered via `$animate` can be issued on the - * element despite being outside the realm of the application or within another application. Say for example if the application - * was bootstrapped on an element that is somewhere inside of the `` tag, but we wanted to allow for an element to be situated - * as a direct child of `document.body`, then this can be achieved by pinning the element via `$animate.pin(element)`. Keep in mind - * that calling `$animate.pin(element, parentElement)` will not actually insert into the DOM anywhere; it will just create the association. - * - * Note that this feature is only active when the `ngAnimate` module is used. - * - * @param {DOMElement} element the external element that will be pinned - * @param {DOMElement} parentElement the host parent element that will be associated with the external element - */ - pin: $$animateQueue.pin, - - /** - * - * @ngdoc method - * @name $animate#enabled - * @kind function - * @description Used to get and set whether animations are enabled or not on the entire application or on an element and its children. This - * function can be called in four ways: - * - * ```js - * // returns true or false - * $animate.enabled(); - * - * // changes the enabled state for all animations - * $animate.enabled(false); - * $animate.enabled(true); - * - * // returns true or false if animations are enabled for an element - * $animate.enabled(element); - * - * // changes the enabled state for an element and its children - * $animate.enabled(element, true); - * $animate.enabled(element, false); - * ``` - * - * @param {DOMElement=} element the element that will be considered for checking/setting the enabled state - * @param {boolean=} enabled whether or not the animations will be enabled for the element - * - * @return {boolean} whether or not animations are enabled - */ - enabled: $$animateQueue.enabled, - - /** - * @ngdoc method - * @name $animate#cancel - * @kind function - * @description Cancels the provided animation. - * - * @param {Promise} animationPromise The animation promise that is returned when an animation is started. - */ - cancel: function(runner) { - runner.end && runner.end(); - }, - - /** - * - * @ngdoc method - * @name $animate#enter - * @kind function - * @description Inserts the element into the DOM either after the `after` element (if provided) or - * as the first child within the `parent` element and then triggers an animation. - * A promise is returned that will be resolved during the next digest once the animation - * has completed. - * - * @param {DOMElement} element the element which will be inserted into the DOM - * @param {DOMElement} parent the parent element which will append the element as - * a child (so long as the after element is not present) - * @param {DOMElement=} after the sibling element after which the element will be appended - * @param {object=} options an optional collection of options/styles that will be applied to the element - * - * @return {Promise} the animation callback promise - */ - enter: function(element, parent, after, options) { - parent = parent && jqLite(parent); - after = after && jqLite(after); - parent = parent || after.parent(); - domInsert(element, parent, after); - return $$animateQueue.push(element, 'enter', prepareAnimateOptions(options)); - }, - - /** - * - * @ngdoc method - * @name $animate#move - * @kind function - * @description Inserts (moves) the element into its new position in the DOM either after - * the `after` element (if provided) or as the first child within the `parent` element - * and then triggers an animation. A promise is returned that will be resolved - * during the next digest once the animation has completed. - * - * @param {DOMElement} element the element which will be moved into the new DOM position - * @param {DOMElement} parent the parent element which will append the element as - * a child (so long as the after element is not present) - * @param {DOMElement=} after the sibling element after which the element will be appended - * @param {object=} options an optional collection of options/styles that will be applied to the element - * - * @return {Promise} the animation callback promise - */ - move: function(element, parent, after, options) { - parent = parent && jqLite(parent); - after = after && jqLite(after); - parent = parent || after.parent(); - domInsert(element, parent, after); - return $$animateQueue.push(element, 'move', prepareAnimateOptions(options)); - }, - - /** - * @ngdoc method - * @name $animate#leave - * @kind function - * @description Triggers an animation and then removes the element from the DOM. - * When the function is called a promise is returned that will be resolved during the next - * digest once the animation has completed. - * - * @param {DOMElement} element the element which will be removed from the DOM - * @param {object=} options an optional collection of options/styles that will be applied to the element - * - * @return {Promise} the animation callback promise - */ - leave: function(element, options) { - return $$animateQueue.push(element, 'leave', prepareAnimateOptions(options), function() { - element.remove(); - }); - }, - - /** - * @ngdoc method - * @name $animate#addClass - * @kind function - * - * @description Triggers an addClass animation surrounding the addition of the provided CSS class(es). Upon - * execution, the addClass operation will only be handled after the next digest and it will not trigger an - * animation if element already contains the CSS class or if the class is removed at a later step. - * Note that class-based animations are treated differently compared to structural animations - * (like enter, move and leave) since the CSS classes may be added/removed at different points - * depending if CSS or JavaScript animations are used. - * - * @param {DOMElement} element the element which the CSS classes will be applied to - * @param {string} className the CSS class(es) that will be added (multiple classes are separated via spaces) - * @param {object=} options an optional collection of options/styles that will be applied to the element - * - * @return {Promise} the animation callback promise - */ - addClass: function(element, className, options) { - options = prepareAnimateOptions(options); - options.addClass = mergeClasses(options.addclass, className); - return $$animateQueue.push(element, 'addClass', options); - }, - - /** - * @ngdoc method - * @name $animate#removeClass - * @kind function - * - * @description Triggers a removeClass animation surrounding the removal of the provided CSS class(es). Upon - * execution, the removeClass operation will only be handled after the next digest and it will not trigger an - * animation if element does not contain the CSS class or if the class is added at a later step. - * Note that class-based animations are treated differently compared to structural animations - * (like enter, move and leave) since the CSS classes may be added/removed at different points - * depending if CSS or JavaScript animations are used. - * - * @param {DOMElement} element the element which the CSS classes will be applied to - * @param {string} className the CSS class(es) that will be removed (multiple classes are separated via spaces) - * @param {object=} options an optional collection of options/styles that will be applied to the element - * - * @return {Promise} the animation callback promise - */ - removeClass: function(element, className, options) { - options = prepareAnimateOptions(options); - options.removeClass = mergeClasses(options.removeClass, className); - return $$animateQueue.push(element, 'removeClass', options); - }, - - /** - * @ngdoc method - * @name $animate#setClass - * @kind function - * - * @description Performs both the addition and removal of a CSS classes on an element and (during the process) - * triggers an animation surrounding the class addition/removal. Much like `$animate.addClass` and - * `$animate.removeClass`, `setClass` will only evaluate the classes being added/removed once a digest has - * passed. Note that class-based animations are treated differently compared to structural animations - * (like enter, move and leave) since the CSS classes may be added/removed at different points - * depending if CSS or JavaScript animations are used. - * - * @param {DOMElement} element the element which the CSS classes will be applied to - * @param {string} add the CSS class(es) that will be added (multiple classes are separated via spaces) - * @param {string} remove the CSS class(es) that will be removed (multiple classes are separated via spaces) - * @param {object=} options an optional collection of options/styles that will be applied to the element - * - * @return {Promise} the animation callback promise - */ - setClass: function(element, add, remove, options) { - options = prepareAnimateOptions(options); - options.addClass = mergeClasses(options.addClass, add); - options.removeClass = mergeClasses(options.removeClass, remove); - return $$animateQueue.push(element, 'setClass', options); - }, - - /** - * @ngdoc method - * @name $animate#animate - * @kind function - * - * @description Performs an inline animation on the element which applies the provided to and from CSS styles to the element. - * If any detected CSS transition, keyframe or JavaScript matches the provided className value then the animation will take - * on the provided styles. For example, if a transition animation is set for the given className then the provided from and - * to styles will be applied alongside the given transition. If a JavaScript animation is detected then the provided styles - * will be given in as function paramters into the `animate` method (or as apart of the `options` parameter). - * - * @param {DOMElement} element the element which the CSS styles will be applied to - * @param {object} from the from (starting) CSS styles that will be applied to the element and across the animation. - * @param {object} to the to (destination) CSS styles that will be applied to the element and across the animation. - * @param {string=} className an optional CSS class that will be applied to the element for the duration of the animation. If - * this value is left as empty then a CSS class of `ng-inline-animate` will be applied to the element. - * (Note that if no animation is detected then this value will not be appplied to the element.) - * @param {object=} options an optional collection of options/styles that will be applied to the element - * - * @return {Promise} the animation callback promise - */ - animate: function(element, from, to, className, options) { - options = prepareAnimateOptions(options); - options.from = options.from ? extend(options.from, from) : from; - options.to = options.to ? extend(options.to, to) : to; - - className = className || 'ng-inline-animate'; - options.tempClasses = mergeClasses(options.tempClasses, className); - return $$animateQueue.push(element, 'animate', options); - } - }; - }]; -}]; - -/** - * @ngdoc service - * @name $animateCss - * @kind object - * - * @description - * This is the core version of `$animateCss`. By default, only when the `ngAnimate` is included, - * then the `$animateCss` service will actually perform animations. - * - * Click here {@link ngAnimate.$animateCss to read the documentation for $animateCss}. - */ -var $CoreAnimateCssProvider = function() { - this.$get = ['$$rAF', '$q', function($$rAF, $q) { - - var RAFPromise = function() {}; - RAFPromise.prototype = { - done: function(cancel) { - this.defer && this.defer[cancel === true ? 'reject' : 'resolve'](); - }, - end: function() { - this.done(); - }, - cancel: function() { - this.done(true); - }, - getPromise: function() { - if (!this.defer) { - this.defer = $q.defer(); - } - return this.defer.promise; - }, - then: function(f1,f2) { - return this.getPromise().then(f1,f2); - }, - 'catch': function(f1) { - return this.getPromise().catch(f1); - }, - 'finally': function(f1) { - return this.getPromise().finally(f1); - } - }; - - return function(element, options) { - if (options.from) { - element.css(options.from); - options.from = null; - } - - var closed, runner = new RAFPromise(); - return { - start: run, - end: run - }; - - function run() { - $$rAF(function() { - close(); - if (!closed) { - runner.done(); - } - closed = true; - }); - return runner; - } - - function close() { - if (options.addClass) { - element.addClass(options.addClass); - options.addClass = null; - } - if (options.removeClass) { - element.removeClass(options.removeClass); - options.removeClass = null; - } - if (options.to) { - element.css(options.to); - options.to = null; - } - } - }; - }]; -}; - -/* global stripHash: true */ - -/** - * ! This is a private undocumented service ! - * - * @name $browser - * @requires $log - * @description - * This object has two goals: - * - * - hide all the global state in the browser caused by the window object - * - abstract away all the browser specific features and inconsistencies - * - * For tests we provide {@link ngMock.$browser mock implementation} of the `$browser` - * service, which can be used for convenient testing of the application without the interaction with - * the real browser apis. - */ -/** - * @param {object} window The global window object. - * @param {object} document jQuery wrapped document. - * @param {object} $log window.console or an object with the same interface. - * @param {object} $sniffer $sniffer service - */ -function Browser(window, document, $log, $sniffer) { - var self = this, - rawDocument = document[0], - location = window.location, - history = window.history, - setTimeout = window.setTimeout, - clearTimeout = window.clearTimeout, - pendingDeferIds = {}; - - self.isMock = false; - - var outstandingRequestCount = 0; - var outstandingRequestCallbacks = []; - - // TODO(vojta): remove this temporary api - self.$$completeOutstandingRequest = completeOutstandingRequest; - self.$$incOutstandingRequestCount = function() { outstandingRequestCount++; }; - - /** - * Executes the `fn` function(supports currying) and decrements the `outstandingRequestCallbacks` - * counter. If the counter reaches 0, all the `outstandingRequestCallbacks` are executed. - */ - function completeOutstandingRequest(fn) { - try { - fn.apply(null, sliceArgs(arguments, 1)); - } finally { - outstandingRequestCount--; - if (outstandingRequestCount === 0) { - while (outstandingRequestCallbacks.length) { - try { - outstandingRequestCallbacks.pop()(); - } catch (e) { - $log.error(e); - } - } - } - } - } - - function getHash(url) { - var index = url.indexOf('#'); - return index === -1 ? '' : url.substr(index); - } - - /** - * @private - * Note: this method is used only by scenario runner - * TODO(vojta): prefix this method with $$ ? - * @param {function()} callback Function that will be called when no outstanding request - */ - self.notifyWhenNoOutstandingRequests = function(callback) { - if (outstandingRequestCount === 0) { - callback(); - } else { - outstandingRequestCallbacks.push(callback); - } - }; - - ////////////////////////////////////////////////////////////// - // URL API - ////////////////////////////////////////////////////////////// - - var cachedState, lastHistoryState, - lastBrowserUrl = location.href, - baseElement = document.find('base'), - reloadLocation = null; - - cacheState(); - lastHistoryState = cachedState; - - /** - * @name $browser#url - * - * @description - * GETTER: - * Without any argument, this method just returns current value of location.href. - * - * SETTER: - * With at least one argument, this method sets url to new value. - * If html5 history api supported, pushState/replaceState is used, otherwise - * location.href/location.replace is used. - * Returns its own instance to allow chaining - * - * NOTE: this api is intended for use only by the $location service. Please use the - * {@link ng.$location $location service} to change url. - * - * @param {string} url New url (when used as setter) - * @param {boolean=} replace Should new url replace current history record? - * @param {object=} state object to use with pushState/replaceState - */ - self.url = function(url, replace, state) { - // In modern browsers `history.state` is `null` by default; treating it separately - // from `undefined` would cause `$browser.url('/foo')` to change `history.state` - // to undefined via `pushState`. Instead, let's change `undefined` to `null` here. - if (isUndefined(state)) { - state = null; - } - - // Android Browser BFCache causes location, history reference to become stale. - if (location !== window.location) location = window.location; - if (history !== window.history) history = window.history; - - // setter - if (url) { - var sameState = lastHistoryState === state; - - // Don't change anything if previous and current URLs and states match. This also prevents - // IE<10 from getting into redirect loop when in LocationHashbangInHtml5Url mode. - // See https://github.com/angular/angular.js/commit/ffb2701 - if (lastBrowserUrl === url && (!$sniffer.history || sameState)) { - return self; - } - var sameBase = lastBrowserUrl && stripHash(lastBrowserUrl) === stripHash(url); - lastBrowserUrl = url; - lastHistoryState = state; - // Don't use history API if only the hash changed - // due to a bug in IE10/IE11 which leads - // to not firing a `hashchange` nor `popstate` event - // in some cases (see #9143). - if ($sniffer.history && (!sameBase || !sameState)) { - history[replace ? 'replaceState' : 'pushState'](state, '', url); - cacheState(); - // Do the assignment again so that those two variables are referentially identical. - lastHistoryState = cachedState; - } else { - if (!sameBase || reloadLocation) { - reloadLocation = url; - } - if (replace) { - location.replace(url); - } else if (!sameBase) { - location.href = url; - } else { - location.hash = getHash(url); - } - } - return self; - // getter - } else { - // - reloadLocation is needed as browsers don't allow to read out - // the new location.href if a reload happened. - // - the replacement is a workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=407172 - return reloadLocation || location.href.replace(/%27/g,"'"); - } - }; - - /** - * @name $browser#state - * - * @description - * This method is a getter. - * - * Return history.state or null if history.state is undefined. - * - * @returns {object} state - */ - self.state = function() { - return cachedState; - }; - - var urlChangeListeners = [], - urlChangeInit = false; - - function cacheStateAndFireUrlChange() { - cacheState(); - fireUrlChange(); - } - - function getCurrentState() { - try { - return history.state; - } catch (e) { - // MSIE can reportedly throw when there is no state (UNCONFIRMED). - } - } - - // This variable should be used *only* inside the cacheState function. - var lastCachedState = null; - function cacheState() { - // This should be the only place in $browser where `history.state` is read. - cachedState = getCurrentState(); - cachedState = isUndefined(cachedState) ? null : cachedState; - - // Prevent callbacks fo fire twice if both hashchange & popstate were fired. - if (equals(cachedState, lastCachedState)) { - cachedState = lastCachedState; - } - lastCachedState = cachedState; - } - - function fireUrlChange() { - if (lastBrowserUrl === self.url() && lastHistoryState === cachedState) { - return; - } - - lastBrowserUrl = self.url(); - lastHistoryState = cachedState; - forEach(urlChangeListeners, function(listener) { - listener(self.url(), cachedState); - }); - } - - /** - * @name $browser#onUrlChange - * - * @description - * Register callback function that will be called, when url changes. - * - * It's only called when the url is changed from outside of angular: - * - user types different url into address bar - * - user clicks on history (forward/back) button - * - user clicks on a link - * - * It's not called when url is changed by $browser.url() method - * - * The listener gets called with new url as parameter. - * - * NOTE: this api is intended for use only by the $location service. Please use the - * {@link ng.$location $location service} to monitor url changes in angular apps. - * - * @param {function(string)} listener Listener function to be called when url changes. - * @return {function(string)} Returns the registered listener fn - handy if the fn is anonymous. - */ - self.onUrlChange = function(callback) { - // TODO(vojta): refactor to use node's syntax for events - if (!urlChangeInit) { - // We listen on both (hashchange/popstate) when available, as some browsers (e.g. Opera) - // don't fire popstate when user change the address bar and don't fire hashchange when url - // changed by push/replaceState - - // html5 history api - popstate event - if ($sniffer.history) jqLite(window).on('popstate', cacheStateAndFireUrlChange); - // hashchange event - jqLite(window).on('hashchange', cacheStateAndFireUrlChange); - - urlChangeInit = true; - } - - urlChangeListeners.push(callback); - return callback; - }; - - /** - * @private - * Remove popstate and hashchange handler from window. - * - * NOTE: this api is intended for use only by $rootScope. - */ - self.$$applicationDestroyed = function() { - jqLite(window).off('hashchange popstate', cacheStateAndFireUrlChange); - }; - - /** - * Checks whether the url has changed outside of Angular. - * Needs to be exported to be able to check for changes that have been done in sync, - * as hashchange/popstate events fire in async. - */ - self.$$checkUrlChange = fireUrlChange; - - ////////////////////////////////////////////////////////////// - // Misc API - ////////////////////////////////////////////////////////////// - - /** - * @name $browser#baseHref - * - * @description - * Returns current - * (always relative - without domain) - * - * @returns {string} The current base href - */ - self.baseHref = function() { - var href = baseElement.attr('href'); - return href ? href.replace(/^(https?\:)?\/\/[^\/]*/, '') : ''; - }; - - /** - * @name $browser#defer - * @param {function()} fn A function, who's execution should be deferred. - * @param {number=} [delay=0] of milliseconds to defer the function execution. - * @returns {*} DeferId that can be used to cancel the task via `$browser.defer.cancel()`. - * - * @description - * Executes a fn asynchronously via `setTimeout(fn, delay)`. - * - * Unlike when calling `setTimeout` directly, in test this function is mocked and instead of using - * `setTimeout` in tests, the fns are queued in an array, which can be programmatically flushed - * via `$browser.defer.flush()`. - * - */ - self.defer = function(fn, delay) { - var timeoutId; - outstandingRequestCount++; - timeoutId = setTimeout(function() { - delete pendingDeferIds[timeoutId]; - completeOutstandingRequest(fn); - }, delay || 0); - pendingDeferIds[timeoutId] = true; - return timeoutId; - }; - - - /** - * @name $browser#defer.cancel - * - * @description - * Cancels a deferred task identified with `deferId`. - * - * @param {*} deferId Token returned by the `$browser.defer` function. - * @returns {boolean} Returns `true` if the task hasn't executed yet and was successfully - * canceled. - */ - self.defer.cancel = function(deferId) { - if (pendingDeferIds[deferId]) { - delete pendingDeferIds[deferId]; - clearTimeout(deferId); - completeOutstandingRequest(noop); - return true; - } - return false; - }; - -} - -function $BrowserProvider() { - this.$get = ['$window', '$log', '$sniffer', '$document', - function($window, $log, $sniffer, $document) { - return new Browser($window, $document, $log, $sniffer); - }]; -} - -/** - * @ngdoc service - * @name $cacheFactory - * - * @description - * Factory that constructs {@link $cacheFactory.Cache Cache} objects and gives access to - * them. - * - * ```js - * - * var cache = $cacheFactory('cacheId'); - * expect($cacheFactory.get('cacheId')).toBe(cache); - * expect($cacheFactory.get('noSuchCacheId')).not.toBeDefined(); - * - * cache.put("key", "value"); - * cache.put("another key", "another value"); - * - * // We've specified no options on creation - * expect(cache.info()).toEqual({id: 'cacheId', size: 2}); - * - * ``` - * - * - * @param {string} cacheId Name or id of the newly created cache. - * @param {object=} options Options object that specifies the cache behavior. Properties: - * - * - `{number=}` `capacity` — turns the cache into LRU cache. - * - * @returns {object} Newly created cache object with the following set of methods: - * - * - `{object}` `info()` — Returns id, size, and options of cache. - * - `{{*}}` `put({string} key, {*} value)` — Puts a new key-value pair into the cache and returns - * it. - * - `{{*}}` `get({string} key)` — Returns cached value for `key` or undefined for cache miss. - * - `{void}` `remove({string} key)` — Removes a key-value pair from the cache. - * - `{void}` `removeAll()` — Removes all cached values. - * - `{void}` `destroy()` — Removes references to this cache from $cacheFactory. - * - * @example - - -
    - - - - -

    Cached Values

    -
    - - : - -
    - -

    Cache Info

    -
    - - : - -
    -
    -
    - - angular.module('cacheExampleApp', []). - controller('CacheController', ['$scope', '$cacheFactory', function($scope, $cacheFactory) { - $scope.keys = []; - $scope.cache = $cacheFactory('cacheId'); - $scope.put = function(key, value) { - if ($scope.cache.get(key) === undefined) { - $scope.keys.push(key); - } - $scope.cache.put(key, value === undefined ? null : value); - }; - }]); - - - p { - margin: 10px 0 3px; - } - -
    - */ -function $CacheFactoryProvider() { - - this.$get = function() { - var caches = {}; - - function cacheFactory(cacheId, options) { - if (cacheId in caches) { - throw minErr('$cacheFactory')('iid', "CacheId '{0}' is already taken!", cacheId); - } - - var size = 0, - stats = extend({}, options, {id: cacheId}), - data = {}, - capacity = (options && options.capacity) || Number.MAX_VALUE, - lruHash = {}, - freshEnd = null, - staleEnd = null; - - /** - * @ngdoc type - * @name $cacheFactory.Cache - * - * @description - * A cache object used to store and retrieve data, primarily used by - * {@link $http $http} and the {@link ng.directive:script script} directive to cache - * templates and other data. - * - * ```js - * angular.module('superCache') - * .factory('superCache', ['$cacheFactory', function($cacheFactory) { - * return $cacheFactory('super-cache'); - * }]); - * ``` - * - * Example test: - * - * ```js - * it('should behave like a cache', inject(function(superCache) { - * superCache.put('key', 'value'); - * superCache.put('another key', 'another value'); - * - * expect(superCache.info()).toEqual({ - * id: 'super-cache', - * size: 2 - * }); - * - * superCache.remove('another key'); - * expect(superCache.get('another key')).toBeUndefined(); - * - * superCache.removeAll(); - * expect(superCache.info()).toEqual({ - * id: 'super-cache', - * size: 0 - * }); - * })); - * ``` - */ - return caches[cacheId] = { - - /** - * @ngdoc method - * @name $cacheFactory.Cache#put - * @kind function - * - * @description - * Inserts a named entry into the {@link $cacheFactory.Cache Cache} object to be - * retrieved later, and incrementing the size of the cache if the key was not already - * present in the cache. If behaving like an LRU cache, it will also remove stale - * entries from the set. - * - * It will not insert undefined values into the cache. - * - * @param {string} key the key under which the cached data is stored. - * @param {*} value the value to store alongside the key. If it is undefined, the key - * will not be stored. - * @returns {*} the value stored. - */ - put: function(key, value) { - if (isUndefined(value)) return; - if (capacity < Number.MAX_VALUE) { - var lruEntry = lruHash[key] || (lruHash[key] = {key: key}); - - refresh(lruEntry); - } - - if (!(key in data)) size++; - data[key] = value; - - if (size > capacity) { - this.remove(staleEnd.key); - } - - return value; - }, - - /** - * @ngdoc method - * @name $cacheFactory.Cache#get - * @kind function - * - * @description - * Retrieves named data stored in the {@link $cacheFactory.Cache Cache} object. - * - * @param {string} key the key of the data to be retrieved - * @returns {*} the value stored. - */ - get: function(key) { - if (capacity < Number.MAX_VALUE) { - var lruEntry = lruHash[key]; - - if (!lruEntry) return; - - refresh(lruEntry); - } - - return data[key]; - }, - - - /** - * @ngdoc method - * @name $cacheFactory.Cache#remove - * @kind function - * - * @description - * Removes an entry from the {@link $cacheFactory.Cache Cache} object. - * - * @param {string} key the key of the entry to be removed - */ - remove: function(key) { - if (capacity < Number.MAX_VALUE) { - var lruEntry = lruHash[key]; - - if (!lruEntry) return; - - if (lruEntry == freshEnd) freshEnd = lruEntry.p; - if (lruEntry == staleEnd) staleEnd = lruEntry.n; - link(lruEntry.n,lruEntry.p); - - delete lruHash[key]; - } - - delete data[key]; - size--; - }, - - - /** - * @ngdoc method - * @name $cacheFactory.Cache#removeAll - * @kind function - * - * @description - * Clears the cache object of any entries. - */ - removeAll: function() { - data = {}; - size = 0; - lruHash = {}; - freshEnd = staleEnd = null; - }, - - - /** - * @ngdoc method - * @name $cacheFactory.Cache#destroy - * @kind function - * - * @description - * Destroys the {@link $cacheFactory.Cache Cache} object entirely, - * removing it from the {@link $cacheFactory $cacheFactory} set. - */ - destroy: function() { - data = null; - stats = null; - lruHash = null; - delete caches[cacheId]; - }, - - - /** - * @ngdoc method - * @name $cacheFactory.Cache#info - * @kind function - * - * @description - * Retrieve information regarding a particular {@link $cacheFactory.Cache Cache}. - * - * @returns {object} an object with the following properties: - *
      - *
    • **id**: the id of the cache instance
    • - *
    • **size**: the number of entries kept in the cache instance
    • - *
    • **...**: any additional properties from the options object when creating the - * cache.
    • - *
    - */ - info: function() { - return extend({}, stats, {size: size}); - } - }; - - - /** - * makes the `entry` the freshEnd of the LRU linked list - */ - function refresh(entry) { - if (entry != freshEnd) { - if (!staleEnd) { - staleEnd = entry; - } else if (staleEnd == entry) { - staleEnd = entry.n; - } - - link(entry.n, entry.p); - link(entry, freshEnd); - freshEnd = entry; - freshEnd.n = null; - } - } - - - /** - * bidirectionally links two entries of the LRU linked list - */ - function link(nextEntry, prevEntry) { - if (nextEntry != prevEntry) { - if (nextEntry) nextEntry.p = prevEntry; //p stands for previous, 'prev' didn't minify - if (prevEntry) prevEntry.n = nextEntry; //n stands for next, 'next' didn't minify - } - } - } - - - /** - * @ngdoc method - * @name $cacheFactory#info - * - * @description - * Get information about all the caches that have been created - * - * @returns {Object} - key-value map of `cacheId` to the result of calling `cache#info` - */ - cacheFactory.info = function() { - var info = {}; - forEach(caches, function(cache, cacheId) { - info[cacheId] = cache.info(); - }); - return info; - }; - - - /** - * @ngdoc method - * @name $cacheFactory#get - * - * @description - * Get access to a cache object by the `cacheId` used when it was created. - * - * @param {string} cacheId Name or id of a cache to access. - * @returns {object} Cache object identified by the cacheId or undefined if no such cache. - */ - cacheFactory.get = function(cacheId) { - return caches[cacheId]; - }; - - - return cacheFactory; - }; -} - -/** - * @ngdoc service - * @name $templateCache - * - * @description - * The first time a template is used, it is loaded in the template cache for quick retrieval. You - * can load templates directly into the cache in a `script` tag, or by consuming the - * `$templateCache` service directly. - * - * Adding via the `script` tag: - * - * ```html - * - * ``` - * - * **Note:** the `script` tag containing the template does not need to be included in the `head` of - * the document, but it must be a descendent of the {@link ng.$rootElement $rootElement} (IE, - * element with ng-app attribute), otherwise the template will be ignored. - * - * Adding via the `$templateCache` service: - * - * ```js - * var myApp = angular.module('myApp', []); - * myApp.run(function($templateCache) { - * $templateCache.put('templateId.html', 'This is the content of the template'); - * }); - * ``` - * - * To retrieve the template later, simply use it in your HTML: - * ```html - *
    - * ``` - * - * or get it via Javascript: - * ```js - * $templateCache.get('templateId.html') - * ``` - * - * See {@link ng.$cacheFactory $cacheFactory}. - * - */ -function $TemplateCacheProvider() { - this.$get = ['$cacheFactory', function($cacheFactory) { - return $cacheFactory('templates'); - }]; -} - -/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * - * Any commits to this file should be reviewed with security in mind. * - * Changes to this file can potentially create security vulnerabilities. * - * An approval from 2 Core members with history of modifying * - * this file is required. * - * * - * Does the change somehow allow for arbitrary javascript to be executed? * - * Or allows for someone to change the prototype of built-in objects? * - * Or gives undesired access to variables likes document or window? * - * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ - -/* ! VARIABLE/FUNCTION NAMING CONVENTIONS THAT APPLY TO THIS FILE! - * - * DOM-related variables: - * - * - "node" - DOM Node - * - "element" - DOM Element or Node - * - "$node" or "$element" - jqLite-wrapped node or element - * - * - * Compiler related stuff: - * - * - "linkFn" - linking fn of a single directive - * - "nodeLinkFn" - function that aggregates all linking fns for a particular node - * - "childLinkFn" - function that aggregates all linking fns for child nodes of a particular node - * - "compositeLinkFn" - function that aggregates all linking fns for a compilation root (nodeList) - */ - - -/** - * @ngdoc service - * @name $compile - * @kind function - * - * @description - * Compiles an HTML string or DOM into a template and produces a template function, which - * can then be used to link {@link ng.$rootScope.Scope `scope`} and the template together. - * - * The compilation is a process of walking the DOM tree and matching DOM elements to - * {@link ng.$compileProvider#directive directives}. - * - *
    - * **Note:** This document is an in-depth reference of all directive options. - * For a gentle introduction to directives with examples of common use cases, - * see the {@link guide/directive directive guide}. - *
    - * - * ## Comprehensive Directive API - * - * There are many different options for a directive. - * - * The difference resides in the return value of the factory function. - * You can either return a "Directive Definition Object" (see below) that defines the directive properties, - * or just the `postLink` function (all other properties will have the default values). - * - *
    - * **Best Practice:** It's recommended to use the "directive definition object" form. - *
    - * - * Here's an example directive declared with a Directive Definition Object: - * - * ```js - * var myModule = angular.module(...); - * - * myModule.directive('directiveName', function factory(injectables) { - * var directiveDefinitionObject = { - * priority: 0, - * template: '
    ', // or // function(tElement, tAttrs) { ... }, - * // or - * // templateUrl: 'directive.html', // or // function(tElement, tAttrs) { ... }, - * transclude: false, - * restrict: 'A', - * templateNamespace: 'html', - * scope: false, - * controller: function($scope, $element, $attrs, $transclude, otherInjectables) { ... }, - * controllerAs: 'stringIdentifier', - * bindToController: false, - * require: 'siblingDirectiveName', // or // ['^parentDirectiveName', '?optionalDirectiveName', '?^optionalParent'], - * compile: function compile(tElement, tAttrs, transclude) { - * return { - * pre: function preLink(scope, iElement, iAttrs, controller) { ... }, - * post: function postLink(scope, iElement, iAttrs, controller) { ... } - * } - * // or - * // return function postLink( ... ) { ... } - * }, - * // or - * // link: { - * // pre: function preLink(scope, iElement, iAttrs, controller) { ... }, - * // post: function postLink(scope, iElement, iAttrs, controller) { ... } - * // } - * // or - * // link: function postLink( ... ) { ... } - * }; - * return directiveDefinitionObject; - * }); - * ``` - * - *
    - * **Note:** Any unspecified options will use the default value. You can see the default values below. - *
    - * - * Therefore the above can be simplified as: - * - * ```js - * var myModule = angular.module(...); - * - * myModule.directive('directiveName', function factory(injectables) { - * var directiveDefinitionObject = { - * link: function postLink(scope, iElement, iAttrs) { ... } - * }; - * return directiveDefinitionObject; - * // or - * // return function postLink(scope, iElement, iAttrs) { ... } - * }); - * ``` - * - * - * - * ### Directive Definition Object - * - * The directive definition object provides instructions to the {@link ng.$compile - * compiler}. The attributes are: - * - * #### `multiElement` - * When this property is set to true, the HTML compiler will collect DOM nodes between - * nodes with the attributes `directive-name-start` and `directive-name-end`, and group them - * together as the directive elements. It is recommended that this feature be used on directives - * which are not strictly behavioural (such as {@link ngClick}), and which - * do not manipulate or replace child nodes (such as {@link ngInclude}). - * - * #### `priority` - * When there are multiple directives defined on a single DOM element, sometimes it - * is necessary to specify the order in which the directives are applied. The `priority` is used - * to sort the directives before their `compile` functions get called. Priority is defined as a - * number. Directives with greater numerical `priority` are compiled first. Pre-link functions - * are also run in priority order, but post-link functions are run in reverse order. The order - * of directives with the same priority is undefined. The default priority is `0`. - * - * #### `terminal` - * If set to true then the current `priority` will be the last set of directives - * which will execute (any directives at the current priority will still execute - * as the order of execution on same `priority` is undefined). Note that expressions - * and other directives used in the directive's template will also be excluded from execution. - * - * #### `scope` - * **If set to `true`,** then a new scope will be created for this directive. If multiple directives on the - * same element request a new scope, only one new scope is created. The new scope rule does not - * apply for the root of the template since the root of the template always gets a new scope. - * - * **If set to `{}` (object hash),** then a new "isolate" scope is created. The 'isolate' scope differs from - * normal scope in that it does not prototypically inherit from the parent scope. This is useful - * when creating reusable components, which should not accidentally read or modify data in the - * parent scope. - * - * The 'isolate' scope takes an object hash which defines a set of local scope properties - * derived from the parent scope. These local properties are useful for aliasing values for - * templates. Locals definition is a hash of local scope property to its source: - * - * * `@` or `@attr` - bind a local scope property to the value of DOM attribute. The result is - * always a string since DOM attributes are strings. If no `attr` name is specified then the - * attribute name is assumed to be the same as the local name. - * Given `` and widget definition - * of `scope: { localName:'@myAttr' }`, then widget scope property `localName` will reflect - * the interpolated value of `hello {{name}}`. As the `name` attribute changes so will the - * `localName` property on the widget scope. The `name` is read from the parent scope (not - * component scope). - * - * * `=` or `=attr` - set up bi-directional binding between a local scope property and the - * parent scope property of name defined via the value of the `attr` attribute. If no `attr` - * name is specified then the attribute name is assumed to be the same as the local name. - * Given `` and widget definition of - * `scope: { localModel:'=myAttr' }`, then widget scope property `localModel` will reflect the - * value of `parentModel` on the parent scope. Any changes to `parentModel` will be reflected - * in `localModel` and any changes in `localModel` will reflect in `parentModel`. If the parent - * scope property doesn't exist, it will throw a NON_ASSIGNABLE_MODEL_EXPRESSION exception. You - * can avoid this behavior using `=?` or `=?attr` in order to flag the property as optional. If - * you want to shallow watch for changes (i.e. $watchCollection instead of $watch) you can use - * `=*` or `=*attr` (`=*?` or `=*?attr` if the property is optional). - * - * * `&` or `&attr` - provides a way to execute an expression in the context of the parent scope. - * If no `attr` name is specified then the attribute name is assumed to be the same as the - * local name. Given `` and widget definition of - * `scope: { localFn:'&myAttr' }`, then isolate scope property `localFn` will point to - * a function wrapper for the `count = count + value` expression. Often it's desirable to - * pass data from the isolated scope via an expression to the parent scope, this can be - * done by passing a map of local variable names and values into the expression wrapper fn. - * For example, if the expression is `increment(amount)` then we can specify the amount value - * by calling the `localFn` as `localFn({amount: 22})`. - * - * - * #### `bindToController` - * When an isolate scope is used for a component (see above), and `controllerAs` is used, `bindToController: true` will - * allow a component to have its properties bound to the controller, rather than to scope. When the controller - * is instantiated, the initial values of the isolate scope bindings are already available. - * - * #### `controller` - * Controller constructor function. The controller is instantiated before the - * pre-linking phase and it is shared with other directives (see - * `require` attribute). This allows the directives to communicate with each other and augment - * each other's behavior. The controller is injectable (and supports bracket notation) with the following locals: - * - * * `$scope` - Current scope associated with the element - * * `$element` - Current element - * * `$attrs` - Current attributes object for the element - * * `$transclude` - A transclude linking function pre-bound to the correct transclusion scope: - * `function([scope], cloneLinkingFn, futureParentElement)`. - * * `scope`: optional argument to override the scope. - * * `cloneLinkingFn`: optional argument to create clones of the original transcluded content. - * * `futureParentElement`: - * * defines the parent to which the `cloneLinkingFn` will add the cloned elements. - * * default: `$element.parent()` resp. `$element` for `transclude:'element'` resp. `transclude:true`. - * * only needed for transcludes that are allowed to contain non html elements (e.g. SVG elements) - * and when the `cloneLinkinFn` is passed, - * as those elements need to created and cloned in a special way when they are defined outside their - * usual containers (e.g. like ``). - * * See also the `directive.templateNamespace` property. - * - * - * #### `require` - * Require another directive and inject its controller as the fourth argument to the linking function. The - * `require` takes a string name (or array of strings) of the directive(s) to pass in. If an array is used, the - * injected argument will be an array in corresponding order. If no such directive can be - * found, or if the directive does not have a controller, then an error is raised (unless no link function - * is specified, in which case error checking is skipped). The name can be prefixed with: - * - * * (no prefix) - Locate the required controller on the current element. Throw an error if not found. - * * `?` - Attempt to locate the required controller or pass `null` to the `link` fn if not found. - * * `^` - Locate the required controller by searching the element and its parents. Throw an error if not found. - * * `^^` - Locate the required controller by searching the element's parents. Throw an error if not found. - * * `?^` - Attempt to locate the required controller by searching the element and its parents or pass - * `null` to the `link` fn if not found. - * * `?^^` - Attempt to locate the required controller by searching the element's parents, or pass - * `null` to the `link` fn if not found. - * - * - * #### `controllerAs` - * Identifier name for a reference to the controller in the directive's scope. - * This allows the controller to be referenced from the directive template. The directive - * needs to define a scope for this configuration to be used. Useful in the case when - * directive is used as component. - * - * - * #### `restrict` - * String of subset of `EACM` which restricts the directive to a specific directive - * declaration style. If omitted, the defaults (elements and attributes) are used. - * - * * `E` - Element name (default): `` - * * `A` - Attribute (default): `
    ` - * * `C` - Class: `
    ` - * * `M` - Comment: `` - * - * - * #### `templateNamespace` - * String representing the document type used by the markup in the template. - * AngularJS needs this information as those elements need to be created and cloned - * in a special way when they are defined outside their usual containers like `` and ``. - * - * * `html` - All root nodes in the template are HTML. Root nodes may also be - * top-level elements such as `` or ``. - * * `svg` - The root nodes in the template are SVG elements (excluding ``). - * * `math` - The root nodes in the template are MathML elements (excluding ``). - * - * If no `templateNamespace` is specified, then the namespace is considered to be `html`. - * - * #### `template` - * HTML markup that may: - * * Replace the contents of the directive's element (default). - * * Replace the directive's element itself (if `replace` is true - DEPRECATED). - * * Wrap the contents of the directive's element (if `transclude` is true). - * - * Value may be: - * - * * A string. For example `
    {{delete_str}}
    `. - * * A function which takes two arguments `tElement` and `tAttrs` (described in the `compile` - * function api below) and returns a string value. - * - * - * #### `templateUrl` - * This is similar to `template` but the template is loaded from the specified URL, asynchronously. - * - * Because template loading is asynchronous the compiler will suspend compilation of directives on that element - * for later when the template has been resolved. In the meantime it will continue to compile and link - * sibling and parent elements as though this element had not contained any directives. - * - * The compiler does not suspend the entire compilation to wait for templates to be loaded because this - * would result in the whole app "stalling" until all templates are loaded asynchronously - even in the - * case when only one deeply nested directive has `templateUrl`. - * - * Template loading is asynchronous even if the template has been preloaded into the {@link $templateCache} - * - * You can specify `templateUrl` as a string representing the URL or as a function which takes two - * arguments `tElement` and `tAttrs` (described in the `compile` function api below) and returns - * a string value representing the url. In either case, the template URL is passed through {@link - * $sce#getTrustedResourceUrl $sce.getTrustedResourceUrl}. - * - * - * #### `replace` ([*DEPRECATED*!], will be removed in next major release - i.e. v2.0) - * specify what the template should replace. Defaults to `false`. - * - * * `true` - the template will replace the directive's element. - * * `false` - the template will replace the contents of the directive's element. - * - * The replacement process migrates all of the attributes / classes from the old element to the new - * one. See the {@link guide/directive#template-expanding-directive - * Directives Guide} for an example. - * - * There are very few scenarios where element replacement is required for the application function, - * the main one being reusable custom components that are used within SVG contexts - * (because SVG doesn't work with custom elements in the DOM tree). - * - * #### `transclude` - * Extract the contents of the element where the directive appears and make it available to the directive. - * The contents are compiled and provided to the directive as a **transclusion function**. See the - * {@link $compile#transclusion Transclusion} section below. - * - * There are two kinds of transclusion depending upon whether you want to transclude just the contents of the - * directive's element or the entire element: - * - * * `true` - transclude the content (i.e. the child nodes) of the directive's element. - * * `'element'` - transclude the whole of the directive's element including any directives on this - * element that defined at a lower priority than this directive. When used, the `template` - * property is ignored. - * - * - * #### `compile` - * - * ```js - * function compile(tElement, tAttrs, transclude) { ... } - * ``` - * - * The compile function deals with transforming the template DOM. Since most directives do not do - * template transformation, it is not used often. The compile function takes the following arguments: - * - * * `tElement` - template element - The element where the directive has been declared. It is - * safe to do template transformation on the element and child elements only. - * - * * `tAttrs` - template attributes - Normalized list of attributes declared on this element shared - * between all directive compile functions. - * - * * `transclude` - [*DEPRECATED*!] A transclude linking function: `function(scope, cloneLinkingFn)` - * - *
    - * **Note:** The template instance and the link instance may be different objects if the template has - * been cloned. For this reason it is **not** safe to do anything other than DOM transformations that - * apply to all cloned DOM nodes within the compile function. Specifically, DOM listener registration - * should be done in a linking function rather than in a compile function. - *
    - - *
    - * **Note:** The compile function cannot handle directives that recursively use themselves in their - * own templates or compile functions. Compiling these directives results in an infinite loop and a - * stack overflow errors. - * - * This can be avoided by manually using $compile in the postLink function to imperatively compile - * a directive's template instead of relying on automatic template compilation via `template` or - * `templateUrl` declaration or manual compilation inside the compile function. - *
    - * - *
    - * **Note:** The `transclude` function that is passed to the compile function is deprecated, as it - * e.g. does not know about the right outer scope. Please use the transclude function that is passed - * to the link function instead. - *
    - - * A compile function can have a return value which can be either a function or an object. - * - * * returning a (post-link) function - is equivalent to registering the linking function via the - * `link` property of the config object when the compile function is empty. - * - * * returning an object with function(s) registered via `pre` and `post` properties - allows you to - * control when a linking function should be called during the linking phase. See info about - * pre-linking and post-linking functions below. - * - * - * #### `link` - * This property is used only if the `compile` property is not defined. - * - * ```js - * function link(scope, iElement, iAttrs, controller, transcludeFn) { ... } - * ``` - * - * The link function is responsible for registering DOM listeners as well as updating the DOM. It is - * executed after the template has been cloned. This is where most of the directive logic will be - * put. - * - * * `scope` - {@link ng.$rootScope.Scope Scope} - The scope to be used by the - * directive for registering {@link ng.$rootScope.Scope#$watch watches}. - * - * * `iElement` - instance element - The element where the directive is to be used. It is safe to - * manipulate the children of the element only in `postLink` function since the children have - * already been linked. - * - * * `iAttrs` - instance attributes - Normalized list of attributes declared on this element shared - * between all directive linking functions. - * - * * `controller` - the directive's required controller instance(s) - Instances are shared - * among all directives, which allows the directives to use the controllers as a communication - * channel. The exact value depends on the directive's `require` property: - * * no controller(s) required: the directive's own controller, or `undefined` if it doesn't have one - * * `string`: the controller instance - * * `array`: array of controller instances - * - * If a required controller cannot be found, and it is optional, the instance is `null`, - * otherwise the {@link error:$compile:ctreq Missing Required Controller} error is thrown. - * - * Note that you can also require the directive's own controller - it will be made available like - * any other controller. - * - * * `transcludeFn` - A transclude linking function pre-bound to the correct transclusion scope. - * This is the same as the `$transclude` - * parameter of directive controllers, see there for details. - * `function([scope], cloneLinkingFn, futureParentElement)`. - * - * #### Pre-linking function - * - * Executed before the child elements are linked. Not safe to do DOM transformation since the - * compiler linking function will fail to locate the correct elements for linking. - * - * #### Post-linking function - * - * Executed after the child elements are linked. - * - * Note that child elements that contain `templateUrl` directives will not have been compiled - * and linked since they are waiting for their template to load asynchronously and their own - * compilation and linking has been suspended until that occurs. - * - * It is safe to do DOM transformation in the post-linking function on elements that are not waiting - * for their async templates to be resolved. - * - * - * ### Transclusion - * - * Transclusion is the process of extracting a collection of DOM elements from one part of the DOM and - * copying them to another part of the DOM, while maintaining their connection to the original AngularJS - * scope from where they were taken. - * - * Transclusion is used (often with {@link ngTransclude}) to insert the - * original contents of a directive's element into a specified place in the template of the directive. - * The benefit of transclusion, over simply moving the DOM elements manually, is that the transcluded - * content has access to the properties on the scope from which it was taken, even if the directive - * has isolated scope. - * See the {@link guide/directive#creating-a-directive-that-wraps-other-elements Directives Guide}. - * - * This makes it possible for the widget to have private state for its template, while the transcluded - * content has access to its originating scope. - * - *
    - * **Note:** When testing an element transclude directive you must not place the directive at the root of the - * DOM fragment that is being compiled. See {@link guide/unit-testing#testing-transclusion-directives - * Testing Transclusion Directives}. - *
    - * - * #### Transclusion Functions - * - * When a directive requests transclusion, the compiler extracts its contents and provides a **transclusion - * function** to the directive's `link` function and `controller`. This transclusion function is a special - * **linking function** that will return the compiled contents linked to a new transclusion scope. - * - *
    - * If you are just using {@link ngTransclude} then you don't need to worry about this function, since - * ngTransclude will deal with it for us. - *
    - * - * If you want to manually control the insertion and removal of the transcluded content in your directive - * then you must use this transclude function. When you call a transclude function it returns a a jqLite/JQuery - * object that contains the compiled DOM, which is linked to the correct transclusion scope. - * - * When you call a transclusion function you can pass in a **clone attach function**. This function accepts - * two parameters, `function(clone, scope) { ... }`, where the `clone` is a fresh compiled copy of your transcluded - * content and the `scope` is the newly created transclusion scope, to which the clone is bound. - * - *
    - * **Best Practice**: Always provide a `cloneFn` (clone attach function) when you call a translude function - * since you then get a fresh clone of the original DOM and also have access to the new transclusion scope. - *
    - * - * It is normal practice to attach your transcluded content (`clone`) to the DOM inside your **clone - * attach function**: - * - * ```js - * var transcludedContent, transclusionScope; - * - * $transclude(function(clone, scope) { - * element.append(clone); - * transcludedContent = clone; - * transclusionScope = scope; - * }); - * ``` - * - * Later, if you want to remove the transcluded content from your DOM then you should also destroy the - * associated transclusion scope: - * - * ```js - * transcludedContent.remove(); - * transclusionScope.$destroy(); - * ``` - * - *
    - * **Best Practice**: if you intend to add and remove transcluded content manually in your directive - * (by calling the transclude function to get the DOM and calling `element.remove()` to remove it), - * then you are also responsible for calling `$destroy` on the transclusion scope. - *
    - * - * The built-in DOM manipulation directives, such as {@link ngIf}, {@link ngSwitch} and {@link ngRepeat} - * automatically destroy their transluded clones as necessary so you do not need to worry about this if - * you are simply using {@link ngTransclude} to inject the transclusion into your directive. - * - * - * #### Transclusion Scopes - * - * When you call a transclude function it returns a DOM fragment that is pre-bound to a **transclusion - * scope**. This scope is special, in that it is a child of the directive's scope (and so gets destroyed - * when the directive's scope gets destroyed) but it inherits the properties of the scope from which it - * was taken. - * - * For example consider a directive that uses transclusion and isolated scope. The DOM hierarchy might look - * like this: - * - * ```html - *
    - *
    - *
    - *
    - *
    - *
    - * ``` - * - * The `$parent` scope hierarchy will look like this: - * - * ``` - * - $rootScope - * - isolate - * - transclusion - * ``` - * - * but the scopes will inherit prototypically from different scopes to their `$parent`. - * - * ``` - * - $rootScope - * - transclusion - * - isolate - * ``` - * - * - * ### Attributes - * - * The {@link ng.$compile.directive.Attributes Attributes} object - passed as a parameter in the - * `link()` or `compile()` functions. It has a variety of uses. - * - * accessing *Normalized attribute names:* - * Directives like 'ngBind' can be expressed in many ways: 'ng:bind', `data-ng-bind`, or 'x-ng-bind'. - * the attributes object allows for normalized access to - * the attributes. - * - * * *Directive inter-communication:* All directives share the same instance of the attributes - * object which allows the directives to use the attributes object as inter directive - * communication. - * - * * *Supports interpolation:* Interpolation attributes are assigned to the attribute object - * allowing other directives to read the interpolated value. - * - * * *Observing interpolated attributes:* Use `$observe` to observe the value changes of attributes - * that contain interpolation (e.g. `src="{{bar}}"`). Not only is this very efficient but it's also - * the only way to easily get the actual value because during the linking phase the interpolation - * hasn't been evaluated yet and so the value is at this time set to `undefined`. - * - * ```js - * function linkingFn(scope, elm, attrs, ctrl) { - * // get the attribute value - * console.log(attrs.ngModel); - * - * // change the attribute - * attrs.$set('ngModel', 'new value'); - * - * // observe changes to interpolated attribute - * attrs.$observe('ngModel', function(value) { - * console.log('ngModel has changed value to ' + value); - * }); - * } - * ``` - * - * ## Example - * - *
    - * **Note**: Typically directives are registered with `module.directive`. The example below is - * to illustrate how `$compile` works. - *
    - * - - - -
    -
    -
    -
    -
    -
    - - it('should auto compile', function() { - var textarea = $('textarea'); - var output = $('div[compile]'); - // The initial state reads 'Hello Angular'. - expect(output.getText()).toBe('Hello Angular'); - textarea.clear(); - textarea.sendKeys('{{name}}!'); - expect(output.getText()).toBe('Angular!'); - }); - -
    - - * - * - * @param {string|DOMElement} element Element or HTML string to compile into a template function. - * @param {function(angular.Scope, cloneAttachFn=)} transclude function available to directives - DEPRECATED. - * - *
    - * **Note:** Passing a `transclude` function to the $compile function is deprecated, as it - * e.g. will not use the right outer scope. Please pass the transclude function as a - * `parentBoundTranscludeFn` to the link function instead. - *
    - * - * @param {number} maxPriority only apply directives lower than given priority (Only effects the - * root element(s), not their children) - * @returns {function(scope, cloneAttachFn=, options=)} a link function which is used to bind template - * (a DOM element/tree) to a scope. Where: - * - * * `scope` - A {@link ng.$rootScope.Scope Scope} to bind to. - * * `cloneAttachFn` - If `cloneAttachFn` is provided, then the link function will clone the - * `template` and call the `cloneAttachFn` function allowing the caller to attach the - * cloned elements to the DOM document at the appropriate place. The `cloneAttachFn` is - * called as:
    `cloneAttachFn(clonedElement, scope)` where: - * - * * `clonedElement` - is a clone of the original `element` passed into the compiler. - * * `scope` - is the current scope with which the linking function is working with. - * - * * `options` - An optional object hash with linking options. If `options` is provided, then the following - * keys may be used to control linking behavior: - * - * * `parentBoundTranscludeFn` - the transclude function made available to - * directives; if given, it will be passed through to the link functions of - * directives found in `element` during compilation. - * * `transcludeControllers` - an object hash with keys that map controller names - * to controller instances; if given, it will make the controllers - * available to directives. - * * `futureParentElement` - defines the parent to which the `cloneAttachFn` will add - * the cloned elements; only needed for transcludes that are allowed to contain non html - * elements (e.g. SVG elements). See also the directive.controller property. - * - * Calling the linking function returns the element of the template. It is either the original - * element passed in, or the clone of the element if the `cloneAttachFn` is provided. - * - * After linking the view is not updated until after a call to $digest which typically is done by - * Angular automatically. - * - * If you need access to the bound view, there are two ways to do it: - * - * - If you are not asking the linking function to clone the template, create the DOM element(s) - * before you send them to the compiler and keep this reference around. - * ```js - * var element = $compile('

    {{total}}

    ')(scope); - * ``` - * - * - if on the other hand, you need the element to be cloned, the view reference from the original - * example would not point to the clone, but rather to the original template that was cloned. In - * this case, you can access the clone via the cloneAttachFn: - * ```js - * var templateElement = angular.element('

    {{total}}

    '), - * scope = ....; - * - * var clonedElement = $compile(templateElement)(scope, function(clonedElement, scope) { - * //attach the clone to DOM document at the right place - * }); - * - * //now we have reference to the cloned DOM via `clonedElement` - * ``` - * - * - * For information on how the compiler works, see the - * {@link guide/compiler Angular HTML Compiler} section of the Developer Guide. - */ - -var $compileMinErr = minErr('$compile'); - -/** - * @ngdoc provider - * @name $compileProvider - * - * @description - */ -$CompileProvider.$inject = ['$provide', '$$sanitizeUriProvider']; -function $CompileProvider($provide, $$sanitizeUriProvider) { - var hasDirectives = {}, - Suffix = 'Directive', - COMMENT_DIRECTIVE_REGEXP = /^\s*directive\:\s*([\w\-]+)\s+(.*)$/, - CLASS_DIRECTIVE_REGEXP = /(([\w\-]+)(?:\:([^;]+))?;?)/, - ALL_OR_NOTHING_ATTRS = makeMap('ngSrc,ngSrcset,src,srcset'), - REQUIRE_PREFIX_REGEXP = /^(?:(\^\^?)?(\?)?(\^\^?)?)?/; - - // Ref: http://developers.whatwg.org/webappapis.html#event-handler-idl-attributes - // The assumption is that future DOM event attribute names will begin with - // 'on' and be composed of only English letters. - var EVENT_HANDLER_ATTR_REGEXP = /^(on[a-z]+|formaction)$/; - - function parseIsolateBindings(scope, directiveName, isController) { - var LOCAL_REGEXP = /^\s*([@&]|=(\*?))(\??)\s*(\w*)\s*$/; - - var bindings = {}; - - forEach(scope, function(definition, scopeName) { - var match = definition.match(LOCAL_REGEXP); - - if (!match) { - throw $compileMinErr('iscp', - "Invalid {3} for directive '{0}'." + - " Definition: {... {1}: '{2}' ...}", - directiveName, scopeName, definition, - (isController ? "controller bindings definition" : - "isolate scope definition")); - } - - bindings[scopeName] = { - mode: match[1][0], - collection: match[2] === '*', - optional: match[3] === '?', - attrName: match[4] || scopeName - }; - }); - - return bindings; - } - - function parseDirectiveBindings(directive, directiveName) { - var bindings = { - isolateScope: null, - bindToController: null - }; - if (isObject(directive.scope)) { - if (directive.bindToController === true) { - bindings.bindToController = parseIsolateBindings(directive.scope, - directiveName, true); - bindings.isolateScope = {}; - } else { - bindings.isolateScope = parseIsolateBindings(directive.scope, - directiveName, false); - } - } - if (isObject(directive.bindToController)) { - bindings.bindToController = - parseIsolateBindings(directive.bindToController, directiveName, true); - } - if (isObject(bindings.bindToController)) { - var controller = directive.controller; - var controllerAs = directive.controllerAs; - if (!controller) { - // There is no controller, there may or may not be a controllerAs property - throw $compileMinErr('noctrl', - "Cannot bind to controller without directive '{0}'s controller.", - directiveName); - } else if (!identifierForController(controller, controllerAs)) { - // There is a controller, but no identifier or controllerAs property - throw $compileMinErr('noident', - "Cannot bind to controller without identifier for directive '{0}'.", - directiveName); - } - } - return bindings; - } - - function assertValidDirectiveName(name) { - var letter = name.charAt(0); - if (!letter || letter !== lowercase(letter)) { - throw $compileMinErr('baddir', "Directive name '{0}' is invalid. The first character must be a lowercase letter", name); - } - if (name !== name.trim()) { - throw $compileMinErr('baddir', - "Directive name '{0}' is invalid. The name should not contain leading or trailing whitespaces", - name); - } - } - - /** - * @ngdoc method - * @name $compileProvider#directive - * @kind function - * - * @description - * Register a new directive with the compiler. - * - * @param {string|Object} name Name of the directive in camel-case (i.e. ngBind which - * will match as ng-bind), or an object map of directives where the keys are the - * names and the values are the factories. - * @param {Function|Array} directiveFactory An injectable directive factory function. See - * {@link guide/directive} for more info. - * @returns {ng.$compileProvider} Self for chaining. - */ - this.directive = function registerDirective(name, directiveFactory) { - assertNotHasOwnProperty(name, 'directive'); - if (isString(name)) { - assertValidDirectiveName(name); - assertArg(directiveFactory, 'directiveFactory'); - if (!hasDirectives.hasOwnProperty(name)) { - hasDirectives[name] = []; - $provide.factory(name + Suffix, ['$injector', '$exceptionHandler', - function($injector, $exceptionHandler) { - var directives = []; - forEach(hasDirectives[name], function(directiveFactory, index) { - try { - var directive = $injector.invoke(directiveFactory); - if (isFunction(directive)) { - directive = { compile: valueFn(directive) }; - } else if (!directive.compile && directive.link) { - directive.compile = valueFn(directive.link); - } - directive.priority = directive.priority || 0; - directive.index = index; - directive.name = directive.name || name; - directive.require = directive.require || (directive.controller && directive.name); - directive.restrict = directive.restrict || 'EA'; - var bindings = directive.$$bindings = - parseDirectiveBindings(directive, directive.name); - if (isObject(bindings.isolateScope)) { - directive.$$isolateBindings = bindings.isolateScope; - } - directive.$$moduleName = directiveFactory.$$moduleName; - directives.push(directive); - } catch (e) { - $exceptionHandler(e); - } - }); - return directives; - }]); - } - hasDirectives[name].push(directiveFactory); - } else { - forEach(name, reverseParams(registerDirective)); - } - return this; - }; - - - /** - * @ngdoc method - * @name $compileProvider#aHrefSanitizationWhitelist - * @kind function - * - * @description - * Retrieves or overrides the default regular expression that is used for whitelisting of safe - * urls during a[href] sanitization. - * - * The sanitization is a security measure aimed at preventing XSS attacks via html links. - * - * Any url about to be assigned to a[href] via data-binding is first normalized and turned into - * an absolute url. Afterwards, the url is matched against the `aHrefSanitizationWhitelist` - * regular expression. If a match is found, the original url is written into the dom. Otherwise, - * the absolute url is prefixed with `'unsafe:'` string and only then is it written into the DOM. - * - * @param {RegExp=} regexp New regexp to whitelist urls with. - * @returns {RegExp|ng.$compileProvider} Current RegExp if called without value or self for - * chaining otherwise. - */ - this.aHrefSanitizationWhitelist = function(regexp) { - if (isDefined(regexp)) { - $$sanitizeUriProvider.aHrefSanitizationWhitelist(regexp); - return this; - } else { - return $$sanitizeUriProvider.aHrefSanitizationWhitelist(); - } - }; - - - /** - * @ngdoc method - * @name $compileProvider#imgSrcSanitizationWhitelist - * @kind function - * - * @description - * Retrieves or overrides the default regular expression that is used for whitelisting of safe - * urls during img[src] sanitization. - * - * The sanitization is a security measure aimed at prevent XSS attacks via html links. - * - * Any url about to be assigned to img[src] via data-binding is first normalized and turned into - * an absolute url. Afterwards, the url is matched against the `imgSrcSanitizationWhitelist` - * regular expression. If a match is found, the original url is written into the dom. Otherwise, - * the absolute url is prefixed with `'unsafe:'` string and only then is it written into the DOM. - * - * @param {RegExp=} regexp New regexp to whitelist urls with. - * @returns {RegExp|ng.$compileProvider} Current RegExp if called without value or self for - * chaining otherwise. - */ - this.imgSrcSanitizationWhitelist = function(regexp) { - if (isDefined(regexp)) { - $$sanitizeUriProvider.imgSrcSanitizationWhitelist(regexp); - return this; - } else { - return $$sanitizeUriProvider.imgSrcSanitizationWhitelist(); - } - }; - - /** - * @ngdoc method - * @name $compileProvider#debugInfoEnabled - * - * @param {boolean=} enabled update the debugInfoEnabled state if provided, otherwise just return the - * current debugInfoEnabled state - * @returns {*} current value if used as getter or itself (chaining) if used as setter - * - * @kind function - * - * @description - * Call this method to enable/disable various debug runtime information in the compiler such as adding - * binding information and a reference to the current scope on to DOM elements. - * If enabled, the compiler will add the following to DOM elements that have been bound to the scope - * * `ng-binding` CSS class - * * `$binding` data property containing an array of the binding expressions - * - * You may want to disable this in production for a significant performance boost. See - * {@link guide/production#disabling-debug-data Disabling Debug Data} for more. - * - * The default value is true. - */ - var debugInfoEnabled = true; - this.debugInfoEnabled = function(enabled) { - if (isDefined(enabled)) { - debugInfoEnabled = enabled; - return this; - } - return debugInfoEnabled; - }; - - this.$get = [ - '$injector', '$interpolate', '$exceptionHandler', '$templateRequest', '$parse', - '$controller', '$rootScope', '$document', '$sce', '$animate', '$$sanitizeUri', - function($injector, $interpolate, $exceptionHandler, $templateRequest, $parse, - $controller, $rootScope, $document, $sce, $animate, $$sanitizeUri) { - - var Attributes = function(element, attributesToCopy) { - if (attributesToCopy) { - var keys = Object.keys(attributesToCopy); - var i, l, key; - - for (i = 0, l = keys.length; i < l; i++) { - key = keys[i]; - this[key] = attributesToCopy[key]; - } - } else { - this.$attr = {}; - } - - this.$$element = element; - }; - - Attributes.prototype = { - /** - * @ngdoc method - * @name $compile.directive.Attributes#$normalize - * @kind function - * - * @description - * Converts an attribute name (e.g. dash/colon/underscore-delimited string, optionally prefixed with `x-` or - * `data-`) to its normalized, camelCase form. - * - * Also there is special case for Moz prefix starting with upper case letter. - * - * For further information check out the guide on {@link guide/directive#matching-directives Matching Directives} - * - * @param {string} name Name to normalize - */ - $normalize: directiveNormalize, - - - /** - * @ngdoc method - * @name $compile.directive.Attributes#$addClass - * @kind function - * - * @description - * Adds the CSS class value specified by the classVal parameter to the element. If animations - * are enabled then an animation will be triggered for the class addition. - * - * @param {string} classVal The className value that will be added to the element - */ - $addClass: function(classVal) { - if (classVal && classVal.length > 0) { - $animate.addClass(this.$$element, classVal); - } - }, - - /** - * @ngdoc method - * @name $compile.directive.Attributes#$removeClass - * @kind function - * - * @description - * Removes the CSS class value specified by the classVal parameter from the element. If - * animations are enabled then an animation will be triggered for the class removal. - * - * @param {string} classVal The className value that will be removed from the element - */ - $removeClass: function(classVal) { - if (classVal && classVal.length > 0) { - $animate.removeClass(this.$$element, classVal); - } - }, - - /** - * @ngdoc method - * @name $compile.directive.Attributes#$updateClass - * @kind function - * - * @description - * Adds and removes the appropriate CSS class values to the element based on the difference - * between the new and old CSS class values (specified as newClasses and oldClasses). - * - * @param {string} newClasses The current CSS className value - * @param {string} oldClasses The former CSS className value - */ - $updateClass: function(newClasses, oldClasses) { - var toAdd = tokenDifference(newClasses, oldClasses); - if (toAdd && toAdd.length) { - $animate.addClass(this.$$element, toAdd); - } - - var toRemove = tokenDifference(oldClasses, newClasses); - if (toRemove && toRemove.length) { - $animate.removeClass(this.$$element, toRemove); - } - }, - - /** - * Set a normalized attribute on the element in a way such that all directives - * can share the attribute. This function properly handles boolean attributes. - * @param {string} key Normalized key. (ie ngAttribute) - * @param {string|boolean} value The value to set. If `null` attribute will be deleted. - * @param {boolean=} writeAttr If false, does not write the value to DOM element attribute. - * Defaults to true. - * @param {string=} attrName Optional none normalized name. Defaults to key. - */ - $set: function(key, value, writeAttr, attrName) { - // TODO: decide whether or not to throw an error if "class" - //is set through this function since it may cause $updateClass to - //become unstable. - - var node = this.$$element[0], - booleanKey = getBooleanAttrName(node, key), - aliasedKey = getAliasedAttrName(node, key), - observer = key, - nodeName; - - if (booleanKey) { - this.$$element.prop(key, value); - attrName = booleanKey; - } else if (aliasedKey) { - this[aliasedKey] = value; - observer = aliasedKey; - } - - this[key] = value; - - // translate normalized key to actual key - if (attrName) { - this.$attr[key] = attrName; - } else { - attrName = this.$attr[key]; - if (!attrName) { - this.$attr[key] = attrName = snake_case(key, '-'); - } - } - - nodeName = nodeName_(this.$$element); - - if ((nodeName === 'a' && key === 'href') || - (nodeName === 'img' && key === 'src')) { - // sanitize a[href] and img[src] values - this[key] = value = $$sanitizeUri(value, key === 'src'); - } else if (nodeName === 'img' && key === 'srcset') { - // sanitize img[srcset] values - var result = ""; - - // first check if there are spaces because it's not the same pattern - var trimmedSrcset = trim(value); - // ( 999x ,| 999w ,| ,|, ) - var srcPattern = /(\s+\d+x\s*,|\s+\d+w\s*,|\s+,|,\s+)/; - var pattern = /\s/.test(trimmedSrcset) ? srcPattern : /(,)/; - - // split srcset into tuple of uri and descriptor except for the last item - var rawUris = trimmedSrcset.split(pattern); - - // for each tuples - var nbrUrisWith2parts = Math.floor(rawUris.length / 2); - for (var i = 0; i < nbrUrisWith2parts; i++) { - var innerIdx = i * 2; - // sanitize the uri - result += $$sanitizeUri(trim(rawUris[innerIdx]), true); - // add the descriptor - result += (" " + trim(rawUris[innerIdx + 1])); - } - - // split the last item into uri and descriptor - var lastTuple = trim(rawUris[i * 2]).split(/\s/); - - // sanitize the last uri - result += $$sanitizeUri(trim(lastTuple[0]), true); - - // and add the last descriptor if any - if (lastTuple.length === 2) { - result += (" " + trim(lastTuple[1])); - } - this[key] = value = result; - } - - if (writeAttr !== false) { - if (value === null || value === undefined) { - this.$$element.removeAttr(attrName); - } else { - this.$$element.attr(attrName, value); - } - } - - // fire observers - var $$observers = this.$$observers; - $$observers && forEach($$observers[observer], function(fn) { - try { - fn(value); - } catch (e) { - $exceptionHandler(e); - } - }); - }, - - - /** - * @ngdoc method - * @name $compile.directive.Attributes#$observe - * @kind function - * - * @description - * Observes an interpolated attribute. - * - * The observer function will be invoked once during the next `$digest` following - * compilation. The observer is then invoked whenever the interpolated value - * changes. - * - * @param {string} key Normalized key. (ie ngAttribute) . - * @param {function(interpolatedValue)} fn Function that will be called whenever - the interpolated value of the attribute changes. - * See the {@link guide/directive#text-and-attribute-bindings Directives} guide for more info. - * @returns {function()} Returns a deregistration function for this observer. - */ - $observe: function(key, fn) { - var attrs = this, - $$observers = (attrs.$$observers || (attrs.$$observers = createMap())), - listeners = ($$observers[key] || ($$observers[key] = [])); - - listeners.push(fn); - $rootScope.$evalAsync(function() { - if (!listeners.$$inter && attrs.hasOwnProperty(key) && !isUndefined(attrs[key])) { - // no one registered attribute interpolation function, so lets call it manually - fn(attrs[key]); - } - }); - - return function() { - arrayRemove(listeners, fn); - }; - } - }; - - - function safeAddClass($element, className) { - try { - $element.addClass(className); - } catch (e) { - // ignore, since it means that we are trying to set class on - // SVG element, where class name is read-only. - } - } - - - var startSymbol = $interpolate.startSymbol(), - endSymbol = $interpolate.endSymbol(), - denormalizeTemplate = (startSymbol == '{{' || endSymbol == '}}') - ? identity - : function denormalizeTemplate(template) { - return template.replace(/\{\{/g, startSymbol).replace(/}}/g, endSymbol); - }, - NG_ATTR_BINDING = /^ngAttr[A-Z]/; - - compile.$$addBindingInfo = debugInfoEnabled ? function $$addBindingInfo($element, binding) { - var bindings = $element.data('$binding') || []; - - if (isArray(binding)) { - bindings = bindings.concat(binding); - } else { - bindings.push(binding); - } - - $element.data('$binding', bindings); - } : noop; - - compile.$$addBindingClass = debugInfoEnabled ? function $$addBindingClass($element) { - safeAddClass($element, 'ng-binding'); - } : noop; - - compile.$$addScopeInfo = debugInfoEnabled ? function $$addScopeInfo($element, scope, isolated, noTemplate) { - var dataName = isolated ? (noTemplate ? '$isolateScopeNoTemplate' : '$isolateScope') : '$scope'; - $element.data(dataName, scope); - } : noop; - - compile.$$addScopeClass = debugInfoEnabled ? function $$addScopeClass($element, isolated) { - safeAddClass($element, isolated ? 'ng-isolate-scope' : 'ng-scope'); - } : noop; - - return compile; - - //================================ - - function compile($compileNodes, transcludeFn, maxPriority, ignoreDirective, - previousCompileContext) { - if (!($compileNodes instanceof jqLite)) { - // jquery always rewraps, whereas we need to preserve the original selector so that we can - // modify it. - $compileNodes = jqLite($compileNodes); - } - // We can not compile top level text elements since text nodes can be merged and we will - // not be able to attach scope data to them, so we will wrap them in - forEach($compileNodes, function(node, index) { - if (node.nodeType == NODE_TYPE_TEXT && node.nodeValue.match(/\S+/) /* non-empty */ ) { - $compileNodes[index] = jqLite(node).wrap('').parent()[0]; - } - }); - var compositeLinkFn = - compileNodes($compileNodes, transcludeFn, $compileNodes, - maxPriority, ignoreDirective, previousCompileContext); - compile.$$addScopeClass($compileNodes); - var namespace = null; - return function publicLinkFn(scope, cloneConnectFn, options) { - assertArg(scope, 'scope'); - - options = options || {}; - var parentBoundTranscludeFn = options.parentBoundTranscludeFn, - transcludeControllers = options.transcludeControllers, - futureParentElement = options.futureParentElement; - - // When `parentBoundTranscludeFn` is passed, it is a - // `controllersBoundTransclude` function (it was previously passed - // as `transclude` to directive.link) so we must unwrap it to get - // its `boundTranscludeFn` - if (parentBoundTranscludeFn && parentBoundTranscludeFn.$$boundTransclude) { - parentBoundTranscludeFn = parentBoundTranscludeFn.$$boundTransclude; - } - - if (!namespace) { - namespace = detectNamespaceForChildElements(futureParentElement); - } - var $linkNode; - if (namespace !== 'html') { - // When using a directive with replace:true and templateUrl the $compileNodes - // (or a child element inside of them) - // might change, so we need to recreate the namespace adapted compileNodes - // for call to the link function. - // Note: This will already clone the nodes... - $linkNode = jqLite( - wrapTemplate(namespace, jqLite('
    ').append($compileNodes).html()) - ); - } else if (cloneConnectFn) { - // important!!: we must call our jqLite.clone() since the jQuery one is trying to be smart - // and sometimes changes the structure of the DOM. - $linkNode = JQLitePrototype.clone.call($compileNodes); - } else { - $linkNode = $compileNodes; - } - - if (transcludeControllers) { - for (var controllerName in transcludeControllers) { - $linkNode.data('$' + controllerName + 'Controller', transcludeControllers[controllerName].instance); - } - } - - compile.$$addScopeInfo($linkNode, scope); - - if (cloneConnectFn) cloneConnectFn($linkNode, scope); - if (compositeLinkFn) compositeLinkFn(scope, $linkNode, $linkNode, parentBoundTranscludeFn); - return $linkNode; - }; - } - - function detectNamespaceForChildElements(parentElement) { - // TODO: Make this detect MathML as well... - var node = parentElement && parentElement[0]; - if (!node) { - return 'html'; - } else { - return nodeName_(node) !== 'foreignobject' && node.toString().match(/SVG/) ? 'svg' : 'html'; - } - } - - /** - * Compile function matches each node in nodeList against the directives. Once all directives - * for a particular node are collected their compile functions are executed. The compile - * functions return values - the linking functions - are combined into a composite linking - * function, which is the a linking function for the node. - * - * @param {NodeList} nodeList an array of nodes or NodeList to compile - * @param {function(angular.Scope, cloneAttachFn=)} transcludeFn A linking function, where the - * scope argument is auto-generated to the new child of the transcluded parent scope. - * @param {DOMElement=} $rootElement If the nodeList is the root of the compilation tree then - * the rootElement must be set the jqLite collection of the compile root. This is - * needed so that the jqLite collection items can be replaced with widgets. - * @param {number=} maxPriority Max directive priority. - * @returns {Function} A composite linking function of all of the matched directives or null. - */ - function compileNodes(nodeList, transcludeFn, $rootElement, maxPriority, ignoreDirective, - previousCompileContext) { - var linkFns = [], - attrs, directives, nodeLinkFn, childNodes, childLinkFn, linkFnFound, nodeLinkFnFound; - - for (var i = 0; i < nodeList.length; i++) { - attrs = new Attributes(); - - // we must always refer to nodeList[i] since the nodes can be replaced underneath us. - directives = collectDirectives(nodeList[i], [], attrs, i === 0 ? maxPriority : undefined, - ignoreDirective); - - nodeLinkFn = (directives.length) - ? applyDirectivesToNode(directives, nodeList[i], attrs, transcludeFn, $rootElement, - null, [], [], previousCompileContext) - : null; - - if (nodeLinkFn && nodeLinkFn.scope) { - compile.$$addScopeClass(attrs.$$element); - } - - childLinkFn = (nodeLinkFn && nodeLinkFn.terminal || - !(childNodes = nodeList[i].childNodes) || - !childNodes.length) - ? null - : compileNodes(childNodes, - nodeLinkFn ? ( - (nodeLinkFn.transcludeOnThisElement || !nodeLinkFn.templateOnThisElement) - && nodeLinkFn.transclude) : transcludeFn); - - if (nodeLinkFn || childLinkFn) { - linkFns.push(i, nodeLinkFn, childLinkFn); - linkFnFound = true; - nodeLinkFnFound = nodeLinkFnFound || nodeLinkFn; - } - - //use the previous context only for the first element in the virtual group - previousCompileContext = null; - } - - // return a linking function if we have found anything, null otherwise - return linkFnFound ? compositeLinkFn : null; - - function compositeLinkFn(scope, nodeList, $rootElement, parentBoundTranscludeFn) { - var nodeLinkFn, childLinkFn, node, childScope, i, ii, idx, childBoundTranscludeFn; - var stableNodeList; - - - if (nodeLinkFnFound) { - // copy nodeList so that if a nodeLinkFn removes or adds an element at this DOM level our - // offsets don't get screwed up - var nodeListLength = nodeList.length; - stableNodeList = new Array(nodeListLength); - - // create a sparse array by only copying the elements which have a linkFn - for (i = 0; i < linkFns.length; i+=3) { - idx = linkFns[i]; - stableNodeList[idx] = nodeList[idx]; - } - } else { - stableNodeList = nodeList; - } - - for (i = 0, ii = linkFns.length; i < ii;) { - node = stableNodeList[linkFns[i++]]; - nodeLinkFn = linkFns[i++]; - childLinkFn = linkFns[i++]; - - if (nodeLinkFn) { - if (nodeLinkFn.scope) { - childScope = scope.$new(); - compile.$$addScopeInfo(jqLite(node), childScope); - var destroyBindings = nodeLinkFn.$$destroyBindings; - if (destroyBindings) { - nodeLinkFn.$$destroyBindings = null; - childScope.$on('$destroyed', destroyBindings); - } - } else { - childScope = scope; - } - - if (nodeLinkFn.transcludeOnThisElement) { - childBoundTranscludeFn = createBoundTranscludeFn( - scope, nodeLinkFn.transclude, parentBoundTranscludeFn); - - } else if (!nodeLinkFn.templateOnThisElement && parentBoundTranscludeFn) { - childBoundTranscludeFn = parentBoundTranscludeFn; - - } else if (!parentBoundTranscludeFn && transcludeFn) { - childBoundTranscludeFn = createBoundTranscludeFn(scope, transcludeFn); - - } else { - childBoundTranscludeFn = null; - } - - nodeLinkFn(childLinkFn, childScope, node, $rootElement, childBoundTranscludeFn, - nodeLinkFn); - - } else if (childLinkFn) { - childLinkFn(scope, node.childNodes, undefined, parentBoundTranscludeFn); - } - } - } - } - - function createBoundTranscludeFn(scope, transcludeFn, previousBoundTranscludeFn) { - - var boundTranscludeFn = function(transcludedScope, cloneFn, controllers, futureParentElement, containingScope) { - - if (!transcludedScope) { - transcludedScope = scope.$new(false, containingScope); - transcludedScope.$$transcluded = true; - } - - return transcludeFn(transcludedScope, cloneFn, { - parentBoundTranscludeFn: previousBoundTranscludeFn, - transcludeControllers: controllers, - futureParentElement: futureParentElement - }); - }; - - return boundTranscludeFn; - } - - /** - * Looks for directives on the given node and adds them to the directive collection which is - * sorted. - * - * @param node Node to search. - * @param directives An array to which the directives are added to. This array is sorted before - * the function returns. - * @param attrs The shared attrs object which is used to populate the normalized attributes. - * @param {number=} maxPriority Max directive priority. - */ - function collectDirectives(node, directives, attrs, maxPriority, ignoreDirective) { - var nodeType = node.nodeType, - attrsMap = attrs.$attr, - match, - className; - - switch (nodeType) { - case NODE_TYPE_ELEMENT: /* Element */ - // use the node name: - addDirective(directives, - directiveNormalize(nodeName_(node)), 'E', maxPriority, ignoreDirective); - - // iterate over the attributes - for (var attr, name, nName, ngAttrName, value, isNgAttr, nAttrs = node.attributes, - j = 0, jj = nAttrs && nAttrs.length; j < jj; j++) { - var attrStartName = false; - var attrEndName = false; - - attr = nAttrs[j]; - name = attr.name; - value = trim(attr.value); - - // support ngAttr attribute binding - ngAttrName = directiveNormalize(name); - if (isNgAttr = NG_ATTR_BINDING.test(ngAttrName)) { - name = name.replace(PREFIX_REGEXP, '') - .substr(8).replace(/_(.)/g, function(match, letter) { - return letter.toUpperCase(); - }); - } - - var directiveNName = ngAttrName.replace(/(Start|End)$/, ''); - if (directiveIsMultiElement(directiveNName)) { - if (ngAttrName === directiveNName + 'Start') { - attrStartName = name; - attrEndName = name.substr(0, name.length - 5) + 'end'; - name = name.substr(0, name.length - 6); - } - } - - nName = directiveNormalize(name.toLowerCase()); - attrsMap[nName] = name; - if (isNgAttr || !attrs.hasOwnProperty(nName)) { - attrs[nName] = value; - if (getBooleanAttrName(node, nName)) { - attrs[nName] = true; // presence means true - } - } - addAttrInterpolateDirective(node, directives, value, nName, isNgAttr); - addDirective(directives, nName, 'A', maxPriority, ignoreDirective, attrStartName, - attrEndName); - } - - // use class as directive - className = node.className; - if (isObject(className)) { - // Maybe SVGAnimatedString - className = className.animVal; - } - if (isString(className) && className !== '') { - while (match = CLASS_DIRECTIVE_REGEXP.exec(className)) { - nName = directiveNormalize(match[2]); - if (addDirective(directives, nName, 'C', maxPriority, ignoreDirective)) { - attrs[nName] = trim(match[3]); - } - className = className.substr(match.index + match[0].length); - } - } - break; - case NODE_TYPE_TEXT: /* Text Node */ - if (msie === 11) { - // Workaround for #11781 - while (node.parentNode && node.nextSibling && node.nextSibling.nodeType === NODE_TYPE_TEXT) { - node.nodeValue = node.nodeValue + node.nextSibling.nodeValue; - node.parentNode.removeChild(node.nextSibling); - } - } - addTextInterpolateDirective(directives, node.nodeValue); - break; - case NODE_TYPE_COMMENT: /* Comment */ - try { - match = COMMENT_DIRECTIVE_REGEXP.exec(node.nodeValue); - if (match) { - nName = directiveNormalize(match[1]); - if (addDirective(directives, nName, 'M', maxPriority, ignoreDirective)) { - attrs[nName] = trim(match[2]); - } - } - } catch (e) { - // turns out that under some circumstances IE9 throws errors when one attempts to read - // comment's node value. - // Just ignore it and continue. (Can't seem to reproduce in test case.) - } - break; - } - - directives.sort(byPriority); - return directives; - } - - /** - * Given a node with an directive-start it collects all of the siblings until it finds - * directive-end. - * @param node - * @param attrStart - * @param attrEnd - * @returns {*} - */ - function groupScan(node, attrStart, attrEnd) { - var nodes = []; - var depth = 0; - if (attrStart && node.hasAttribute && node.hasAttribute(attrStart)) { - do { - if (!node) { - throw $compileMinErr('uterdir', - "Unterminated attribute, found '{0}' but no matching '{1}' found.", - attrStart, attrEnd); - } - if (node.nodeType == NODE_TYPE_ELEMENT) { - if (node.hasAttribute(attrStart)) depth++; - if (node.hasAttribute(attrEnd)) depth--; - } - nodes.push(node); - node = node.nextSibling; - } while (depth > 0); - } else { - nodes.push(node); - } - - return jqLite(nodes); - } - - /** - * Wrapper for linking function which converts normal linking function into a grouped - * linking function. - * @param linkFn - * @param attrStart - * @param attrEnd - * @returns {Function} - */ - function groupElementsLinkFnWrapper(linkFn, attrStart, attrEnd) { - return function(scope, element, attrs, controllers, transcludeFn) { - element = groupScan(element[0], attrStart, attrEnd); - return linkFn(scope, element, attrs, controllers, transcludeFn); - }; - } - - /** - * Once the directives have been collected, their compile functions are executed. This method - * is responsible for inlining directive templates as well as terminating the application - * of the directives if the terminal directive has been reached. - * - * @param {Array} directives Array of collected directives to execute their compile function. - * this needs to be pre-sorted by priority order. - * @param {Node} compileNode The raw DOM node to apply the compile functions to - * @param {Object} templateAttrs The shared attribute function - * @param {function(angular.Scope, cloneAttachFn=)} transcludeFn A linking function, where the - * scope argument is auto-generated to the new - * child of the transcluded parent scope. - * @param {JQLite} jqCollection If we are working on the root of the compile tree then this - * argument has the root jqLite array so that we can replace nodes - * on it. - * @param {Object=} originalReplaceDirective An optional directive that will be ignored when - * compiling the transclusion. - * @param {Array.} preLinkFns - * @param {Array.} postLinkFns - * @param {Object} previousCompileContext Context used for previous compilation of the current - * node - * @returns {Function} linkFn - */ - function applyDirectivesToNode(directives, compileNode, templateAttrs, transcludeFn, - jqCollection, originalReplaceDirective, preLinkFns, postLinkFns, - previousCompileContext) { - previousCompileContext = previousCompileContext || {}; - - var terminalPriority = -Number.MAX_VALUE, - newScopeDirective = previousCompileContext.newScopeDirective, - controllerDirectives = previousCompileContext.controllerDirectives, - newIsolateScopeDirective = previousCompileContext.newIsolateScopeDirective, - templateDirective = previousCompileContext.templateDirective, - nonTlbTranscludeDirective = previousCompileContext.nonTlbTranscludeDirective, - hasTranscludeDirective = false, - hasTemplate = false, - hasElementTranscludeDirective = previousCompileContext.hasElementTranscludeDirective, - $compileNode = templateAttrs.$$element = jqLite(compileNode), - directive, - directiveName, - $template, - replaceDirective = originalReplaceDirective, - childTranscludeFn = transcludeFn, - linkFn, - directiveValue; - - // executes all directives on the current element - for (var i = 0, ii = directives.length; i < ii; i++) { - directive = directives[i]; - var attrStart = directive.$$start; - var attrEnd = directive.$$end; - - // collect multiblock sections - if (attrStart) { - $compileNode = groupScan(compileNode, attrStart, attrEnd); - } - $template = undefined; - - if (terminalPriority > directive.priority) { - break; // prevent further processing of directives - } - - if (directiveValue = directive.scope) { - - // skip the check for directives with async templates, we'll check the derived sync - // directive when the template arrives - if (!directive.templateUrl) { - if (isObject(directiveValue)) { - // This directive is trying to add an isolated scope. - // Check that there is no scope of any kind already - assertNoDuplicate('new/isolated scope', newIsolateScopeDirective || newScopeDirective, - directive, $compileNode); - newIsolateScopeDirective = directive; - } else { - // This directive is trying to add a child scope. - // Check that there is no isolated scope already - assertNoDuplicate('new/isolated scope', newIsolateScopeDirective, directive, - $compileNode); - } - } - - newScopeDirective = newScopeDirective || directive; - } - - directiveName = directive.name; - - if (!directive.templateUrl && directive.controller) { - directiveValue = directive.controller; - controllerDirectives = controllerDirectives || createMap(); - assertNoDuplicate("'" + directiveName + "' controller", - controllerDirectives[directiveName], directive, $compileNode); - controllerDirectives[directiveName] = directive; - } - - if (directiveValue = directive.transclude) { - hasTranscludeDirective = true; - - // Special case ngIf and ngRepeat so that we don't complain about duplicate transclusion. - // This option should only be used by directives that know how to safely handle element transclusion, - // where the transcluded nodes are added or replaced after linking. - if (!directive.$$tlb) { - assertNoDuplicate('transclusion', nonTlbTranscludeDirective, directive, $compileNode); - nonTlbTranscludeDirective = directive; - } - - if (directiveValue == 'element') { - hasElementTranscludeDirective = true; - terminalPriority = directive.priority; - $template = $compileNode; - $compileNode = templateAttrs.$$element = - jqLite(document.createComment(' ' + directiveName + ': ' + - templateAttrs[directiveName] + ' ')); - compileNode = $compileNode[0]; - replaceWith(jqCollection, sliceArgs($template), compileNode); - - childTranscludeFn = compile($template, transcludeFn, terminalPriority, - replaceDirective && replaceDirective.name, { - // Don't pass in: - // - controllerDirectives - otherwise we'll create duplicates controllers - // - newIsolateScopeDirective or templateDirective - combining templates with - // element transclusion doesn't make sense. - // - // We need only nonTlbTranscludeDirective so that we prevent putting transclusion - // on the same element more than once. - nonTlbTranscludeDirective: nonTlbTranscludeDirective - }); - } else { - $template = jqLite(jqLiteClone(compileNode)).contents(); - $compileNode.empty(); // clear contents - childTranscludeFn = compile($template, transcludeFn); - } - } - - if (directive.template) { - hasTemplate = true; - assertNoDuplicate('template', templateDirective, directive, $compileNode); - templateDirective = directive; - - directiveValue = (isFunction(directive.template)) - ? directive.template($compileNode, templateAttrs) - : directive.template; - - directiveValue = denormalizeTemplate(directiveValue); - - if (directive.replace) { - replaceDirective = directive; - if (jqLiteIsTextNode(directiveValue)) { - $template = []; - } else { - $template = removeComments(wrapTemplate(directive.templateNamespace, trim(directiveValue))); - } - compileNode = $template[0]; - - if ($template.length != 1 || compileNode.nodeType !== NODE_TYPE_ELEMENT) { - throw $compileMinErr('tplrt', - "Template for directive '{0}' must have exactly one root element. {1}", - directiveName, ''); - } - - replaceWith(jqCollection, $compileNode, compileNode); - - var newTemplateAttrs = {$attr: {}}; - - // combine directives from the original node and from the template: - // - take the array of directives for this element - // - split it into two parts, those that already applied (processed) and those that weren't (unprocessed) - // - collect directives from the template and sort them by priority - // - combine directives as: processed + template + unprocessed - var templateDirectives = collectDirectives(compileNode, [], newTemplateAttrs); - var unprocessedDirectives = directives.splice(i + 1, directives.length - (i + 1)); - - if (newIsolateScopeDirective) { - markDirectivesAsIsolate(templateDirectives); - } - directives = directives.concat(templateDirectives).concat(unprocessedDirectives); - mergeTemplateAttributes(templateAttrs, newTemplateAttrs); - - ii = directives.length; - } else { - $compileNode.html(directiveValue); - } - } - - if (directive.templateUrl) { - hasTemplate = true; - assertNoDuplicate('template', templateDirective, directive, $compileNode); - templateDirective = directive; - - if (directive.replace) { - replaceDirective = directive; - } - - nodeLinkFn = compileTemplateUrl(directives.splice(i, directives.length - i), $compileNode, - templateAttrs, jqCollection, hasTranscludeDirective && childTranscludeFn, preLinkFns, postLinkFns, { - controllerDirectives: controllerDirectives, - newScopeDirective: (newScopeDirective !== directive) && newScopeDirective, - newIsolateScopeDirective: newIsolateScopeDirective, - templateDirective: templateDirective, - nonTlbTranscludeDirective: nonTlbTranscludeDirective - }); - ii = directives.length; - } else if (directive.compile) { - try { - linkFn = directive.compile($compileNode, templateAttrs, childTranscludeFn); - if (isFunction(linkFn)) { - addLinkFns(null, linkFn, attrStart, attrEnd); - } else if (linkFn) { - addLinkFns(linkFn.pre, linkFn.post, attrStart, attrEnd); - } - } catch (e) { - $exceptionHandler(e, startingTag($compileNode)); - } - } - - if (directive.terminal) { - nodeLinkFn.terminal = true; - terminalPriority = Math.max(terminalPriority, directive.priority); - } - - } - - nodeLinkFn.scope = newScopeDirective && newScopeDirective.scope === true; - nodeLinkFn.transcludeOnThisElement = hasTranscludeDirective; - nodeLinkFn.templateOnThisElement = hasTemplate; - nodeLinkFn.transclude = childTranscludeFn; - - previousCompileContext.hasElementTranscludeDirective = hasElementTranscludeDirective; - - // might be normal or delayed nodeLinkFn depending on if templateUrl is present - return nodeLinkFn; - - //////////////////// - - function addLinkFns(pre, post, attrStart, attrEnd) { - if (pre) { - if (attrStart) pre = groupElementsLinkFnWrapper(pre, attrStart, attrEnd); - pre.require = directive.require; - pre.directiveName = directiveName; - if (newIsolateScopeDirective === directive || directive.$$isolateScope) { - pre = cloneAndAnnotateFn(pre, {isolateScope: true}); - } - preLinkFns.push(pre); - } - if (post) { - if (attrStart) post = groupElementsLinkFnWrapper(post, attrStart, attrEnd); - post.require = directive.require; - post.directiveName = directiveName; - if (newIsolateScopeDirective === directive || directive.$$isolateScope) { - post = cloneAndAnnotateFn(post, {isolateScope: true}); - } - postLinkFns.push(post); - } - } - - - function getControllers(directiveName, require, $element, elementControllers) { - var value; - - if (isString(require)) { - var match = require.match(REQUIRE_PREFIX_REGEXP); - var name = require.substring(match[0].length); - var inheritType = match[1] || match[3]; - var optional = match[2] === '?'; - - //If only parents then start at the parent element - if (inheritType === '^^') { - $element = $element.parent(); - //Otherwise attempt getting the controller from elementControllers in case - //the element is transcluded (and has no data) and to avoid .data if possible - } else { - value = elementControllers && elementControllers[name]; - value = value && value.instance; - } - - if (!value) { - var dataName = '$' + name + 'Controller'; - value = inheritType ? $element.inheritedData(dataName) : $element.data(dataName); - } - - if (!value && !optional) { - throw $compileMinErr('ctreq', - "Controller '{0}', required by directive '{1}', can't be found!", - name, directiveName); - } - } else if (isArray(require)) { - value = []; - for (var i = 0, ii = require.length; i < ii; i++) { - value[i] = getControllers(directiveName, require[i], $element, elementControllers); - } - } - - return value || null; - } - - function setupControllers($element, attrs, transcludeFn, controllerDirectives, isolateScope, scope) { - var elementControllers = createMap(); - for (var controllerKey in controllerDirectives) { - var directive = controllerDirectives[controllerKey]; - var locals = { - $scope: directive === newIsolateScopeDirective || directive.$$isolateScope ? isolateScope : scope, - $element: $element, - $attrs: attrs, - $transclude: transcludeFn - }; - - var controller = directive.controller; - if (controller == '@') { - controller = attrs[directive.name]; - } - - var controllerInstance = $controller(controller, locals, true, directive.controllerAs); - - // For directives with element transclusion the element is a comment, - // but jQuery .data doesn't support attaching data to comment nodes as it's hard to - // clean up (http://bugs.jquery.com/ticket/8335). - // Instead, we save the controllers for the element in a local hash and attach to .data - // later, once we have the actual element. - elementControllers[directive.name] = controllerInstance; - if (!hasElementTranscludeDirective) { - $element.data('$' + directive.name + 'Controller', controllerInstance.instance); - } - } - return elementControllers; - } - - function nodeLinkFn(childLinkFn, scope, linkNode, $rootElement, boundTranscludeFn, - thisLinkFn) { - var i, ii, linkFn, controller, isolateScope, elementControllers, transcludeFn, $element, - attrs; - - if (compileNode === linkNode) { - attrs = templateAttrs; - $element = templateAttrs.$$element; - } else { - $element = jqLite(linkNode); - attrs = new Attributes($element, templateAttrs); - } - - if (newIsolateScopeDirective) { - isolateScope = scope.$new(true); - } - - if (boundTranscludeFn) { - // track `boundTranscludeFn` so it can be unwrapped if `transcludeFn` - // is later passed as `parentBoundTranscludeFn` to `publicLinkFn` - transcludeFn = controllersBoundTransclude; - transcludeFn.$$boundTransclude = boundTranscludeFn; - } - - if (controllerDirectives) { - elementControllers = setupControllers($element, attrs, transcludeFn, controllerDirectives, isolateScope, scope); - } - - if (newIsolateScopeDirective) { - // Initialize isolate scope bindings for new isolate scope directive. - compile.$$addScopeInfo($element, isolateScope, true, !(templateDirective && (templateDirective === newIsolateScopeDirective || - templateDirective === newIsolateScopeDirective.$$originalDirective))); - compile.$$addScopeClass($element, true); - isolateScope.$$isolateBindings = - newIsolateScopeDirective.$$isolateBindings; - initializeDirectiveBindings(scope, attrs, isolateScope, - isolateScope.$$isolateBindings, - newIsolateScopeDirective, isolateScope); - } - if (elementControllers) { - // Initialize bindToController bindings for new/isolate scopes - var scopeDirective = newIsolateScopeDirective || newScopeDirective; - var bindings; - var controllerForBindings; - if (scopeDirective && elementControllers[scopeDirective.name]) { - bindings = scopeDirective.$$bindings.bindToController; - controller = elementControllers[scopeDirective.name]; - - if (controller && controller.identifier && bindings) { - controllerForBindings = controller; - thisLinkFn.$$destroyBindings = - initializeDirectiveBindings(scope, attrs, controller.instance, - bindings, scopeDirective); - } - } - for (i in elementControllers) { - controller = elementControllers[i]; - var controllerResult = controller(); - - if (controllerResult !== controller.instance) { - // If the controller constructor has a return value, overwrite the instance - // from setupControllers and update the element data - controller.instance = controllerResult; - $element.data('$' + i + 'Controller', controllerResult); - if (controller === controllerForBindings) { - // Remove and re-install bindToController bindings - thisLinkFn.$$destroyBindings(); - thisLinkFn.$$destroyBindings = - initializeDirectiveBindings(scope, attrs, controllerResult, bindings, scopeDirective); - } - } - } - } - - // PRELINKING - for (i = 0, ii = preLinkFns.length; i < ii; i++) { - linkFn = preLinkFns[i]; - invokeLinkFn(linkFn, - linkFn.isolateScope ? isolateScope : scope, - $element, - attrs, - linkFn.require && getControllers(linkFn.directiveName, linkFn.require, $element, elementControllers), - transcludeFn - ); - } - - // RECURSION - // We only pass the isolate scope, if the isolate directive has a template, - // otherwise the child elements do not belong to the isolate directive. - var scopeToChild = scope; - if (newIsolateScopeDirective && (newIsolateScopeDirective.template || newIsolateScopeDirective.templateUrl === null)) { - scopeToChild = isolateScope; - } - childLinkFn && childLinkFn(scopeToChild, linkNode.childNodes, undefined, boundTranscludeFn); - - // POSTLINKING - for (i = postLinkFns.length - 1; i >= 0; i--) { - linkFn = postLinkFns[i]; - invokeLinkFn(linkFn, - linkFn.isolateScope ? isolateScope : scope, - $element, - attrs, - linkFn.require && getControllers(linkFn.directiveName, linkFn.require, $element, elementControllers), - transcludeFn - ); - } - - // This is the function that is injected as `$transclude`. - // Note: all arguments are optional! - function controllersBoundTransclude(scope, cloneAttachFn, futureParentElement) { - var transcludeControllers; - - // No scope passed in: - if (!isScope(scope)) { - futureParentElement = cloneAttachFn; - cloneAttachFn = scope; - scope = undefined; - } - - if (hasElementTranscludeDirective) { - transcludeControllers = elementControllers; - } - if (!futureParentElement) { - futureParentElement = hasElementTranscludeDirective ? $element.parent() : $element; - } - return boundTranscludeFn(scope, cloneAttachFn, transcludeControllers, futureParentElement, scopeToChild); - } - } - } - - function markDirectivesAsIsolate(directives) { - // mark all directives as needing isolate scope. - for (var j = 0, jj = directives.length; j < jj; j++) { - directives[j] = inherit(directives[j], {$$isolateScope: true}); - } - } - - /** - * looks up the directive and decorates it with exception handling and proper parameters. We - * call this the boundDirective. - * - * @param {string} name name of the directive to look up. - * @param {string} location The directive must be found in specific format. - * String containing any of theses characters: - * - * * `E`: element name - * * `A': attribute - * * `C`: class - * * `M`: comment - * @returns {boolean} true if directive was added. - */ - function addDirective(tDirectives, name, location, maxPriority, ignoreDirective, startAttrName, - endAttrName) { - if (name === ignoreDirective) return null; - var match = null; - if (hasDirectives.hasOwnProperty(name)) { - for (var directive, directives = $injector.get(name + Suffix), - i = 0, ii = directives.length; i < ii; i++) { - try { - directive = directives[i]; - if ((maxPriority === undefined || maxPriority > directive.priority) && - directive.restrict.indexOf(location) != -1) { - if (startAttrName) { - directive = inherit(directive, {$$start: startAttrName, $$end: endAttrName}); - } - tDirectives.push(directive); - match = directive; - } - } catch (e) { $exceptionHandler(e); } - } - } - return match; - } - - - /** - * looks up the directive and returns true if it is a multi-element directive, - * and therefore requires DOM nodes between -start and -end markers to be grouped - * together. - * - * @param {string} name name of the directive to look up. - * @returns true if directive was registered as multi-element. - */ - function directiveIsMultiElement(name) { - if (hasDirectives.hasOwnProperty(name)) { - for (var directive, directives = $injector.get(name + Suffix), - i = 0, ii = directives.length; i < ii; i++) { - directive = directives[i]; - if (directive.multiElement) { - return true; - } - } - } - return false; - } - - /** - * When the element is replaced with HTML template then the new attributes - * on the template need to be merged with the existing attributes in the DOM. - * The desired effect is to have both of the attributes present. - * - * @param {object} dst destination attributes (original DOM) - * @param {object} src source attributes (from the directive template) - */ - function mergeTemplateAttributes(dst, src) { - var srcAttr = src.$attr, - dstAttr = dst.$attr, - $element = dst.$$element; - - // reapply the old attributes to the new element - forEach(dst, function(value, key) { - if (key.charAt(0) != '$') { - if (src[key] && src[key] !== value) { - value += (key === 'style' ? ';' : ' ') + src[key]; - } - dst.$set(key, value, true, srcAttr[key]); - } - }); - - // copy the new attributes on the old attrs object - forEach(src, function(value, key) { - if (key == 'class') { - safeAddClass($element, value); - dst['class'] = (dst['class'] ? dst['class'] + ' ' : '') + value; - } else if (key == 'style') { - $element.attr('style', $element.attr('style') + ';' + value); - dst['style'] = (dst['style'] ? dst['style'] + ';' : '') + value; - // `dst` will never contain hasOwnProperty as DOM parser won't let it. - // You will get an "InvalidCharacterError: DOM Exception 5" error if you - // have an attribute like "has-own-property" or "data-has-own-property", etc. - } else if (key.charAt(0) != '$' && !dst.hasOwnProperty(key)) { - dst[key] = value; - dstAttr[key] = srcAttr[key]; - } - }); - } - - - function compileTemplateUrl(directives, $compileNode, tAttrs, - $rootElement, childTranscludeFn, preLinkFns, postLinkFns, previousCompileContext) { - var linkQueue = [], - afterTemplateNodeLinkFn, - afterTemplateChildLinkFn, - beforeTemplateCompileNode = $compileNode[0], - origAsyncDirective = directives.shift(), - derivedSyncDirective = inherit(origAsyncDirective, { - templateUrl: null, transclude: null, replace: null, $$originalDirective: origAsyncDirective - }), - templateUrl = (isFunction(origAsyncDirective.templateUrl)) - ? origAsyncDirective.templateUrl($compileNode, tAttrs) - : origAsyncDirective.templateUrl, - templateNamespace = origAsyncDirective.templateNamespace; - - $compileNode.empty(); - - $templateRequest(templateUrl) - .then(function(content) { - var compileNode, tempTemplateAttrs, $template, childBoundTranscludeFn; - - content = denormalizeTemplate(content); - - if (origAsyncDirective.replace) { - if (jqLiteIsTextNode(content)) { - $template = []; - } else { - $template = removeComments(wrapTemplate(templateNamespace, trim(content))); - } - compileNode = $template[0]; - - if ($template.length != 1 || compileNode.nodeType !== NODE_TYPE_ELEMENT) { - throw $compileMinErr('tplrt', - "Template for directive '{0}' must have exactly one root element. {1}", - origAsyncDirective.name, templateUrl); - } - - tempTemplateAttrs = {$attr: {}}; - replaceWith($rootElement, $compileNode, compileNode); - var templateDirectives = collectDirectives(compileNode, [], tempTemplateAttrs); - - if (isObject(origAsyncDirective.scope)) { - markDirectivesAsIsolate(templateDirectives); - } - directives = templateDirectives.concat(directives); - mergeTemplateAttributes(tAttrs, tempTemplateAttrs); - } else { - compileNode = beforeTemplateCompileNode; - $compileNode.html(content); - } - - directives.unshift(derivedSyncDirective); - - afterTemplateNodeLinkFn = applyDirectivesToNode(directives, compileNode, tAttrs, - childTranscludeFn, $compileNode, origAsyncDirective, preLinkFns, postLinkFns, - previousCompileContext); - forEach($rootElement, function(node, i) { - if (node == compileNode) { - $rootElement[i] = $compileNode[0]; - } - }); - afterTemplateChildLinkFn = compileNodes($compileNode[0].childNodes, childTranscludeFn); - - while (linkQueue.length) { - var scope = linkQueue.shift(), - beforeTemplateLinkNode = linkQueue.shift(), - linkRootElement = linkQueue.shift(), - boundTranscludeFn = linkQueue.shift(), - linkNode = $compileNode[0]; - - if (scope.$$destroyed) continue; - - if (beforeTemplateLinkNode !== beforeTemplateCompileNode) { - var oldClasses = beforeTemplateLinkNode.className; - - if (!(previousCompileContext.hasElementTranscludeDirective && - origAsyncDirective.replace)) { - // it was cloned therefore we have to clone as well. - linkNode = jqLiteClone(compileNode); - } - replaceWith(linkRootElement, jqLite(beforeTemplateLinkNode), linkNode); - - // Copy in CSS classes from original node - safeAddClass(jqLite(linkNode), oldClasses); - } - if (afterTemplateNodeLinkFn.transcludeOnThisElement) { - childBoundTranscludeFn = createBoundTranscludeFn(scope, afterTemplateNodeLinkFn.transclude, boundTranscludeFn); - } else { - childBoundTranscludeFn = boundTranscludeFn; - } - afterTemplateNodeLinkFn(afterTemplateChildLinkFn, scope, linkNode, $rootElement, - childBoundTranscludeFn, afterTemplateNodeLinkFn); - } - linkQueue = null; - }); - - return function delayedNodeLinkFn(ignoreChildLinkFn, scope, node, rootElement, boundTranscludeFn) { - var childBoundTranscludeFn = boundTranscludeFn; - if (scope.$$destroyed) return; - if (linkQueue) { - linkQueue.push(scope, - node, - rootElement, - childBoundTranscludeFn); - } else { - if (afterTemplateNodeLinkFn.transcludeOnThisElement) { - childBoundTranscludeFn = createBoundTranscludeFn(scope, afterTemplateNodeLinkFn.transclude, boundTranscludeFn); - } - afterTemplateNodeLinkFn(afterTemplateChildLinkFn, scope, node, rootElement, childBoundTranscludeFn, - afterTemplateNodeLinkFn); - } - }; - } - - - /** - * Sorting function for bound directives. - */ - function byPriority(a, b) { - var diff = b.priority - a.priority; - if (diff !== 0) return diff; - if (a.name !== b.name) return (a.name < b.name) ? -1 : 1; - return a.index - b.index; - } - - function assertNoDuplicate(what, previousDirective, directive, element) { - - function wrapModuleNameIfDefined(moduleName) { - return moduleName ? - (' (module: ' + moduleName + ')') : - ''; - } - - if (previousDirective) { - throw $compileMinErr('multidir', 'Multiple directives [{0}{1}, {2}{3}] asking for {4} on: {5}', - previousDirective.name, wrapModuleNameIfDefined(previousDirective.$$moduleName), - directive.name, wrapModuleNameIfDefined(directive.$$moduleName), what, startingTag(element)); - } - } - - - function addTextInterpolateDirective(directives, text) { - var interpolateFn = $interpolate(text, true); - if (interpolateFn) { - directives.push({ - priority: 0, - compile: function textInterpolateCompileFn(templateNode) { - var templateNodeParent = templateNode.parent(), - hasCompileParent = !!templateNodeParent.length; - - // When transcluding a template that has bindings in the root - // we don't have a parent and thus need to add the class during linking fn. - if (hasCompileParent) compile.$$addBindingClass(templateNodeParent); - - return function textInterpolateLinkFn(scope, node) { - var parent = node.parent(); - if (!hasCompileParent) compile.$$addBindingClass(parent); - compile.$$addBindingInfo(parent, interpolateFn.expressions); - scope.$watch(interpolateFn, function interpolateFnWatchAction(value) { - node[0].nodeValue = value; - }); - }; - } - }); - } - } - - - function wrapTemplate(type, template) { - type = lowercase(type || 'html'); - switch (type) { - case 'svg': - case 'math': - var wrapper = document.createElement('div'); - wrapper.innerHTML = '<' + type + '>' + template + ''; - return wrapper.childNodes[0].childNodes; - default: - return template; - } - } - - - function getTrustedContext(node, attrNormalizedName) { - if (attrNormalizedName == "srcdoc") { - return $sce.HTML; - } - var tag = nodeName_(node); - // maction[xlink:href] can source SVG. It's not limited to . - if (attrNormalizedName == "xlinkHref" || - (tag == "form" && attrNormalizedName == "action") || - (tag != "img" && (attrNormalizedName == "src" || - attrNormalizedName == "ngSrc"))) { - return $sce.RESOURCE_URL; - } - } - - - function addAttrInterpolateDirective(node, directives, value, name, allOrNothing) { - var trustedContext = getTrustedContext(node, name); - allOrNothing = ALL_OR_NOTHING_ATTRS[name] || allOrNothing; - - var interpolateFn = $interpolate(value, true, trustedContext, allOrNothing); - - // no interpolation found -> ignore - if (!interpolateFn) return; - - - if (name === "multiple" && nodeName_(node) === "select") { - throw $compileMinErr("selmulti", - "Binding to the 'multiple' attribute is not supported. Element: {0}", - startingTag(node)); - } - - directives.push({ - priority: 100, - compile: function() { - return { - pre: function attrInterpolatePreLinkFn(scope, element, attr) { - var $$observers = (attr.$$observers || (attr.$$observers = {})); - - if (EVENT_HANDLER_ATTR_REGEXP.test(name)) { - throw $compileMinErr('nodomevents', - "Interpolations for HTML DOM event attributes are disallowed. Please use the " + - "ng- versions (such as ng-click instead of onclick) instead."); - } - - // If the attribute has changed since last $interpolate()ed - var newValue = attr[name]; - if (newValue !== value) { - // we need to interpolate again since the attribute value has been updated - // (e.g. by another directive's compile function) - // ensure unset/empty values make interpolateFn falsy - interpolateFn = newValue && $interpolate(newValue, true, trustedContext, allOrNothing); - value = newValue; - } - - // if attribute was updated so that there is no interpolation going on we don't want to - // register any observers - if (!interpolateFn) return; - - // initialize attr object so that it's ready in case we need the value for isolate - // scope initialization, otherwise the value would not be available from isolate - // directive's linking fn during linking phase - attr[name] = interpolateFn(scope); - - ($$observers[name] || ($$observers[name] = [])).$$inter = true; - (attr.$$observers && attr.$$observers[name].$$scope || scope). - $watch(interpolateFn, function interpolateFnWatchAction(newValue, oldValue) { - //special case for class attribute addition + removal - //so that class changes can tap into the animation - //hooks provided by the $animate service. Be sure to - //skip animations when the first digest occurs (when - //both the new and the old values are the same) since - //the CSS classes are the non-interpolated values - if (name === 'class' && newValue != oldValue) { - attr.$updateClass(newValue, oldValue); - } else { - attr.$set(name, newValue); - } - }); - } - }; - } - }); - } - - - /** - * This is a special jqLite.replaceWith, which can replace items which - * have no parents, provided that the containing jqLite collection is provided. - * - * @param {JqLite=} $rootElement The root of the compile tree. Used so that we can replace nodes - * in the root of the tree. - * @param {JqLite} elementsToRemove The jqLite element which we are going to replace. We keep - * the shell, but replace its DOM node reference. - * @param {Node} newNode The new DOM node. - */ - function replaceWith($rootElement, elementsToRemove, newNode) { - var firstElementToRemove = elementsToRemove[0], - removeCount = elementsToRemove.length, - parent = firstElementToRemove.parentNode, - i, ii; - - if ($rootElement) { - for (i = 0, ii = $rootElement.length; i < ii; i++) { - if ($rootElement[i] == firstElementToRemove) { - $rootElement[i++] = newNode; - for (var j = i, j2 = j + removeCount - 1, - jj = $rootElement.length; - j < jj; j++, j2++) { - if (j2 < jj) { - $rootElement[j] = $rootElement[j2]; - } else { - delete $rootElement[j]; - } - } - $rootElement.length -= removeCount - 1; - - // If the replaced element is also the jQuery .context then replace it - // .context is a deprecated jQuery api, so we should set it only when jQuery set it - // http://api.jquery.com/context/ - if ($rootElement.context === firstElementToRemove) { - $rootElement.context = newNode; - } - break; - } - } - } - - if (parent) { - parent.replaceChild(newNode, firstElementToRemove); - } - - // TODO(perf): what's this document fragment for? is it needed? can we at least reuse it? - var fragment = document.createDocumentFragment(); - fragment.appendChild(firstElementToRemove); - - if (jqLite.hasData(firstElementToRemove)) { - // Copy over user data (that includes Angular's $scope etc.). Don't copy private - // data here because there's no public interface in jQuery to do that and copying over - // event listeners (which is the main use of private data) wouldn't work anyway. - jqLite(newNode).data(jqLite(firstElementToRemove).data()); - - // Remove data of the replaced element. We cannot just call .remove() - // on the element it since that would deallocate scope that is needed - // for the new node. Instead, remove the data "manually". - if (!jQuery) { - delete jqLite.cache[firstElementToRemove[jqLite.expando]]; - } else { - // jQuery 2.x doesn't expose the data storage. Use jQuery.cleanData to clean up after - // the replaced element. The cleanData version monkey-patched by Angular would cause - // the scope to be trashed and we do need the very same scope to work with the new - // element. However, we cannot just cache the non-patched version and use it here as - // that would break if another library patches the method after Angular does (one - // example is jQuery UI). Instead, set a flag indicating scope destroying should be - // skipped this one time. - skipDestroyOnNextJQueryCleanData = true; - jQuery.cleanData([firstElementToRemove]); - } - } - - for (var k = 1, kk = elementsToRemove.length; k < kk; k++) { - var element = elementsToRemove[k]; - jqLite(element).remove(); // must do this way to clean up expando - fragment.appendChild(element); - delete elementsToRemove[k]; - } - - elementsToRemove[0] = newNode; - elementsToRemove.length = 1; - } - - - function cloneAndAnnotateFn(fn, annotation) { - return extend(function() { return fn.apply(null, arguments); }, fn, annotation); - } - - - function invokeLinkFn(linkFn, scope, $element, attrs, controllers, transcludeFn) { - try { - linkFn(scope, $element, attrs, controllers, transcludeFn); - } catch (e) { - $exceptionHandler(e, startingTag($element)); - } - } - - - // Set up $watches for isolate scope and controller bindings. This process - // only occurs for isolate scopes and new scopes with controllerAs. - function initializeDirectiveBindings(scope, attrs, destination, bindings, - directive, newScope) { - var onNewScopeDestroyed; - forEach(bindings, function(definition, scopeName) { - var attrName = definition.attrName, - optional = definition.optional, - mode = definition.mode, // @, =, or & - lastValue, - parentGet, parentSet, compare; - - switch (mode) { - - case '@': - if (!optional && !hasOwnProperty.call(attrs, attrName)) { - destination[scopeName] = attrs[attrName] = void 0; - } - attrs.$observe(attrName, function(value) { - if (isString(value)) { - destination[scopeName] = value; - } - }); - attrs.$$observers[attrName].$$scope = scope; - if (isString(attrs[attrName])) { - // If the attribute has been provided then we trigger an interpolation to ensure - // the value is there for use in the link fn - destination[scopeName] = $interpolate(attrs[attrName])(scope); - } - break; - - case '=': - if (!hasOwnProperty.call(attrs, attrName)) { - if (optional) break; - attrs[attrName] = void 0; - } - if (optional && !attrs[attrName]) break; - - parentGet = $parse(attrs[attrName]); - if (parentGet.literal) { - compare = equals; - } else { - compare = function(a, b) { return a === b || (a !== a && b !== b); }; - } - parentSet = parentGet.assign || function() { - // reset the change, or we will throw this exception on every $digest - lastValue = destination[scopeName] = parentGet(scope); - throw $compileMinErr('nonassign', - "Expression '{0}' used with directive '{1}' is non-assignable!", - attrs[attrName], directive.name); - }; - lastValue = destination[scopeName] = parentGet(scope); - var parentValueWatch = function parentValueWatch(parentValue) { - if (!compare(parentValue, destination[scopeName])) { - // we are out of sync and need to copy - if (!compare(parentValue, lastValue)) { - // parent changed and it has precedence - destination[scopeName] = parentValue; - } else { - // if the parent can be assigned then do so - parentSet(scope, parentValue = destination[scopeName]); - } - } - return lastValue = parentValue; - }; - parentValueWatch.$stateful = true; - var unwatch; - if (definition.collection) { - unwatch = scope.$watchCollection(attrs[attrName], parentValueWatch); - } else { - unwatch = scope.$watch($parse(attrs[attrName], parentValueWatch), null, parentGet.literal); - } - onNewScopeDestroyed = (onNewScopeDestroyed || []); - onNewScopeDestroyed.push(unwatch); - break; - - case '&': - // Don't assign Object.prototype method to scope - parentGet = attrs.hasOwnProperty(attrName) ? $parse(attrs[attrName]) : noop; - - // Don't assign noop to destination if expression is not valid - if (parentGet === noop && optional) break; - - destination[scopeName] = function(locals) { - return parentGet(scope, locals); - }; - break; - } - }); - var destroyBindings = onNewScopeDestroyed ? function destroyBindings() { - for (var i = 0, ii = onNewScopeDestroyed.length; i < ii; ++i) { - onNewScopeDestroyed[i](); - } - } : noop; - if (newScope && destroyBindings !== noop) { - newScope.$on('$destroy', destroyBindings); - return noop; - } - return destroyBindings; - } - }]; -} - -var PREFIX_REGEXP = /^((?:x|data)[\:\-_])/i; -/** - * Converts all accepted directives format into proper directive name. - * @param name Name to normalize - */ -function directiveNormalize(name) { - return camelCase(name.replace(PREFIX_REGEXP, '')); -} - -/** - * @ngdoc type - * @name $compile.directive.Attributes - * - * @description - * A shared object between directive compile / linking functions which contains normalized DOM - * element attributes. The values reflect current binding state `{{ }}`. The normalization is - * needed since all of these are treated as equivalent in Angular: - * - * ``` - * - * ``` - */ - -/** - * @ngdoc property - * @name $compile.directive.Attributes#$attr - * - * @description - * A map of DOM element attribute names to the normalized name. This is - * needed to do reverse lookup from normalized name back to actual name. - */ - - -/** - * @ngdoc method - * @name $compile.directive.Attributes#$set - * @kind function - * - * @description - * Set DOM element attribute value. - * - * - * @param {string} name Normalized element attribute name of the property to modify. The name is - * reverse-translated using the {@link ng.$compile.directive.Attributes#$attr $attr} - * property to the original name. - * @param {string} value Value to set the attribute to. The value can be an interpolated string. - */ - - - -/** - * Closure compiler type information - */ - -function nodesetLinkingFn( - /* angular.Scope */ scope, - /* NodeList */ nodeList, - /* Element */ rootElement, - /* function(Function) */ boundTranscludeFn -) {} - -function directiveLinkingFn( - /* nodesetLinkingFn */ nodesetLinkingFn, - /* angular.Scope */ scope, - /* Node */ node, - /* Element */ rootElement, - /* function(Function) */ boundTranscludeFn -) {} - -function tokenDifference(str1, str2) { - var values = '', - tokens1 = str1.split(/\s+/), - tokens2 = str2.split(/\s+/); - - outer: - for (var i = 0; i < tokens1.length; i++) { - var token = tokens1[i]; - for (var j = 0; j < tokens2.length; j++) { - if (token == tokens2[j]) continue outer; - } - values += (values.length > 0 ? ' ' : '') + token; - } - return values; -} - -function removeComments(jqNodes) { - jqNodes = jqLite(jqNodes); - var i = jqNodes.length; - - if (i <= 1) { - return jqNodes; - } - - while (i--) { - var node = jqNodes[i]; - if (node.nodeType === NODE_TYPE_COMMENT) { - splice.call(jqNodes, i, 1); - } - } - return jqNodes; -} - -var $controllerMinErr = minErr('$controller'); - - -var CNTRL_REG = /^(\S+)(\s+as\s+(\w+))?$/; -function identifierForController(controller, ident) { - if (ident && isString(ident)) return ident; - if (isString(controller)) { - var match = CNTRL_REG.exec(controller); - if (match) return match[3]; - } -} - - -/** - * @ngdoc provider - * @name $controllerProvider - * @description - * The {@link ng.$controller $controller service} is used by Angular to create new - * controllers. - * - * This provider allows controller registration via the - * {@link ng.$controllerProvider#register register} method. - */ -function $ControllerProvider() { - var controllers = {}, - globals = false; - - /** - * @ngdoc method - * @name $controllerProvider#register - * @param {string|Object} name Controller name, or an object map of controllers where the keys are - * the names and the values are the constructors. - * @param {Function|Array} constructor Controller constructor fn (optionally decorated with DI - * annotations in the array notation). - */ - this.register = function(name, constructor) { - assertNotHasOwnProperty(name, 'controller'); - if (isObject(name)) { - extend(controllers, name); - } else { - controllers[name] = constructor; - } - }; - - /** - * @ngdoc method - * @name $controllerProvider#allowGlobals - * @description If called, allows `$controller` to find controller constructors on `window` - */ - this.allowGlobals = function() { - globals = true; - }; - - - this.$get = ['$injector', '$window', function($injector, $window) { - - /** - * @ngdoc service - * @name $controller - * @requires $injector - * - * @param {Function|string} constructor If called with a function then it's considered to be the - * controller constructor function. Otherwise it's considered to be a string which is used - * to retrieve the controller constructor using the following steps: - * - * * check if a controller with given name is registered via `$controllerProvider` - * * check if evaluating the string on the current scope returns a constructor - * * if $controllerProvider#allowGlobals, check `window[constructor]` on the global - * `window` object (not recommended) - * - * The string can use the `controller as property` syntax, where the controller instance is published - * as the specified property on the `scope`; the `scope` must be injected into `locals` param for this - * to work correctly. - * - * @param {Object} locals Injection locals for Controller. - * @return {Object} Instance of given controller. - * - * @description - * `$controller` service is responsible for instantiating controllers. - * - * It's just a simple call to {@link auto.$injector $injector}, but extracted into - * a service, so that one can override this service with [BC version](https://gist.github.com/1649788). - */ - return function(expression, locals, later, ident) { - // PRIVATE API: - // param `later` --- indicates that the controller's constructor is invoked at a later time. - // If true, $controller will allocate the object with the correct - // prototype chain, but will not invoke the controller until a returned - // callback is invoked. - // param `ident` --- An optional label which overrides the label parsed from the controller - // expression, if any. - var instance, match, constructor, identifier; - later = later === true; - if (ident && isString(ident)) { - identifier = ident; - } - - if (isString(expression)) { - match = expression.match(CNTRL_REG); - if (!match) { - throw $controllerMinErr('ctrlfmt', - "Badly formed controller string '{0}'. " + - "Must match `__name__ as __id__` or `__name__`.", expression); - } - constructor = match[1], - identifier = identifier || match[3]; - expression = controllers.hasOwnProperty(constructor) - ? controllers[constructor] - : getter(locals.$scope, constructor, true) || - (globals ? getter($window, constructor, true) : undefined); - - assertArgFn(expression, constructor, true); - } - - if (later) { - // Instantiate controller later: - // This machinery is used to create an instance of the object before calling the - // controller's constructor itself. - // - // This allows properties to be added to the controller before the constructor is - // invoked. Primarily, this is used for isolate scope bindings in $compile. - // - // This feature is not intended for use by applications, and is thus not documented - // publicly. - // Object creation: http://jsperf.com/create-constructor/2 - var controllerPrototype = (isArray(expression) ? - expression[expression.length - 1] : expression).prototype; - instance = Object.create(controllerPrototype || null); - - if (identifier) { - addIdentifier(locals, identifier, instance, constructor || expression.name); - } - - var instantiate; - return instantiate = extend(function() { - var result = $injector.invoke(expression, instance, locals, constructor); - if (result !== instance && (isObject(result) || isFunction(result))) { - instance = result; - if (identifier) { - // If result changed, re-assign controllerAs value to scope. - addIdentifier(locals, identifier, instance, constructor || expression.name); - } - } - return instance; - }, { - instance: instance, - identifier: identifier - }); - } - - instance = $injector.instantiate(expression, locals, constructor); - - if (identifier) { - addIdentifier(locals, identifier, instance, constructor || expression.name); - } - - return instance; - }; - - function addIdentifier(locals, identifier, instance, name) { - if (!(locals && isObject(locals.$scope))) { - throw minErr('$controller')('noscp', - "Cannot export controller '{0}' as '{1}'! No $scope object provided via `locals`.", - name, identifier); - } - - locals.$scope[identifier] = instance; - } - }]; -} - -/** - * @ngdoc service - * @name $document - * @requires $window - * - * @description - * A {@link angular.element jQuery or jqLite} wrapper for the browser's `window.document` object. - * - * @example - - -
    -

    $document title:

    -

    window.document title:

    -
    -
    - - angular.module('documentExample', []) - .controller('ExampleController', ['$scope', '$document', function($scope, $document) { - $scope.title = $document[0].title; - $scope.windowTitle = angular.element(window.document)[0].title; - }]); - -
    - */ -function $DocumentProvider() { - this.$get = ['$window', function(window) { - return jqLite(window.document); - }]; -} - -/** - * @ngdoc service - * @name $exceptionHandler - * @requires ng.$log - * - * @description - * Any uncaught exception in angular expressions is delegated to this service. - * The default implementation simply delegates to `$log.error` which logs it into - * the browser console. - * - * In unit tests, if `angular-mocks.js` is loaded, this service is overridden by - * {@link ngMock.$exceptionHandler mock $exceptionHandler} which aids in testing. - * - * ## Example: - * - * ```js - * angular.module('exceptionOverride', []).factory('$exceptionHandler', function() { - * return function(exception, cause) { - * exception.message += ' (caused by "' + cause + '")'; - * throw exception; - * }; - * }); - * ``` - * - * This example will override the normal action of `$exceptionHandler`, to make angular - * exceptions fail hard when they happen, instead of just logging to the console. - * - *
    - * Note, that code executed in event-listeners (even those registered using jqLite's `on`/`bind` - * methods) does not delegate exceptions to the {@link ng.$exceptionHandler $exceptionHandler} - * (unless executed during a digest). - * - * If you wish, you can manually delegate exceptions, e.g. - * `try { ... } catch(e) { $exceptionHandler(e); }` - * - * @param {Error} exception Exception associated with the error. - * @param {string=} cause optional information about the context in which - * the error was thrown. - * - */ -function $ExceptionHandlerProvider() { - this.$get = ['$log', function($log) { - return function(exception, cause) { - $log.error.apply($log, arguments); - }; - }]; -} - -var $$ForceReflowProvider = function() { - this.$get = ['$document', function($document) { - return function(domNode) { - //the line below will force the browser to perform a repaint so - //that all the animated elements within the animation frame will - //be properly updated and drawn on screen. This is required to - //ensure that the preparation animation is properly flushed so that - //the active state picks up from there. DO NOT REMOVE THIS LINE. - //DO NOT OPTIMIZE THIS LINE. THE MINIFIER WILL REMOVE IT OTHERWISE WHICH - //WILL RESULT IN AN UNPREDICTABLE BUG THAT IS VERY HARD TO TRACK DOWN AND - //WILL TAKE YEARS AWAY FROM YOUR LIFE. - if (domNode) { - if (!domNode.nodeType && domNode instanceof jqLite) { - domNode = domNode[0]; - } - } else { - domNode = $document[0].body; - } - return domNode.offsetWidth + 1; - }; - }]; -}; - -var APPLICATION_JSON = 'application/json'; -var CONTENT_TYPE_APPLICATION_JSON = {'Content-Type': APPLICATION_JSON + ';charset=utf-8'}; -var JSON_START = /^\[|^\{(?!\{)/; -var JSON_ENDS = { - '[': /]$/, - '{': /}$/ -}; -var JSON_PROTECTION_PREFIX = /^\)\]\}',?\n/; -var $httpMinErr = minErr('$http'); -var $httpMinErrLegacyFn = function(method) { - return function() { - throw $httpMinErr('legacy', 'The method `{0}` on the promise returned from `$http` has been disabled.', method); - }; -}; - -function serializeValue(v) { - if (isObject(v)) { - return isDate(v) ? v.toISOString() : toJson(v); - } - return v; -} - - -function $HttpParamSerializerProvider() { - /** - * @ngdoc service - * @name $httpParamSerializer - * @description - * - * Default {@link $http `$http`} params serializer that converts objects to strings - * according to the following rules: - * - * * `{'foo': 'bar'}` results in `foo=bar` - * * `{'foo': Date.now()}` results in `foo=2015-04-01T09%3A50%3A49.262Z` (`toISOString()` and encoded representation of a Date object) - * * `{'foo': ['bar', 'baz']}` results in `foo=bar&foo=baz` (repeated key for each array element) - * * `{'foo': {'bar':'baz'}}` results in `foo=%7B%22bar%22%3A%22baz%22%7D"` (stringified and encoded representation of an object) - * - * Note that serializer will sort the request parameters alphabetically. - * */ - - this.$get = function() { - return function ngParamSerializer(params) { - if (!params) return ''; - var parts = []; - forEachSorted(params, function(value, key) { - if (value === null || isUndefined(value)) return; - if (isArray(value)) { - forEach(value, function(v, k) { - parts.push(encodeUriQuery(key) + '=' + encodeUriQuery(serializeValue(v))); - }); - } else { - parts.push(encodeUriQuery(key) + '=' + encodeUriQuery(serializeValue(value))); - } - }); - - return parts.join('&'); - }; - }; -} - -function $HttpParamSerializerJQLikeProvider() { - /** - * @ngdoc service - * @name $httpParamSerializerJQLike - * @description - * - * Alternative {@link $http `$http`} params serializer that follows - * jQuery's [`param()`](http://api.jquery.com/jquery.param/) method logic. - * The serializer will also sort the params alphabetically. - * - * To use it for serializing `$http` request parameters, set it as the `paramSerializer` property: - * - * ```js - * $http({ - * url: myUrl, - * method: 'GET', - * params: myParams, - * paramSerializer: '$httpParamSerializerJQLike' - * }); - * ``` - * - * It is also possible to set it as the default `paramSerializer` in the - * {@link $httpProvider#defaults `$httpProvider`}. - * - * Additionally, you can inject the serializer and use it explicitly, for example to serialize - * form data for submission: - * - * ```js - * .controller(function($http, $httpParamSerializerJQLike) { - * //... - * - * $http({ - * url: myUrl, - * method: 'POST', - * data: $httpParamSerializerJQLike(myData), - * headers: { - * 'Content-Type': 'application/x-www-form-urlencoded' - * } - * }); - * - * }); - * ``` - * - * */ - this.$get = function() { - return function jQueryLikeParamSerializer(params) { - if (!params) return ''; - var parts = []; - serialize(params, '', true); - return parts.join('&'); - - function serialize(toSerialize, prefix, topLevel) { - if (toSerialize === null || isUndefined(toSerialize)) return; - if (isArray(toSerialize)) { - forEach(toSerialize, function(value, index) { - serialize(value, prefix + '[' + (isObject(value) ? index : '') + ']'); - }); - } else if (isObject(toSerialize) && !isDate(toSerialize)) { - forEachSorted(toSerialize, function(value, key) { - serialize(value, prefix + - (topLevel ? '' : '[') + - key + - (topLevel ? '' : ']')); - }); - } else { - parts.push(encodeUriQuery(prefix) + '=' + encodeUriQuery(serializeValue(toSerialize))); - } - } - }; - }; -} - -function defaultHttpResponseTransform(data, headers) { - if (isString(data)) { - // Strip json vulnerability protection prefix and trim whitespace - var tempData = data.replace(JSON_PROTECTION_PREFIX, '').trim(); - - if (tempData) { - var contentType = headers('Content-Type'); - if ((contentType && (contentType.indexOf(APPLICATION_JSON) === 0)) || isJsonLike(tempData)) { - data = fromJson(tempData); - } - } - } - - return data; -} - -function isJsonLike(str) { - var jsonStart = str.match(JSON_START); - return jsonStart && JSON_ENDS[jsonStart[0]].test(str); -} - -/** - * Parse headers into key value object - * - * @param {string} headers Raw headers as a string - * @returns {Object} Parsed headers as key value object - */ -function parseHeaders(headers) { - var parsed = createMap(), i; - - function fillInParsed(key, val) { - if (key) { - parsed[key] = parsed[key] ? parsed[key] + ', ' + val : val; - } - } - - if (isString(headers)) { - forEach(headers.split('\n'), function(line) { - i = line.indexOf(':'); - fillInParsed(lowercase(trim(line.substr(0, i))), trim(line.substr(i + 1))); - }); - } else if (isObject(headers)) { - forEach(headers, function(headerVal, headerKey) { - fillInParsed(lowercase(headerKey), trim(headerVal)); - }); - } - - return parsed; -} - - -/** - * Returns a function that provides access to parsed headers. - * - * Headers are lazy parsed when first requested. - * @see parseHeaders - * - * @param {(string|Object)} headers Headers to provide access to. - * @returns {function(string=)} Returns a getter function which if called with: - * - * - if called with single an argument returns a single header value or null - * - if called with no arguments returns an object containing all headers. - */ -function headersGetter(headers) { - var headersObj; - - return function(name) { - if (!headersObj) headersObj = parseHeaders(headers); - - if (name) { - var value = headersObj[lowercase(name)]; - if (value === void 0) { - value = null; - } - return value; - } - - return headersObj; - }; -} - - -/** - * Chain all given functions - * - * This function is used for both request and response transforming - * - * @param {*} data Data to transform. - * @param {function(string=)} headers HTTP headers getter fn. - * @param {number} status HTTP status code of the response. - * @param {(Function|Array.)} fns Function or an array of functions. - * @returns {*} Transformed data. - */ -function transformData(data, headers, status, fns) { - if (isFunction(fns)) { - return fns(data, headers, status); - } - - forEach(fns, function(fn) { - data = fn(data, headers, status); - }); - - return data; -} - - -function isSuccess(status) { - return 200 <= status && status < 300; -} - - -/** - * @ngdoc provider - * @name $httpProvider - * @description - * Use `$httpProvider` to change the default behavior of the {@link ng.$http $http} service. - * */ -function $HttpProvider() { - /** - * @ngdoc property - * @name $httpProvider#defaults - * @description - * - * Object containing default values for all {@link ng.$http $http} requests. - * - * - **`defaults.cache`** - {Object} - an object built with {@link ng.$cacheFactory `$cacheFactory`} - * that will provide the cache for all requests who set their `cache` property to `true`. - * If you set the `defaults.cache = false` then only requests that specify their own custom - * cache object will be cached. See {@link $http#caching $http Caching} for more information. - * - * - **`defaults.xsrfCookieName`** - {string} - Name of cookie containing the XSRF token. - * Defaults value is `'XSRF-TOKEN'`. - * - * - **`defaults.xsrfHeaderName`** - {string} - Name of HTTP header to populate with the - * XSRF token. Defaults value is `'X-XSRF-TOKEN'`. - * - * - **`defaults.headers`** - {Object} - Default headers for all $http requests. - * Refer to {@link ng.$http#setting-http-headers $http} for documentation on - * setting default headers. - * - **`defaults.headers.common`** - * - **`defaults.headers.post`** - * - **`defaults.headers.put`** - * - **`defaults.headers.patch`** - * - * - * - **`defaults.paramSerializer`** - `{string|function(Object):string}` - A function - * used to the prepare string representation of request parameters (specified as an object). - * If specified as string, it is interpreted as a function registered with the {@link auto.$injector $injector}. - * Defaults to {@link ng.$httpParamSerializer $httpParamSerializer}. - * - **/ - var defaults = this.defaults = { - // transform incoming response data - transformResponse: [defaultHttpResponseTransform], - - // transform outgoing request data - transformRequest: [function(d) { - return isObject(d) && !isFile(d) && !isBlob(d) && !isFormData(d) ? toJson(d) : d; - }], - - // default headers - headers: { - common: { - 'Accept': 'application/json, text/plain, */*' - }, - post: shallowCopy(CONTENT_TYPE_APPLICATION_JSON), - put: shallowCopy(CONTENT_TYPE_APPLICATION_JSON), - patch: shallowCopy(CONTENT_TYPE_APPLICATION_JSON) - }, - - xsrfCookieName: 'XSRF-TOKEN', - xsrfHeaderName: 'X-XSRF-TOKEN', - - paramSerializer: '$httpParamSerializer' - }; - - var useApplyAsync = false; - /** - * @ngdoc method - * @name $httpProvider#useApplyAsync - * @description - * - * Configure $http service to combine processing of multiple http responses received at around - * the same time via {@link ng.$rootScope.Scope#$applyAsync $rootScope.$applyAsync}. This can result in - * significant performance improvement for bigger applications that make many HTTP requests - * concurrently (common during application bootstrap). - * - * Defaults to false. If no value is specified, returns the current configured value. - * - * @param {boolean=} value If true, when requests are loaded, they will schedule a deferred - * "apply" on the next tick, giving time for subsequent requests in a roughly ~10ms window - * to load and share the same digest cycle. - * - * @returns {boolean|Object} If a value is specified, returns the $httpProvider for chaining. - * otherwise, returns the current configured value. - **/ - this.useApplyAsync = function(value) { - if (isDefined(value)) { - useApplyAsync = !!value; - return this; - } - return useApplyAsync; - }; - - var useLegacyPromise = true; - /** - * @ngdoc method - * @name $httpProvider#useLegacyPromiseExtensions - * @description - * - * Configure `$http` service to return promises without the shorthand methods `success` and `error`. - * This should be used to make sure that applications work without these methods. - * - * Defaults to false. If no value is specified, returns the current configured value. - * - * @param {boolean=} value If true, `$http` will return a normal promise without the `success` and `error` methods. - * - * @returns {boolean|Object} If a value is specified, returns the $httpProvider for chaining. - * otherwise, returns the current configured value. - **/ - this.useLegacyPromiseExtensions = function(value) { - if (isDefined(value)) { - useLegacyPromise = !!value; - return this; - } - return useLegacyPromise; - }; - - /** - * @ngdoc property - * @name $httpProvider#interceptors - * @description - * - * Array containing service factories for all synchronous or asynchronous {@link ng.$http $http} - * pre-processing of request or postprocessing of responses. - * - * These service factories are ordered by request, i.e. they are applied in the same order as the - * array, on request, but reverse order, on response. - * - * {@link ng.$http#interceptors Interceptors detailed info} - **/ - var interceptorFactories = this.interceptors = []; - - this.$get = ['$httpBackend', '$$cookieReader', '$cacheFactory', '$rootScope', '$q', '$injector', - function($httpBackend, $$cookieReader, $cacheFactory, $rootScope, $q, $injector) { - - var defaultCache = $cacheFactory('$http'); - - /** - * Make sure that default param serializer is exposed as a function - */ - defaults.paramSerializer = isString(defaults.paramSerializer) ? - $injector.get(defaults.paramSerializer) : defaults.paramSerializer; - - /** - * Interceptors stored in reverse order. Inner interceptors before outer interceptors. - * The reversal is needed so that we can build up the interception chain around the - * server request. - */ - var reversedInterceptors = []; - - forEach(interceptorFactories, function(interceptorFactory) { - reversedInterceptors.unshift(isString(interceptorFactory) - ? $injector.get(interceptorFactory) : $injector.invoke(interceptorFactory)); - }); - - /** - * @ngdoc service - * @kind function - * @name $http - * @requires ng.$httpBackend - * @requires $cacheFactory - * @requires $rootScope - * @requires $q - * @requires $injector - * - * @description - * The `$http` service is a core Angular service that facilitates communication with the remote - * HTTP servers via the browser's [XMLHttpRequest](https://developer.mozilla.org/en/xmlhttprequest) - * object or via [JSONP](http://en.wikipedia.org/wiki/JSONP). - * - * For unit testing applications that use `$http` service, see - * {@link ngMock.$httpBackend $httpBackend mock}. - * - * For a higher level of abstraction, please check out the {@link ngResource.$resource - * $resource} service. - * - * The $http API is based on the {@link ng.$q deferred/promise APIs} exposed by - * the $q service. While for simple usage patterns this doesn't matter much, for advanced usage - * it is important to familiarize yourself with these APIs and the guarantees they provide. - * - * - * ## General usage - * The `$http` service is a function which takes a single argument — a configuration object — - * that is used to generate an HTTP request and returns a {@link ng.$q promise}. - * - * ```js - * // Simple GET request example : - * $http.get('/someUrl'). - * then(function(response) { - * // this callback will be called asynchronously - * // when the response is available - * }, function(response) { - * // called asynchronously if an error occurs - * // or server returns response with an error status. - * }); - * ``` - * - * ```js - * // Simple POST request example (passing data) : - * $http.post('/someUrl', {msg:'hello word!'}). - * then(function(response) { - * // this callback will be called asynchronously - * // when the response is available - * }, function(response) { - * // called asynchronously if an error occurs - * // or server returns response with an error status. - * }); - * ``` - * - * The response object has these properties: - * - * - **data** – `{string|Object}` – The response body transformed with the transform - * functions. - * - **status** – `{number}` – HTTP status code of the response. - * - **headers** – `{function([headerName])}` – Header getter function. - * - **config** – `{Object}` – The configuration object that was used to generate the request. - * - **statusText** – `{string}` – HTTP status text of the response. - * - * A response status code between 200 and 299 is considered a success status and - * will result in the success callback being called. Note that if the response is a redirect, - * XMLHttpRequest will transparently follow it, meaning that the error callback will not be - * called for such responses. - * - * ## Writing Unit Tests that use $http - * When unit testing (using {@link ngMock ngMock}), it is necessary to call - * {@link ngMock.$httpBackend#flush $httpBackend.flush()} to flush each pending - * request using trained responses. - * - * ``` - * $httpBackend.expectGET(...); - * $http.get(...); - * $httpBackend.flush(); - * ``` - * - * ## Shortcut methods - * - * Shortcut methods are also available. All shortcut methods require passing in the URL, and - * request data must be passed in for POST/PUT requests. - * - * ```js - * $http.get('/someUrl').then(successCallback); - * $http.post('/someUrl', data).then(successCallback); - * ``` - * - * Complete list of shortcut methods: - * - * - {@link ng.$http#get $http.get} - * - {@link ng.$http#head $http.head} - * - {@link ng.$http#post $http.post} - * - {@link ng.$http#put $http.put} - * - {@link ng.$http#delete $http.delete} - * - {@link ng.$http#jsonp $http.jsonp} - * - {@link ng.$http#patch $http.patch} - * - * - * ## Deprecation Notice - *
    - * The `$http` legacy promise methods `success` and `error` have been deprecated. - * Use the standard `then` method instead. - * If {@link $httpProvider#useLegacyPromiseExtensions `$httpProvider.useLegacyPromiseExtensions`} is set to - * `false` then these methods will throw {@link $http:legacy `$http/legacy`} error. - *
    - * - * ## Setting HTTP Headers - * - * The $http service will automatically add certain HTTP headers to all requests. These defaults - * can be fully configured by accessing the `$httpProvider.defaults.headers` configuration - * object, which currently contains this default configuration: - * - * - `$httpProvider.defaults.headers.common` (headers that are common for all requests): - * - `Accept: application/json, text/plain, * / *` - * - `$httpProvider.defaults.headers.post`: (header defaults for POST requests) - * - `Content-Type: application/json` - * - `$httpProvider.defaults.headers.put` (header defaults for PUT requests) - * - `Content-Type: application/json` - * - * To add or overwrite these defaults, simply add or remove a property from these configuration - * objects. To add headers for an HTTP method other than POST or PUT, simply add a new object - * with the lowercased HTTP method name as the key, e.g. - * `$httpProvider.defaults.headers.get = { 'My-Header' : 'value' }`. - * - * The defaults can also be set at runtime via the `$http.defaults` object in the same - * fashion. For example: - * - * ``` - * module.run(function($http) { - * $http.defaults.headers.common.Authorization = 'Basic YmVlcDpib29w' - * }); - * ``` - * - * In addition, you can supply a `headers` property in the config object passed when - * calling `$http(config)`, which overrides the defaults without changing them globally. - * - * To explicitly remove a header automatically added via $httpProvider.defaults.headers on a per request basis, - * Use the `headers` property, setting the desired header to `undefined`. For example: - * - * ```js - * var req = { - * method: 'POST', - * url: 'http://example.com', - * headers: { - * 'Content-Type': undefined - * }, - * data: { test: 'test' } - * } - * - * $http(req).then(function(){...}, function(){...}); - * ``` - * - * ## Transforming Requests and Responses - * - * Both requests and responses can be transformed using transformation functions: `transformRequest` - * and `transformResponse`. These properties can be a single function that returns - * the transformed value (`function(data, headersGetter, status)`) or an array of such transformation functions, - * which allows you to `push` or `unshift` a new transformation function into the transformation chain. - * - * ### Default Transformations - * - * The `$httpProvider` provider and `$http` service expose `defaults.transformRequest` and - * `defaults.transformResponse` properties. If a request does not provide its own transformations - * then these will be applied. - * - * You can augment or replace the default transformations by modifying these properties by adding to or - * replacing the array. - * - * Angular provides the following default transformations: - * - * Request transformations (`$httpProvider.defaults.transformRequest` and `$http.defaults.transformRequest`): - * - * - If the `data` property of the request configuration object contains an object, serialize it - * into JSON format. - * - * Response transformations (`$httpProvider.defaults.transformResponse` and `$http.defaults.transformResponse`): - * - * - If XSRF prefix is detected, strip it (see Security Considerations section below). - * - If JSON response is detected, deserialize it using a JSON parser. - * - * - * ### Overriding the Default Transformations Per Request - * - * If you wish override the request/response transformations only for a single request then provide - * `transformRequest` and/or `transformResponse` properties on the configuration object passed - * into `$http`. - * - * Note that if you provide these properties on the config object the default transformations will be - * overwritten. If you wish to augment the default transformations then you must include them in your - * local transformation array. - * - * The following code demonstrates adding a new response transformation to be run after the default response - * transformations have been run. - * - * ```js - * function appendTransform(defaults, transform) { - * - * // We can't guarantee that the default transformation is an array - * defaults = angular.isArray(defaults) ? defaults : [defaults]; - * - * // Append the new transformation to the defaults - * return defaults.concat(transform); - * } - * - * $http({ - * url: '...', - * method: 'GET', - * transformResponse: appendTransform($http.defaults.transformResponse, function(value) { - * return doTransform(value); - * }) - * }); - * ``` - * - * - * ## Caching - * - * To enable caching, set the request configuration `cache` property to `true` (to use default - * cache) or to a custom cache object (built with {@link ng.$cacheFactory `$cacheFactory`}). - * When the cache is enabled, `$http` stores the response from the server in the specified - * cache. The next time the same request is made, the response is served from the cache without - * sending a request to the server. - * - * Note that even if the response is served from cache, delivery of the data is asynchronous in - * the same way that real requests are. - * - * If there are multiple GET requests for the same URL that should be cached using the same - * cache, but the cache is not populated yet, only one request to the server will be made and - * the remaining requests will be fulfilled using the response from the first request. - * - * You can change the default cache to a new object (built with - * {@link ng.$cacheFactory `$cacheFactory`}) by updating the - * {@link ng.$http#defaults `$http.defaults.cache`} property. All requests who set - * their `cache` property to `true` will now use this cache object. - * - * If you set the default cache to `false` then only requests that specify their own custom - * cache object will be cached. - * - * ## Interceptors - * - * Before you start creating interceptors, be sure to understand the - * {@link ng.$q $q and deferred/promise APIs}. - * - * For purposes of global error handling, authentication, or any kind of synchronous or - * asynchronous pre-processing of request or postprocessing of responses, it is desirable to be - * able to intercept requests before they are handed to the server and - * responses before they are handed over to the application code that - * initiated these requests. The interceptors leverage the {@link ng.$q - * promise APIs} to fulfill this need for both synchronous and asynchronous pre-processing. - * - * The interceptors are service factories that are registered with the `$httpProvider` by - * adding them to the `$httpProvider.interceptors` array. The factory is called and - * injected with dependencies (if specified) and returns the interceptor. - * - * There are two kinds of interceptors (and two kinds of rejection interceptors): - * - * * `request`: interceptors get called with a http `config` object. The function is free to - * modify the `config` object or create a new one. The function needs to return the `config` - * object directly, or a promise containing the `config` or a new `config` object. - * * `requestError`: interceptor gets called when a previous interceptor threw an error or - * resolved with a rejection. - * * `response`: interceptors get called with http `response` object. The function is free to - * modify the `response` object or create a new one. The function needs to return the `response` - * object directly, or as a promise containing the `response` or a new `response` object. - * * `responseError`: interceptor gets called when a previous interceptor threw an error or - * resolved with a rejection. - * - * - * ```js - * // register the interceptor as a service - * $provide.factory('myHttpInterceptor', function($q, dependency1, dependency2) { - * return { - * // optional method - * 'request': function(config) { - * // do something on success - * return config; - * }, - * - * // optional method - * 'requestError': function(rejection) { - * // do something on error - * if (canRecover(rejection)) { - * return responseOrNewPromise - * } - * return $q.reject(rejection); - * }, - * - * - * - * // optional method - * 'response': function(response) { - * // do something on success - * return response; - * }, - * - * // optional method - * 'responseError': function(rejection) { - * // do something on error - * if (canRecover(rejection)) { - * return responseOrNewPromise - * } - * return $q.reject(rejection); - * } - * }; - * }); - * - * $httpProvider.interceptors.push('myHttpInterceptor'); - * - * - * // alternatively, register the interceptor via an anonymous factory - * $httpProvider.interceptors.push(function($q, dependency1, dependency2) { - * return { - * 'request': function(config) { - * // same as above - * }, - * - * 'response': function(response) { - * // same as above - * } - * }; - * }); - * ``` - * - * ## Security Considerations - * - * When designing web applications, consider security threats from: - * - * - [JSON vulnerability](http://haacked.com/archive/2008/11/20/anatomy-of-a-subtle-json-vulnerability.aspx) - * - [XSRF](http://en.wikipedia.org/wiki/Cross-site_request_forgery) - * - * Both server and the client must cooperate in order to eliminate these threats. Angular comes - * pre-configured with strategies that address these issues, but for this to work backend server - * cooperation is required. - * - * ### JSON Vulnerability Protection - * - * A [JSON vulnerability](http://haacked.com/archive/2008/11/20/anatomy-of-a-subtle-json-vulnerability.aspx) - * allows third party website to turn your JSON resource URL into - * [JSONP](http://en.wikipedia.org/wiki/JSONP) request under some conditions. To - * counter this your server can prefix all JSON requests with following string `")]}',\n"`. - * Angular will automatically strip the prefix before processing it as JSON. - * - * For example if your server needs to return: - * ```js - * ['one','two'] - * ``` - * - * which is vulnerable to attack, your server can return: - * ```js - * )]}', - * ['one','two'] - * ``` - * - * Angular will strip the prefix, before processing the JSON. - * - * - * ### Cross Site Request Forgery (XSRF) Protection - * - * [XSRF](http://en.wikipedia.org/wiki/Cross-site_request_forgery) is a technique by which - * an unauthorized site can gain your user's private data. Angular provides a mechanism - * to counter XSRF. When performing XHR requests, the $http service reads a token from a cookie - * (by default, `XSRF-TOKEN`) and sets it as an HTTP header (`X-XSRF-TOKEN`). Since only - * JavaScript that runs on your domain could read the cookie, your server can be assured that - * the XHR came from JavaScript running on your domain. The header will not be set for - * cross-domain requests. - * - * To take advantage of this, your server needs to set a token in a JavaScript readable session - * cookie called `XSRF-TOKEN` on the first HTTP GET request. On subsequent XHR requests the - * server can verify that the cookie matches `X-XSRF-TOKEN` HTTP header, and therefore be sure - * that only JavaScript running on your domain could have sent the request. The token must be - * unique for each user and must be verifiable by the server (to prevent the JavaScript from - * making up its own tokens). We recommend that the token is a digest of your site's - * authentication cookie with a [salt](https://en.wikipedia.org/wiki/Salt_(cryptography)) - * for added security. - * - * The name of the headers can be specified using the xsrfHeaderName and xsrfCookieName - * properties of either $httpProvider.defaults at config-time, $http.defaults at run-time, - * or the per-request config object. - * - * In order to prevent collisions in environments where multiple Angular apps share the - * same domain or subdomain, we recommend that each application uses unique cookie name. - * - * @param {object} config Object describing the request to be made and how it should be - * processed. The object has following properties: - * - * - **method** – `{string}` – HTTP method (e.g. 'GET', 'POST', etc) - * - **url** – `{string}` – Absolute or relative URL of the resource that is being requested. - * - **params** – `{Object.}` – Map of strings or objects which will be serialized - * with the `paramSerializer` and appended as GET parameters. - * - **data** – `{string|Object}` – Data to be sent as the request message data. - * - **headers** – `{Object}` – Map of strings or functions which return strings representing - * HTTP headers to send to the server. If the return value of a function is null, the - * header will not be sent. Functions accept a config object as an argument. - * - **xsrfHeaderName** – `{string}` – Name of HTTP header to populate with the XSRF token. - * - **xsrfCookieName** – `{string}` – Name of cookie containing the XSRF token. - * - **transformRequest** – - * `{function(data, headersGetter)|Array.}` – - * transform function or an array of such functions. The transform function takes the http - * request body and headers and returns its transformed (typically serialized) version. - * See {@link ng.$http#overriding-the-default-transformations-per-request - * Overriding the Default Transformations} - * - **transformResponse** – - * `{function(data, headersGetter, status)|Array.}` – - * transform function or an array of such functions. The transform function takes the http - * response body, headers and status and returns its transformed (typically deserialized) version. - * See {@link ng.$http#overriding-the-default-transformations-per-request - * Overriding the Default TransformationjqLiks} - * - **paramSerializer** - `{string|function(Object):string}` - A function used to - * prepare the string representation of request parameters (specified as an object). - * If specified as string, it is interpreted as function registered with the - * {@link $injector $injector}, which means you can create your own serializer - * by registering it as a {@link auto.$provide#service service}. - * The default serializer is the {@link $httpParamSerializer $httpParamSerializer}; - * alternatively, you can use the {@link $httpParamSerializerJQLike $httpParamSerializerJQLike} - * - **cache** – `{boolean|Cache}` – If true, a default $http cache will be used to cache the - * GET request, otherwise if a cache instance built with - * {@link ng.$cacheFactory $cacheFactory}, this cache will be used for - * caching. - * - **timeout** – `{number|Promise}` – timeout in milliseconds, or {@link ng.$q promise} - * that should abort the request when resolved. - * - **withCredentials** - `{boolean}` - whether to set the `withCredentials` flag on the - * XHR object. See [requests with credentials](https://developer.mozilla.org/docs/Web/HTTP/Access_control_CORS#Requests_with_credentials) - * for more information. - * - **responseType** - `{string}` - see - * [XMLHttpRequest.responseType](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest#xmlhttprequest-responsetype). - * - * @returns {HttpPromise} Returns a {@link ng.$q `Promise}` that will be resolved to a response object - * when the request succeeds or fails. - * - * - * @property {Array.} pendingRequests Array of config objects for currently pending - * requests. This is primarily meant to be used for debugging purposes. - * - * - * @example - - -
    - - -
    - - - -
    http status code: {{status}}
    -
    http response data: {{data}}
    -
    -
    - - angular.module('httpExample', []) - .controller('FetchController', ['$scope', '$http', '$templateCache', - function($scope, $http, $templateCache) { - $scope.method = 'GET'; - $scope.url = 'http-hello.html'; - - $scope.fetch = function() { - $scope.code = null; - $scope.response = null; - - $http({method: $scope.method, url: $scope.url, cache: $templateCache}). - then(function(response) { - $scope.status = response.status; - $scope.data = response.data; - }, function(response) { - $scope.data = response.data || "Request failed"; - $scope.status = response.status; - }); - }; - - $scope.updateModel = function(method, url) { - $scope.method = method; - $scope.url = url; - }; - }]); - - - Hello, $http! - - - var status = element(by.binding('status')); - var data = element(by.binding('data')); - var fetchBtn = element(by.id('fetchbtn')); - var sampleGetBtn = element(by.id('samplegetbtn')); - var sampleJsonpBtn = element(by.id('samplejsonpbtn')); - var invalidJsonpBtn = element(by.id('invalidjsonpbtn')); - - it('should make an xhr GET request', function() { - sampleGetBtn.click(); - fetchBtn.click(); - expect(status.getText()).toMatch('200'); - expect(data.getText()).toMatch(/Hello, \$http!/); - }); - -// Commented out due to flakes. See https://github.com/angular/angular.js/issues/9185 -// it('should make a JSONP request to angularjs.org', function() { -// sampleJsonpBtn.click(); -// fetchBtn.click(); -// expect(status.getText()).toMatch('200'); -// expect(data.getText()).toMatch(/Super Hero!/); -// }); - - it('should make JSONP request to invalid URL and invoke the error handler', - function() { - invalidJsonpBtn.click(); - fetchBtn.click(); - expect(status.getText()).toMatch('0'); - expect(data.getText()).toMatch('Request failed'); - }); - -
    - */ - function $http(requestConfig) { - - if (!angular.isObject(requestConfig)) { - throw minErr('$http')('badreq', 'Http request configuration must be an object. Received: {0}', requestConfig); - } - - var config = extend({ - method: 'get', - transformRequest: defaults.transformRequest, - transformResponse: defaults.transformResponse, - paramSerializer: defaults.paramSerializer - }, requestConfig); - - config.headers = mergeHeaders(requestConfig); - config.method = uppercase(config.method); - config.paramSerializer = isString(config.paramSerializer) ? - $injector.get(config.paramSerializer) : config.paramSerializer; - - var serverRequest = function(config) { - var headers = config.headers; - var reqData = transformData(config.data, headersGetter(headers), undefined, config.transformRequest); - - // strip content-type if data is undefined - if (isUndefined(reqData)) { - forEach(headers, function(value, header) { - if (lowercase(header) === 'content-type') { - delete headers[header]; - } - }); - } - - if (isUndefined(config.withCredentials) && !isUndefined(defaults.withCredentials)) { - config.withCredentials = defaults.withCredentials; - } - - // send request - return sendReq(config, reqData).then(transformResponse, transformResponse); - }; - - var chain = [serverRequest, undefined]; - var promise = $q.when(config); - - // apply interceptors - forEach(reversedInterceptors, function(interceptor) { - if (interceptor.request || interceptor.requestError) { - chain.unshift(interceptor.request, interceptor.requestError); - } - if (interceptor.response || interceptor.responseError) { - chain.push(interceptor.response, interceptor.responseError); - } - }); - - while (chain.length) { - var thenFn = chain.shift(); - var rejectFn = chain.shift(); - - promise = promise.then(thenFn, rejectFn); - } - - if (useLegacyPromise) { - promise.success = function(fn) { - assertArgFn(fn, 'fn'); - - promise.then(function(response) { - fn(response.data, response.status, response.headers, config); - }); - return promise; - }; - - promise.error = function(fn) { - assertArgFn(fn, 'fn'); - - promise.then(null, function(response) { - fn(response.data, response.status, response.headers, config); - }); - return promise; - }; - } else { - promise.success = $httpMinErrLegacyFn('success'); - promise.error = $httpMinErrLegacyFn('error'); - } - - return promise; - - function transformResponse(response) { - // make a copy since the response must be cacheable - var resp = extend({}, response); - if (!response.data) { - resp.data = response.data; - } else { - resp.data = transformData(response.data, response.headers, response.status, config.transformResponse); - } - return (isSuccess(response.status)) - ? resp - : $q.reject(resp); - } - - function executeHeaderFns(headers, config) { - var headerContent, processedHeaders = {}; - - forEach(headers, function(headerFn, header) { - if (isFunction(headerFn)) { - headerContent = headerFn(config); - if (headerContent != null) { - processedHeaders[header] = headerContent; - } - } else { - processedHeaders[header] = headerFn; - } - }); - - return processedHeaders; - } - - function mergeHeaders(config) { - var defHeaders = defaults.headers, - reqHeaders = extend({}, config.headers), - defHeaderName, lowercaseDefHeaderName, reqHeaderName; - - defHeaders = extend({}, defHeaders.common, defHeaders[lowercase(config.method)]); - - // using for-in instead of forEach to avoid unecessary iteration after header has been found - defaultHeadersIteration: - for (defHeaderName in defHeaders) { - lowercaseDefHeaderName = lowercase(defHeaderName); - - for (reqHeaderName in reqHeaders) { - if (lowercase(reqHeaderName) === lowercaseDefHeaderName) { - continue defaultHeadersIteration; - } - } - - reqHeaders[defHeaderName] = defHeaders[defHeaderName]; - } - - // execute if header value is a function for merged headers - return executeHeaderFns(reqHeaders, shallowCopy(config)); - } - } - - $http.pendingRequests = []; - - /** - * @ngdoc method - * @name $http#get - * - * @description - * Shortcut method to perform `GET` request. - * - * @param {string} url Relative or absolute URL specifying the destination of the request - * @param {Object=} config Optional configuration object - * @returns {HttpPromise} Future object - */ - - /** - * @ngdoc method - * @name $http#delete - * - * @description - * Shortcut method to perform `DELETE` request. - * - * @param {string} url Relative or absolute URL specifying the destination of the request - * @param {Object=} config Optional configuration object - * @returns {HttpPromise} Future object - */ - - /** - * @ngdoc method - * @name $http#head - * - * @description - * Shortcut method to perform `HEAD` request. - * - * @param {string} url Relative or absolute URL specifying the destination of the request - * @param {Object=} config Optional configuration object - * @returns {HttpPromise} Future object - */ - - /** - * @ngdoc method - * @name $http#jsonp - * - * @description - * Shortcut method to perform `JSONP` request. - * - * @param {string} url Relative or absolute URL specifying the destination of the request. - * The name of the callback should be the string `JSON_CALLBACK`. - * @param {Object=} config Optional configuration object - * @returns {HttpPromise} Future object - */ - createShortMethods('get', 'delete', 'head', 'jsonp'); - - /** - * @ngdoc method - * @name $http#post - * - * @description - * Shortcut method to perform `POST` request. - * - * @param {string} url Relative or absolute URL specifying the destination of the request - * @param {*} data Request content - * @param {Object=} config Optional configuration object - * @returns {HttpPromise} Future object - */ - - /** - * @ngdoc method - * @name $http#put - * - * @description - * Shortcut method to perform `PUT` request. - * - * @param {string} url Relative or absolute URL specifying the destination of the request - * @param {*} data Request content - * @param {Object=} config Optional configuration object - * @returns {HttpPromise} Future object - */ - - /** - * @ngdoc method - * @name $http#patch - * - * @description - * Shortcut method to perform `PATCH` request. - * - * @param {string} url Relative or absolute URL specifying the destination of the request - * @param {*} data Request content - * @param {Object=} config Optional configuration object - * @returns {HttpPromise} Future object - */ - createShortMethodsWithData('post', 'put', 'patch'); - - /** - * @ngdoc property - * @name $http#defaults - * - * @description - * Runtime equivalent of the `$httpProvider.defaults` property. Allows configuration of - * default headers, withCredentials as well as request and response transformations. - * - * See "Setting HTTP Headers" and "Transforming Requests and Responses" sections above. - */ - $http.defaults = defaults; - - - return $http; - - - function createShortMethods(names) { - forEach(arguments, function(name) { - $http[name] = function(url, config) { - return $http(extend({}, config || {}, { - method: name, - url: url - })); - }; - }); - } - - - function createShortMethodsWithData(name) { - forEach(arguments, function(name) { - $http[name] = function(url, data, config) { - return $http(extend({}, config || {}, { - method: name, - url: url, - data: data - })); - }; - }); - } - - - /** - * Makes the request. - * - * !!! ACCESSES CLOSURE VARS: - * $httpBackend, defaults, $log, $rootScope, defaultCache, $http.pendingRequests - */ - function sendReq(config, reqData) { - var deferred = $q.defer(), - promise = deferred.promise, - cache, - cachedResp, - reqHeaders = config.headers, - url = buildUrl(config.url, config.paramSerializer(config.params)); - - $http.pendingRequests.push(config); - promise.then(removePendingReq, removePendingReq); - - - if ((config.cache || defaults.cache) && config.cache !== false && - (config.method === 'GET' || config.method === 'JSONP')) { - cache = isObject(config.cache) ? config.cache - : isObject(defaults.cache) ? defaults.cache - : defaultCache; - } - - if (cache) { - cachedResp = cache.get(url); - if (isDefined(cachedResp)) { - if (isPromiseLike(cachedResp)) { - // cached request has already been sent, but there is no response yet - cachedResp.then(resolvePromiseWithResult, resolvePromiseWithResult); - } else { - // serving from cache - if (isArray(cachedResp)) { - resolvePromise(cachedResp[1], cachedResp[0], shallowCopy(cachedResp[2]), cachedResp[3]); - } else { - resolvePromise(cachedResp, 200, {}, 'OK'); - } - } - } else { - // put the promise for the non-transformed response into cache as a placeholder - cache.put(url, promise); - } - } - - - // if we won't have the response in cache, set the xsrf headers and - // send the request to the backend - if (isUndefined(cachedResp)) { - var xsrfValue = urlIsSameOrigin(config.url) - ? $$cookieReader()[config.xsrfCookieName || defaults.xsrfCookieName] - : undefined; - if (xsrfValue) { - reqHeaders[(config.xsrfHeaderName || defaults.xsrfHeaderName)] = xsrfValue; - } - - $httpBackend(config.method, url, reqData, done, reqHeaders, config.timeout, - config.withCredentials, config.responseType); - } - - return promise; - - - /** - * Callback registered to $httpBackend(): - * - caches the response if desired - * - resolves the raw $http promise - * - calls $apply - */ - function done(status, response, headersString, statusText) { - if (cache) { - if (isSuccess(status)) { - cache.put(url, [status, response, parseHeaders(headersString), statusText]); - } else { - // remove promise from the cache - cache.remove(url); - } - } - - function resolveHttpPromise() { - resolvePromise(response, status, headersString, statusText); - } - - if (useApplyAsync) { - $rootScope.$applyAsync(resolveHttpPromise); - } else { - resolveHttpPromise(); - if (!$rootScope.$$phase) $rootScope.$apply(); - } - } - - - /** - * Resolves the raw $http promise. - */ - function resolvePromise(response, status, headers, statusText) { - // normalize internal statuses to 0 - status = Math.max(status, 0); - - (isSuccess(status) ? deferred.resolve : deferred.reject)({ - data: response, - status: status, - headers: headersGetter(headers), - config: config, - statusText: statusText - }); - } - - function resolvePromiseWithResult(result) { - resolvePromise(result.data, result.status, shallowCopy(result.headers()), result.statusText); - } - - function removePendingReq() { - var idx = $http.pendingRequests.indexOf(config); - if (idx !== -1) $http.pendingRequests.splice(idx, 1); - } - } - - - function buildUrl(url, serializedParams) { - if (serializedParams.length > 0) { - url += ((url.indexOf('?') == -1) ? '?' : '&') + serializedParams; - } - return url; - } - }]; -} - -function createXhr() { - return new window.XMLHttpRequest(); -} - -/** - * @ngdoc service - * @name $httpBackend - * @requires $window - * @requires $document - * - * @description - * HTTP backend used by the {@link ng.$http service} that delegates to - * XMLHttpRequest object or JSONP and deals with browser incompatibilities. - * - * You should never need to use this service directly, instead use the higher-level abstractions: - * {@link ng.$http $http} or {@link ngResource.$resource $resource}. - * - * During testing this implementation is swapped with {@link ngMock.$httpBackend mock - * $httpBackend} which can be trained with responses. - */ -function $HttpBackendProvider() { - this.$get = ['$browser', '$window', '$document', function($browser, $window, $document) { - return createHttpBackend($browser, createXhr, $browser.defer, $window.angular.callbacks, $document[0]); - }]; -} - -function createHttpBackend($browser, createXhr, $browserDefer, callbacks, rawDocument) { - // TODO(vojta): fix the signature - return function(method, url, post, callback, headers, timeout, withCredentials, responseType) { - $browser.$$incOutstandingRequestCount(); - url = url || $browser.url(); - - if (lowercase(method) == 'jsonp') { - var callbackId = '_' + (callbacks.counter++).toString(36); - callbacks[callbackId] = function(data) { - callbacks[callbackId].data = data; - callbacks[callbackId].called = true; - }; - - var jsonpDone = jsonpReq(url.replace('JSON_CALLBACK', 'angular.callbacks.' + callbackId), - callbackId, function(status, text) { - completeRequest(callback, status, callbacks[callbackId].data, "", text); - callbacks[callbackId] = noop; - }); - } else { - - var xhr = createXhr(); - - xhr.open(method, url, true); - forEach(headers, function(value, key) { - if (isDefined(value)) { - xhr.setRequestHeader(key, value); - } - }); - - xhr.onload = function requestLoaded() { - var statusText = xhr.statusText || ''; - - // responseText is the old-school way of retrieving response (supported by IE9) - // response/responseType properties were introduced in XHR Level2 spec (supported by IE10) - var response = ('response' in xhr) ? xhr.response : xhr.responseText; - - // normalize IE9 bug (http://bugs.jquery.com/ticket/1450) - var status = xhr.status === 1223 ? 204 : xhr.status; - - // fix status code when it is 0 (0 status is undocumented). - // Occurs when accessing file resources or on Android 4.1 stock browser - // while retrieving files from application cache. - if (status === 0) { - status = response ? 200 : urlResolve(url).protocol == 'file' ? 404 : 0; - } - - completeRequest(callback, - status, - response, - xhr.getAllResponseHeaders(), - statusText); - }; - - var requestError = function() { - // The response is always empty - // See https://xhr.spec.whatwg.org/#request-error-steps and https://fetch.spec.whatwg.org/#concept-network-error - completeRequest(callback, -1, null, null, ''); - }; - - xhr.onerror = requestError; - xhr.onabort = requestError; - - if (withCredentials) { - xhr.withCredentials = true; - } - - if (responseType) { - try { - xhr.responseType = responseType; - } catch (e) { - // WebKit added support for the json responseType value on 09/03/2013 - // https://bugs.webkit.org/show_bug.cgi?id=73648. Versions of Safari prior to 7 are - // known to throw when setting the value "json" as the response type. Other older - // browsers implementing the responseType - // - // The json response type can be ignored if not supported, because JSON payloads are - // parsed on the client-side regardless. - if (responseType !== 'json') { - throw e; - } - } - } - - xhr.send(post); - } - - if (timeout > 0) { - var timeoutId = $browserDefer(timeoutRequest, timeout); - } else if (isPromiseLike(timeout)) { - timeout.then(timeoutRequest); - } - - - function timeoutRequest() { - jsonpDone && jsonpDone(); - xhr && xhr.abort(); - } - - function completeRequest(callback, status, response, headersString, statusText) { - // cancel timeout and subsequent timeout promise resolution - if (timeoutId !== undefined) { - $browserDefer.cancel(timeoutId); - } - jsonpDone = xhr = null; - - callback(status, response, headersString, statusText); - $browser.$$completeOutstandingRequest(noop); - } - }; - - function jsonpReq(url, callbackId, done) { - // we can't use jQuery/jqLite here because jQuery does crazy stuff with script elements, e.g.: - // - fetches local scripts via XHR and evals them - // - adds and immediately removes script elements from the document - var script = rawDocument.createElement('script'), callback = null; - script.type = "text/javascript"; - script.src = url; - script.async = true; - - callback = function(event) { - removeEventListenerFn(script, "load", callback); - removeEventListenerFn(script, "error", callback); - rawDocument.body.removeChild(script); - script = null; - var status = -1; - var text = "unknown"; - - if (event) { - if (event.type === "load" && !callbacks[callbackId].called) { - event = { type: "error" }; - } - text = event.type; - status = event.type === "error" ? 404 : 200; - } - - if (done) { - done(status, text); - } - }; - - addEventListenerFn(script, "load", callback); - addEventListenerFn(script, "error", callback); - rawDocument.body.appendChild(script); - return callback; - } -} - -var $interpolateMinErr = angular.$interpolateMinErr = minErr('$interpolate'); -$interpolateMinErr.throwNoconcat = function(text) { - throw $interpolateMinErr('noconcat', - "Error while interpolating: {0}\nStrict Contextual Escaping disallows " + - "interpolations that concatenate multiple expressions when a trusted value is " + - "required. See http://docs.angularjs.org/api/ng.$sce", text); -}; - -$interpolateMinErr.interr = function(text, err) { - return $interpolateMinErr('interr', "Can't interpolate: {0}\n{1}", text, err.toString()); -}; - -/** - * @ngdoc provider - * @name $interpolateProvider - * - * @description - * - * Used for configuring the interpolation markup. Defaults to `{{` and `}}`. - * - * @example - - - -
    - //demo.label// -
    -
    - - it('should interpolate binding with custom symbols', function() { - expect(element(by.binding('demo.label')).getText()).toBe('This binding is brought you by // interpolation symbols.'); - }); - -
    - */ -function $InterpolateProvider() { - var startSymbol = '{{'; - var endSymbol = '}}'; - - /** - * @ngdoc method - * @name $interpolateProvider#startSymbol - * @description - * Symbol to denote start of expression in the interpolated string. Defaults to `{{`. - * - * @param {string=} value new value to set the starting symbol to. - * @returns {string|self} Returns the symbol when used as getter and self if used as setter. - */ - this.startSymbol = function(value) { - if (value) { - startSymbol = value; - return this; - } else { - return startSymbol; - } - }; - - /** - * @ngdoc method - * @name $interpolateProvider#endSymbol - * @description - * Symbol to denote the end of expression in the interpolated string. Defaults to `}}`. - * - * @param {string=} value new value to set the ending symbol to. - * @returns {string|self} Returns the symbol when used as getter and self if used as setter. - */ - this.endSymbol = function(value) { - if (value) { - endSymbol = value; - return this; - } else { - return endSymbol; - } - }; - - - this.$get = ['$parse', '$exceptionHandler', '$sce', function($parse, $exceptionHandler, $sce) { - var startSymbolLength = startSymbol.length, - endSymbolLength = endSymbol.length, - escapedStartRegexp = new RegExp(startSymbol.replace(/./g, escape), 'g'), - escapedEndRegexp = new RegExp(endSymbol.replace(/./g, escape), 'g'); - - function escape(ch) { - return '\\\\\\' + ch; - } - - function unescapeText(text) { - return text.replace(escapedStartRegexp, startSymbol). - replace(escapedEndRegexp, endSymbol); - } - - function stringify(value) { - if (value == null) { // null || undefined - return ''; - } - switch (typeof value) { - case 'string': - break; - case 'number': - value = '' + value; - break; - default: - value = toJson(value); - } - - return value; - } - - /** - * @ngdoc service - * @name $interpolate - * @kind function - * - * @requires $parse - * @requires $sce - * - * @description - * - * Compiles a string with markup into an interpolation function. This service is used by the - * HTML {@link ng.$compile $compile} service for data binding. See - * {@link ng.$interpolateProvider $interpolateProvider} for configuring the - * interpolation markup. - * - * - * ```js - * var $interpolate = ...; // injected - * var exp = $interpolate('Hello {{name | uppercase}}!'); - * expect(exp({name:'Angular'}).toEqual('Hello ANGULAR!'); - * ``` - * - * `$interpolate` takes an optional fourth argument, `allOrNothing`. If `allOrNothing` is - * `true`, the interpolation function will return `undefined` unless all embedded expressions - * evaluate to a value other than `undefined`. - * - * ```js - * var $interpolate = ...; // injected - * var context = {greeting: 'Hello', name: undefined }; - * - * // default "forgiving" mode - * var exp = $interpolate('{{greeting}} {{name}}!'); - * expect(exp(context)).toEqual('Hello !'); - * - * // "allOrNothing" mode - * exp = $interpolate('{{greeting}} {{name}}!', false, null, true); - * expect(exp(context)).toBeUndefined(); - * context.name = 'Angular'; - * expect(exp(context)).toEqual('Hello Angular!'); - * ``` - * - * `allOrNothing` is useful for interpolating URLs. `ngSrc` and `ngSrcset` use this behavior. - * - * ####Escaped Interpolation - * $interpolate provides a mechanism for escaping interpolation markers. Start and end markers - * can be escaped by preceding each of their characters with a REVERSE SOLIDUS U+005C (backslash). - * It will be rendered as a regular start/end marker, and will not be interpreted as an expression - * or binding. - * - * This enables web-servers to prevent script injection attacks and defacing attacks, to some - * degree, while also enabling code examples to work without relying on the - * {@link ng.directive:ngNonBindable ngNonBindable} directive. - * - * **For security purposes, it is strongly encouraged that web servers escape user-supplied data, - * replacing angle brackets (<, >) with &lt; and &gt; respectively, and replacing all - * interpolation start/end markers with their escaped counterparts.** - * - * Escaped interpolation markers are only replaced with the actual interpolation markers in rendered - * output when the $interpolate service processes the text. So, for HTML elements interpolated - * by {@link ng.$compile $compile}, or otherwise interpolated with the `mustHaveExpression` parameter - * set to `true`, the interpolated text must contain an unescaped interpolation expression. As such, - * this is typically useful only when user-data is used in rendering a template from the server, or - * when otherwise untrusted data is used by a directive. - * - * - * - *
    - *

    {{apptitle}}: \{\{ username = "defaced value"; \}\} - *

    - *

    {{username}} attempts to inject code which will deface the - * application, but fails to accomplish their task, because the server has correctly - * escaped the interpolation start/end markers with REVERSE SOLIDUS U+005C (backslash) - * characters.

    - *

    Instead, the result of the attempted script injection is visible, and can be removed - * from the database by an administrator.

    - *
    - *
    - *
    - * - * @param {string} text The text with markup to interpolate. - * @param {boolean=} mustHaveExpression if set to true then the interpolation string must have - * embedded expression in order to return an interpolation function. Strings with no - * embedded expression will return null for the interpolation function. - * @param {string=} trustedContext when provided, the returned function passes the interpolated - * result through {@link ng.$sce#getTrusted $sce.getTrusted(interpolatedResult, - * trustedContext)} before returning it. Refer to the {@link ng.$sce $sce} service that - * provides Strict Contextual Escaping for details. - * @param {boolean=} allOrNothing if `true`, then the returned function returns undefined - * unless all embedded expressions evaluate to a value other than `undefined`. - * @returns {function(context)} an interpolation function which is used to compute the - * interpolated string. The function has these parameters: - * - * - `context`: evaluation context for all expressions embedded in the interpolated text - */ - function $interpolate(text, mustHaveExpression, trustedContext, allOrNothing) { - allOrNothing = !!allOrNothing; - var startIndex, - endIndex, - index = 0, - expressions = [], - parseFns = [], - textLength = text.length, - exp, - concat = [], - expressionPositions = []; - - while (index < textLength) { - if (((startIndex = text.indexOf(startSymbol, index)) != -1) && - ((endIndex = text.indexOf(endSymbol, startIndex + startSymbolLength)) != -1)) { - if (index !== startIndex) { - concat.push(unescapeText(text.substring(index, startIndex))); - } - exp = text.substring(startIndex + startSymbolLength, endIndex); - expressions.push(exp); - parseFns.push($parse(exp, parseStringifyInterceptor)); - index = endIndex + endSymbolLength; - expressionPositions.push(concat.length); - concat.push(''); - } else { - // we did not find an interpolation, so we have to add the remainder to the separators array - if (index !== textLength) { - concat.push(unescapeText(text.substring(index))); - } - break; - } - } - - // Concatenating expressions makes it hard to reason about whether some combination of - // concatenated values are unsafe to use and could easily lead to XSS. By requiring that a - // single expression be used for iframe[src], object[src], etc., we ensure that the value - // that's used is assigned or constructed by some JS code somewhere that is more testable or - // make it obvious that you bound the value to some user controlled value. This helps reduce - // the load when auditing for XSS issues. - if (trustedContext && concat.length > 1) { - $interpolateMinErr.throwNoconcat(text); - } - - if (!mustHaveExpression || expressions.length) { - var compute = function(values) { - for (var i = 0, ii = expressions.length; i < ii; i++) { - if (allOrNothing && isUndefined(values[i])) return; - concat[expressionPositions[i]] = values[i]; - } - return concat.join(''); - }; - - var getValue = function(value) { - return trustedContext ? - $sce.getTrusted(trustedContext, value) : - $sce.valueOf(value); - }; - - return extend(function interpolationFn(context) { - var i = 0; - var ii = expressions.length; - var values = new Array(ii); - - try { - for (; i < ii; i++) { - values[i] = parseFns[i](context); - } - - return compute(values); - } catch (err) { - $exceptionHandler($interpolateMinErr.interr(text, err)); - } - - }, { - // all of these properties are undocumented for now - exp: text, //just for compatibility with regular watchers created via $watch - expressions: expressions, - $$watchDelegate: function(scope, listener) { - var lastValue; - return scope.$watchGroup(parseFns, function interpolateFnWatcher(values, oldValues) { - var currValue = compute(values); - if (isFunction(listener)) { - listener.call(this, currValue, values !== oldValues ? lastValue : currValue, scope); - } - lastValue = currValue; - }); - } - }); - } - - function parseStringifyInterceptor(value) { - try { - value = getValue(value); - return allOrNothing && !isDefined(value) ? value : stringify(value); - } catch (err) { - $exceptionHandler($interpolateMinErr.interr(text, err)); - } - } - } - - - /** - * @ngdoc method - * @name $interpolate#startSymbol - * @description - * Symbol to denote the start of expression in the interpolated string. Defaults to `{{`. - * - * Use {@link ng.$interpolateProvider#startSymbol `$interpolateProvider.startSymbol`} to change - * the symbol. - * - * @returns {string} start symbol. - */ - $interpolate.startSymbol = function() { - return startSymbol; - }; - - - /** - * @ngdoc method - * @name $interpolate#endSymbol - * @description - * Symbol to denote the end of expression in the interpolated string. Defaults to `}}`. - * - * Use {@link ng.$interpolateProvider#endSymbol `$interpolateProvider.endSymbol`} to change - * the symbol. - * - * @returns {string} end symbol. - */ - $interpolate.endSymbol = function() { - return endSymbol; - }; - - return $interpolate; - }]; -} - -function $IntervalProvider() { - this.$get = ['$rootScope', '$window', '$q', '$$q', - function($rootScope, $window, $q, $$q) { - var intervals = {}; - - - /** - * @ngdoc service - * @name $interval - * - * @description - * Angular's wrapper for `window.setInterval`. The `fn` function is executed every `delay` - * milliseconds. - * - * The return value of registering an interval function is a promise. This promise will be - * notified upon each tick of the interval, and will be resolved after `count` iterations, or - * run indefinitely if `count` is not defined. The value of the notification will be the - * number of iterations that have run. - * To cancel an interval, call `$interval.cancel(promise)`. - * - * In tests you can use {@link ngMock.$interval#flush `$interval.flush(millis)`} to - * move forward by `millis` milliseconds and trigger any functions scheduled to run in that - * time. - * - *
    - * **Note**: Intervals created by this service must be explicitly destroyed when you are finished - * with them. In particular they are not automatically destroyed when a controller's scope or a - * directive's element are destroyed. - * You should take this into consideration and make sure to always cancel the interval at the - * appropriate moment. See the example below for more details on how and when to do this. - *
    - * - * @param {function()} fn A function that should be called repeatedly. - * @param {number} delay Number of milliseconds between each function call. - * @param {number=} [count=0] Number of times to repeat. If not set, or 0, will repeat - * indefinitely. - * @param {boolean=} [invokeApply=true] If set to `false` skips model dirty checking, otherwise - * will invoke `fn` within the {@link ng.$rootScope.Scope#$apply $apply} block. - * @param {...*=} Pass additional parameters to the executed function. - * @returns {promise} A promise which will be notified on each iteration. - * - * @example - * - * - * - * - *
    - *
    - *
    - * Current time is: - *
    - * Blood 1 : {{blood_1}} - * Blood 2 : {{blood_2}} - * - * - * - *
    - *
    - * - *
    - *
    - */ - function interval(fn, delay, count, invokeApply) { - var hasParams = arguments.length > 4, - args = hasParams ? sliceArgs(arguments, 4) : [], - setInterval = $window.setInterval, - clearInterval = $window.clearInterval, - iteration = 0, - skipApply = (isDefined(invokeApply) && !invokeApply), - deferred = (skipApply ? $$q : $q).defer(), - promise = deferred.promise; - - count = isDefined(count) ? count : 0; - - promise.then(null, null, (!hasParams) ? fn : function() { - fn.apply(null, args); - }); - - promise.$$intervalId = setInterval(function tick() { - deferred.notify(iteration++); - - if (count > 0 && iteration >= count) { - deferred.resolve(iteration); - clearInterval(promise.$$intervalId); - delete intervals[promise.$$intervalId]; - } - - if (!skipApply) $rootScope.$apply(); - - }, delay); - - intervals[promise.$$intervalId] = deferred; - - return promise; - } - - - /** - * @ngdoc method - * @name $interval#cancel - * - * @description - * Cancels a task associated with the `promise`. - * - * @param {Promise=} promise returned by the `$interval` function. - * @returns {boolean} Returns `true` if the task was successfully canceled. - */ - interval.cancel = function(promise) { - if (promise && promise.$$intervalId in intervals) { - intervals[promise.$$intervalId].reject('canceled'); - $window.clearInterval(promise.$$intervalId); - delete intervals[promise.$$intervalId]; - return true; - } - return false; - }; - - return interval; - }]; -} - -/** - * @ngdoc service - * @name $locale - * - * @description - * $locale service provides localization rules for various Angular components. As of right now the - * only public api is: - * - * * `id` – `{string}` – locale id formatted as `languageId-countryId` (e.g. `en-us`) - */ - -var PATH_MATCH = /^([^\?#]*)(\?([^#]*))?(#(.*))?$/, - DEFAULT_PORTS = {'http': 80, 'https': 443, 'ftp': 21}; -var $locationMinErr = minErr('$location'); - - -/** - * Encode path using encodeUriSegment, ignoring forward slashes - * - * @param {string} path Path to encode - * @returns {string} - */ -function encodePath(path) { - var segments = path.split('/'), - i = segments.length; - - while (i--) { - segments[i] = encodeUriSegment(segments[i]); - } - - return segments.join('/'); -} - -function parseAbsoluteUrl(absoluteUrl, locationObj) { - var parsedUrl = urlResolve(absoluteUrl); - - locationObj.$$protocol = parsedUrl.protocol; - locationObj.$$host = parsedUrl.hostname; - locationObj.$$port = toInt(parsedUrl.port) || DEFAULT_PORTS[parsedUrl.protocol] || null; -} - - -function parseAppUrl(relativeUrl, locationObj) { - var prefixed = (relativeUrl.charAt(0) !== '/'); - if (prefixed) { - relativeUrl = '/' + relativeUrl; - } - var match = urlResolve(relativeUrl); - locationObj.$$path = decodeURIComponent(prefixed && match.pathname.charAt(0) === '/' ? - match.pathname.substring(1) : match.pathname); - locationObj.$$search = parseKeyValue(match.search); - locationObj.$$hash = decodeURIComponent(match.hash); - - // make sure path starts with '/'; - if (locationObj.$$path && locationObj.$$path.charAt(0) != '/') { - locationObj.$$path = '/' + locationObj.$$path; - } -} - - -/** - * - * @param {string} begin - * @param {string} whole - * @returns {string} returns text from whole after begin or undefined if it does not begin with - * expected string. - */ -function beginsWith(begin, whole) { - if (whole.indexOf(begin) === 0) { - return whole.substr(begin.length); - } -} - - -function stripHash(url) { - var index = url.indexOf('#'); - return index == -1 ? url : url.substr(0, index); -} - -function trimEmptyHash(url) { - return url.replace(/(#.+)|#$/, '$1'); -} - - -function stripFile(url) { - return url.substr(0, stripHash(url).lastIndexOf('/') + 1); -} - -/* return the server only (scheme://host:port) */ -function serverBase(url) { - return url.substring(0, url.indexOf('/', url.indexOf('//') + 2)); -} - - -/** - * LocationHtml5Url represents an url - * This object is exposed as $location service when HTML5 mode is enabled and supported - * - * @constructor - * @param {string} appBase application base URL - * @param {string} appBaseNoFile application base URL stripped of any filename - * @param {string} basePrefix url path prefix - */ -function LocationHtml5Url(appBase, appBaseNoFile, basePrefix) { - this.$$html5 = true; - basePrefix = basePrefix || ''; - parseAbsoluteUrl(appBase, this); - - - /** - * Parse given html5 (regular) url string into properties - * @param {string} url HTML5 url - * @private - */ - this.$$parse = function(url) { - var pathUrl = beginsWith(appBaseNoFile, url); - if (!isString(pathUrl)) { - throw $locationMinErr('ipthprfx', 'Invalid url "{0}", missing path prefix "{1}".', url, - appBaseNoFile); - } - - parseAppUrl(pathUrl, this); - - if (!this.$$path) { - this.$$path = '/'; - } - - this.$$compose(); - }; - - /** - * Compose url and update `absUrl` property - * @private - */ - this.$$compose = function() { - var search = toKeyValue(this.$$search), - hash = this.$$hash ? '#' + encodeUriSegment(this.$$hash) : ''; - - this.$$url = encodePath(this.$$path) + (search ? '?' + search : '') + hash; - this.$$absUrl = appBaseNoFile + this.$$url.substr(1); // first char is always '/' - }; - - this.$$parseLinkUrl = function(url, relHref) { - if (relHref && relHref[0] === '#') { - // special case for links to hash fragments: - // keep the old url and only replace the hash fragment - this.hash(relHref.slice(1)); - return true; - } - var appUrl, prevAppUrl; - var rewrittenUrl; - - if ((appUrl = beginsWith(appBase, url)) !== undefined) { - prevAppUrl = appUrl; - if ((appUrl = beginsWith(basePrefix, appUrl)) !== undefined) { - rewrittenUrl = appBaseNoFile + (beginsWith('/', appUrl) || appUrl); - } else { - rewrittenUrl = appBase + prevAppUrl; - } - } else if ((appUrl = beginsWith(appBaseNoFile, url)) !== undefined) { - rewrittenUrl = appBaseNoFile + appUrl; - } else if (appBaseNoFile == url + '/') { - rewrittenUrl = appBaseNoFile; - } - if (rewrittenUrl) { - this.$$parse(rewrittenUrl); - } - return !!rewrittenUrl; - }; -} - - -/** - * LocationHashbangUrl represents url - * This object is exposed as $location service when developer doesn't opt into html5 mode. - * It also serves as the base class for html5 mode fallback on legacy browsers. - * - * @constructor - * @param {string} appBase application base URL - * @param {string} appBaseNoFile application base URL stripped of any filename - * @param {string} hashPrefix hashbang prefix - */ -function LocationHashbangUrl(appBase, appBaseNoFile, hashPrefix) { - - parseAbsoluteUrl(appBase, this); - - - /** - * Parse given hashbang url into properties - * @param {string} url Hashbang url - * @private - */ - this.$$parse = function(url) { - var withoutBaseUrl = beginsWith(appBase, url) || beginsWith(appBaseNoFile, url); - var withoutHashUrl; - - if (!isUndefined(withoutBaseUrl) && withoutBaseUrl.charAt(0) === '#') { - - // The rest of the url starts with a hash so we have - // got either a hashbang path or a plain hash fragment - withoutHashUrl = beginsWith(hashPrefix, withoutBaseUrl); - if (isUndefined(withoutHashUrl)) { - // There was no hashbang prefix so we just have a hash fragment - withoutHashUrl = withoutBaseUrl; - } - - } else { - // There was no hashbang path nor hash fragment: - // If we are in HTML5 mode we use what is left as the path; - // Otherwise we ignore what is left - if (this.$$html5) { - withoutHashUrl = withoutBaseUrl; - } else { - withoutHashUrl = ''; - if (isUndefined(withoutBaseUrl)) { - appBase = url; - this.replace(); - } - } - } - - parseAppUrl(withoutHashUrl, this); - - this.$$path = removeWindowsDriveName(this.$$path, withoutHashUrl, appBase); - - this.$$compose(); - - /* - * In Windows, on an anchor node on documents loaded from - * the filesystem, the browser will return a pathname - * prefixed with the drive name ('/C:/path') when a - * pathname without a drive is set: - * * a.setAttribute('href', '/foo') - * * a.pathname === '/C:/foo' //true - * - * Inside of Angular, we're always using pathnames that - * do not include drive names for routing. - */ - function removeWindowsDriveName(path, url, base) { - /* - Matches paths for file protocol on windows, - such as /C:/foo/bar, and captures only /foo/bar. - */ - var windowsFilePathExp = /^\/[A-Z]:(\/.*)/; - - var firstPathSegmentMatch; - - //Get the relative path from the input URL. - if (url.indexOf(base) === 0) { - url = url.replace(base, ''); - } - - // The input URL intentionally contains a first path segment that ends with a colon. - if (windowsFilePathExp.exec(url)) { - return path; - } - - firstPathSegmentMatch = windowsFilePathExp.exec(path); - return firstPathSegmentMatch ? firstPathSegmentMatch[1] : path; - } - }; - - /** - * Compose hashbang url and update `absUrl` property - * @private - */ - this.$$compose = function() { - var search = toKeyValue(this.$$search), - hash = this.$$hash ? '#' + encodeUriSegment(this.$$hash) : ''; - - this.$$url = encodePath(this.$$path) + (search ? '?' + search : '') + hash; - this.$$absUrl = appBase + (this.$$url ? hashPrefix + this.$$url : ''); - }; - - this.$$parseLinkUrl = function(url, relHref) { - if (stripHash(appBase) == stripHash(url)) { - this.$$parse(url); - return true; - } - return false; - }; -} - - -/** - * LocationHashbangUrl represents url - * This object is exposed as $location service when html5 history api is enabled but the browser - * does not support it. - * - * @constructor - * @param {string} appBase application base URL - * @param {string} appBaseNoFile application base URL stripped of any filename - * @param {string} hashPrefix hashbang prefix - */ -function LocationHashbangInHtml5Url(appBase, appBaseNoFile, hashPrefix) { - this.$$html5 = true; - LocationHashbangUrl.apply(this, arguments); - - this.$$parseLinkUrl = function(url, relHref) { - if (relHref && relHref[0] === '#') { - // special case for links to hash fragments: - // keep the old url and only replace the hash fragment - this.hash(relHref.slice(1)); - return true; - } - - var rewrittenUrl; - var appUrl; - - if (appBase == stripHash(url)) { - rewrittenUrl = url; - } else if ((appUrl = beginsWith(appBaseNoFile, url))) { - rewrittenUrl = appBase + hashPrefix + appUrl; - } else if (appBaseNoFile === url + '/') { - rewrittenUrl = appBaseNoFile; - } - if (rewrittenUrl) { - this.$$parse(rewrittenUrl); - } - return !!rewrittenUrl; - }; - - this.$$compose = function() { - var search = toKeyValue(this.$$search), - hash = this.$$hash ? '#' + encodeUriSegment(this.$$hash) : ''; - - this.$$url = encodePath(this.$$path) + (search ? '?' + search : '') + hash; - // include hashPrefix in $$absUrl when $$url is empty so IE9 does not reload page because of removal of '#' - this.$$absUrl = appBase + hashPrefix + this.$$url; - }; - -} - - -var locationPrototype = { - - /** - * Are we in html5 mode? - * @private - */ - $$html5: false, - - /** - * Has any change been replacing? - * @private - */ - $$replace: false, - - /** - * @ngdoc method - * @name $location#absUrl - * - * @description - * This method is getter only. - * - * Return full url representation with all segments encoded according to rules specified in - * [RFC 3986](http://www.ietf.org/rfc/rfc3986.txt). - * - * - * ```js - * // given url http://example.com/#/some/path?foo=bar&baz=xoxo - * var absUrl = $location.absUrl(); - * // => "http://example.com/#/some/path?foo=bar&baz=xoxo" - * ``` - * - * @return {string} full url - */ - absUrl: locationGetter('$$absUrl'), - - /** - * @ngdoc method - * @name $location#url - * - * @description - * This method is getter / setter. - * - * Return url (e.g. `/path?a=b#hash`) when called without any parameter. - * - * Change path, search and hash, when called with parameter and return `$location`. - * - * - * ```js - * // given url http://example.com/#/some/path?foo=bar&baz=xoxo - * var url = $location.url(); - * // => "/some/path?foo=bar&baz=xoxo" - * ``` - * - * @param {string=} url New url without base prefix (e.g. `/path?a=b#hash`) - * @return {string} url - */ - url: function(url) { - if (isUndefined(url)) { - return this.$$url; - } - - var match = PATH_MATCH.exec(url); - if (match[1] || url === '') this.path(decodeURIComponent(match[1])); - if (match[2] || match[1] || url === '') this.search(match[3] || ''); - this.hash(match[5] || ''); - - return this; - }, - - /** - * @ngdoc method - * @name $location#protocol - * - * @description - * This method is getter only. - * - * Return protocol of current url. - * - * - * ```js - * // given url http://example.com/#/some/path?foo=bar&baz=xoxo - * var protocol = $location.protocol(); - * // => "http" - * ``` - * - * @return {string} protocol of current url - */ - protocol: locationGetter('$$protocol'), - - /** - * @ngdoc method - * @name $location#host - * - * @description - * This method is getter only. - * - * Return host of current url. - * - * Note: compared to the non-angular version `location.host` which returns `hostname:port`, this returns the `hostname` portion only. - * - * - * ```js - * // given url http://example.com/#/some/path?foo=bar&baz=xoxo - * var host = $location.host(); - * // => "example.com" - * - * // given url http://user:password@example.com:8080/#/some/path?foo=bar&baz=xoxo - * host = $location.host(); - * // => "example.com" - * host = location.host; - * // => "example.com:8080" - * ``` - * - * @return {string} host of current url. - */ - host: locationGetter('$$host'), - - /** - * @ngdoc method - * @name $location#port - * - * @description - * This method is getter only. - * - * Return port of current url. - * - * - * ```js - * // given url http://example.com/#/some/path?foo=bar&baz=xoxo - * var port = $location.port(); - * // => 80 - * ``` - * - * @return {Number} port - */ - port: locationGetter('$$port'), - - /** - * @ngdoc method - * @name $location#path - * - * @description - * This method is getter / setter. - * - * Return path of current url when called without any parameter. - * - * Change path when called with parameter and return `$location`. - * - * Note: Path should always begin with forward slash (/), this method will add the forward slash - * if it is missing. - * - * - * ```js - * // given url http://example.com/#/some/path?foo=bar&baz=xoxo - * var path = $location.path(); - * // => "/some/path" - * ``` - * - * @param {(string|number)=} path New path - * @return {string} path - */ - path: locationGetterSetter('$$path', function(path) { - path = path !== null ? path.toString() : ''; - return path.charAt(0) == '/' ? path : '/' + path; - }), - - /** - * @ngdoc method - * @name $location#search - * - * @description - * This method is getter / setter. - * - * Return search part (as object) of current url when called without any parameter. - * - * Change search part when called with parameter and return `$location`. - * - * - * ```js - * // given url http://example.com/#/some/path?foo=bar&baz=xoxo - * var searchObject = $location.search(); - * // => {foo: 'bar', baz: 'xoxo'} - * - * // set foo to 'yipee' - * $location.search('foo', 'yipee'); - * // $location.search() => {foo: 'yipee', baz: 'xoxo'} - * ``` - * - * @param {string|Object.|Object.>} search New search params - string or - * hash object. - * - * When called with a single argument the method acts as a setter, setting the `search` component - * of `$location` to the specified value. - * - * If the argument is a hash object containing an array of values, these values will be encoded - * as duplicate search parameters in the url. - * - * @param {(string|Number|Array|boolean)=} paramValue If `search` is a string or number, then `paramValue` - * will override only a single search property. - * - * If `paramValue` is an array, it will override the property of the `search` component of - * `$location` specified via the first argument. - * - * If `paramValue` is `null`, the property specified via the first argument will be deleted. - * - * If `paramValue` is `true`, the property specified via the first argument will be added with no - * value nor trailing equal sign. - * - * @return {Object} If called with no arguments returns the parsed `search` object. If called with - * one or more arguments returns `$location` object itself. - */ - search: function(search, paramValue) { - switch (arguments.length) { - case 0: - return this.$$search; - case 1: - if (isString(search) || isNumber(search)) { - search = search.toString(); - this.$$search = parseKeyValue(search); - } else if (isObject(search)) { - search = copy(search, {}); - // remove object undefined or null properties - forEach(search, function(value, key) { - if (value == null) delete search[key]; - }); - - this.$$search = search; - } else { - throw $locationMinErr('isrcharg', - 'The first argument of the `$location#search()` call must be a string or an object.'); - } - break; - default: - if (isUndefined(paramValue) || paramValue === null) { - delete this.$$search[search]; - } else { - this.$$search[search] = paramValue; - } - } - - this.$$compose(); - return this; - }, - - /** - * @ngdoc method - * @name $location#hash - * - * @description - * This method is getter / setter. - * - * Return hash fragment when called without any parameter. - * - * Change hash fragment when called with parameter and return `$location`. - * - * - * ```js - * // given url http://example.com/#/some/path?foo=bar&baz=xoxo#hashValue - * var hash = $location.hash(); - * // => "hashValue" - * ``` - * - * @param {(string|number)=} hash New hash fragment - * @return {string} hash - */ - hash: locationGetterSetter('$$hash', function(hash) { - return hash !== null ? hash.toString() : ''; - }), - - /** - * @ngdoc method - * @name $location#replace - * - * @description - * If called, all changes to $location during current `$digest` will be replacing current history - * record, instead of adding new one. - */ - replace: function() { - this.$$replace = true; - return this; - } -}; - -forEach([LocationHashbangInHtml5Url, LocationHashbangUrl, LocationHtml5Url], function(Location) { - Location.prototype = Object.create(locationPrototype); - - /** - * @ngdoc method - * @name $location#state - * - * @description - * This method is getter / setter. - * - * Return the history state object when called without any parameter. - * - * Change the history state object when called with one parameter and return `$location`. - * The state object is later passed to `pushState` or `replaceState`. - * - * NOTE: This method is supported only in HTML5 mode and only in browsers supporting - * the HTML5 History API (i.e. methods `pushState` and `replaceState`). If you need to support - * older browsers (like IE9 or Android < 4.0), don't use this method. - * - * @param {object=} state State object for pushState or replaceState - * @return {object} state - */ - Location.prototype.state = function(state) { - if (!arguments.length) { - return this.$$state; - } - - if (Location !== LocationHtml5Url || !this.$$html5) { - throw $locationMinErr('nostate', 'History API state support is available only ' + - 'in HTML5 mode and only in browsers supporting HTML5 History API'); - } - // The user might modify `stateObject` after invoking `$location.state(stateObject)` - // but we're changing the $$state reference to $browser.state() during the $digest - // so the modification window is narrow. - this.$$state = isUndefined(state) ? null : state; - - return this; - }; -}); - - -function locationGetter(property) { - return function() { - return this[property]; - }; -} - - -function locationGetterSetter(property, preprocess) { - return function(value) { - if (isUndefined(value)) { - return this[property]; - } - - this[property] = preprocess(value); - this.$$compose(); - - return this; - }; -} - - -/** - * @ngdoc service - * @name $location - * - * @requires $rootElement - * - * @description - * The $location service parses the URL in the browser address bar (based on the - * [window.location](https://developer.mozilla.org/en/window.location)) and makes the URL - * available to your application. Changes to the URL in the address bar are reflected into - * $location service and changes to $location are reflected into the browser address bar. - * - * **The $location service:** - * - * - Exposes the current URL in the browser address bar, so you can - * - Watch and observe the URL. - * - Change the URL. - * - Synchronizes the URL with the browser when the user - * - Changes the address bar. - * - Clicks the back or forward button (or clicks a History link). - * - Clicks on a link. - * - Represents the URL object as a set of methods (protocol, host, port, path, search, hash). - * - * For more information see {@link guide/$location Developer Guide: Using $location} - */ - -/** - * @ngdoc provider - * @name $locationProvider - * @description - * Use the `$locationProvider` to configure how the application deep linking paths are stored. - */ -function $LocationProvider() { - var hashPrefix = '', - html5Mode = { - enabled: false, - requireBase: true, - rewriteLinks: true - }; - - /** - * @ngdoc method - * @name $locationProvider#hashPrefix - * @description - * @param {string=} prefix Prefix for hash part (containing path and search) - * @returns {*} current value if used as getter or itself (chaining) if used as setter - */ - this.hashPrefix = function(prefix) { - if (isDefined(prefix)) { - hashPrefix = prefix; - return this; - } else { - return hashPrefix; - } - }; - - /** - * @ngdoc method - * @name $locationProvider#html5Mode - * @description - * @param {(boolean|Object)=} mode If boolean, sets `html5Mode.enabled` to value. - * If object, sets `enabled`, `requireBase` and `rewriteLinks` to respective values. Supported - * properties: - * - **enabled** – `{boolean}` – (default: false) If true, will rely on `history.pushState` to - * change urls where supported. Will fall back to hash-prefixed paths in browsers that do not - * support `pushState`. - * - **requireBase** - `{boolean}` - (default: `true`) When html5Mode is enabled, specifies - * whether or not a tag is required to be present. If `enabled` and `requireBase` are - * true, and a base tag is not present, an error will be thrown when `$location` is injected. - * See the {@link guide/$location $location guide for more information} - * - **rewriteLinks** - `{boolean}` - (default: `true`) When html5Mode is enabled, - * enables/disables url rewriting for relative links. - * - * @returns {Object} html5Mode object if used as getter or itself (chaining) if used as setter - */ - this.html5Mode = function(mode) { - if (isBoolean(mode)) { - html5Mode.enabled = mode; - return this; - } else if (isObject(mode)) { - - if (isBoolean(mode.enabled)) { - html5Mode.enabled = mode.enabled; - } - - if (isBoolean(mode.requireBase)) { - html5Mode.requireBase = mode.requireBase; - } - - if (isBoolean(mode.rewriteLinks)) { - html5Mode.rewriteLinks = mode.rewriteLinks; - } - - return this; - } else { - return html5Mode; - } - }; - - /** - * @ngdoc event - * @name $location#$locationChangeStart - * @eventType broadcast on root scope - * @description - * Broadcasted before a URL will change. - * - * This change can be prevented by calling - * `preventDefault` method of the event. See {@link ng.$rootScope.Scope#$on} for more - * details about event object. Upon successful change - * {@link ng.$location#$locationChangeSuccess $locationChangeSuccess} is fired. - * - * The `newState` and `oldState` parameters may be defined only in HTML5 mode and when - * the browser supports the HTML5 History API. - * - * @param {Object} angularEvent Synthetic event object. - * @param {string} newUrl New URL - * @param {string=} oldUrl URL that was before it was changed. - * @param {string=} newState New history state object - * @param {string=} oldState History state object that was before it was changed. - */ - - /** - * @ngdoc event - * @name $location#$locationChangeSuccess - * @eventType broadcast on root scope - * @description - * Broadcasted after a URL was changed. - * - * The `newState` and `oldState` parameters may be defined only in HTML5 mode and when - * the browser supports the HTML5 History API. - * - * @param {Object} angularEvent Synthetic event object. - * @param {string} newUrl New URL - * @param {string=} oldUrl URL that was before it was changed. - * @param {string=} newState New history state object - * @param {string=} oldState History state object that was before it was changed. - */ - - this.$get = ['$rootScope', '$browser', '$sniffer', '$rootElement', '$window', - function($rootScope, $browser, $sniffer, $rootElement, $window) { - var $location, - LocationMode, - baseHref = $browser.baseHref(), // if base[href] is undefined, it defaults to '' - initialUrl = $browser.url(), - appBase; - - if (html5Mode.enabled) { - if (!baseHref && html5Mode.requireBase) { - throw $locationMinErr('nobase', - "$location in HTML5 mode requires a tag to be present!"); - } - appBase = serverBase(initialUrl) + (baseHref || '/'); - LocationMode = $sniffer.history ? LocationHtml5Url : LocationHashbangInHtml5Url; - } else { - appBase = stripHash(initialUrl); - LocationMode = LocationHashbangUrl; - } - var appBaseNoFile = stripFile(appBase); - - $location = new LocationMode(appBase, appBaseNoFile, '#' + hashPrefix); - $location.$$parseLinkUrl(initialUrl, initialUrl); - - $location.$$state = $browser.state(); - - var IGNORE_URI_REGEXP = /^\s*(javascript|mailto):/i; - - function setBrowserUrlWithFallback(url, replace, state) { - var oldUrl = $location.url(); - var oldState = $location.$$state; - try { - $browser.url(url, replace, state); - - // Make sure $location.state() returns referentially identical (not just deeply equal) - // state object; this makes possible quick checking if the state changed in the digest - // loop. Checking deep equality would be too expensive. - $location.$$state = $browser.state(); - } catch (e) { - // Restore old values if pushState fails - $location.url(oldUrl); - $location.$$state = oldState; - - throw e; - } - } - - $rootElement.on('click', function(event) { - // TODO(vojta): rewrite link when opening in new tab/window (in legacy browser) - // currently we open nice url link and redirect then - - if (!html5Mode.rewriteLinks || event.ctrlKey || event.metaKey || event.shiftKey || event.which == 2 || event.button == 2) return; - - var elm = jqLite(event.target); - - // traverse the DOM up to find first A tag - while (nodeName_(elm[0]) !== 'a') { - // ignore rewriting if no A tag (reached root element, or no parent - removed from document) - if (elm[0] === $rootElement[0] || !(elm = elm.parent())[0]) return; - } - - var absHref = elm.prop('href'); - // get the actual href attribute - see - // http://msdn.microsoft.com/en-us/library/ie/dd347148(v=vs.85).aspx - var relHref = elm.attr('href') || elm.attr('xlink:href'); - - if (isObject(absHref) && absHref.toString() === '[object SVGAnimatedString]') { - // SVGAnimatedString.animVal should be identical to SVGAnimatedString.baseVal, unless during - // an animation. - absHref = urlResolve(absHref.animVal).href; - } - - // Ignore when url is started with javascript: or mailto: - if (IGNORE_URI_REGEXP.test(absHref)) return; - - if (absHref && !elm.attr('target') && !event.isDefaultPrevented()) { - if ($location.$$parseLinkUrl(absHref, relHref)) { - // We do a preventDefault for all urls that are part of the angular application, - // in html5mode and also without, so that we are able to abort navigation without - // getting double entries in the location history. - event.preventDefault(); - // update location manually - if ($location.absUrl() != $browser.url()) { - $rootScope.$apply(); - // hack to work around FF6 bug 684208 when scenario runner clicks on links - $window.angular['ff-684208-preventDefault'] = true; - } - } - } - }); - - - // rewrite hashbang url <> html5 url - if (trimEmptyHash($location.absUrl()) != trimEmptyHash(initialUrl)) { - $browser.url($location.absUrl(), true); - } - - var initializing = true; - - // update $location when $browser url changes - $browser.onUrlChange(function(newUrl, newState) { - - if (isUndefined(beginsWith(appBaseNoFile, newUrl))) { - // If we are navigating outside of the app then force a reload - $window.location.href = newUrl; - return; - } - - $rootScope.$evalAsync(function() { - var oldUrl = $location.absUrl(); - var oldState = $location.$$state; - var defaultPrevented; - - $location.$$parse(newUrl); - $location.$$state = newState; - - defaultPrevented = $rootScope.$broadcast('$locationChangeStart', newUrl, oldUrl, - newState, oldState).defaultPrevented; - - // if the location was changed by a `$locationChangeStart` handler then stop - // processing this location change - if ($location.absUrl() !== newUrl) return; - - if (defaultPrevented) { - $location.$$parse(oldUrl); - $location.$$state = oldState; - setBrowserUrlWithFallback(oldUrl, false, oldState); - } else { - initializing = false; - afterLocationChange(oldUrl, oldState); - } - }); - if (!$rootScope.$$phase) $rootScope.$digest(); - }); - - // update browser - $rootScope.$watch(function $locationWatch() { - var oldUrl = trimEmptyHash($browser.url()); - var newUrl = trimEmptyHash($location.absUrl()); - var oldState = $browser.state(); - var currentReplace = $location.$$replace; - var urlOrStateChanged = oldUrl !== newUrl || - ($location.$$html5 && $sniffer.history && oldState !== $location.$$state); - - if (initializing || urlOrStateChanged) { - initializing = false; - - $rootScope.$evalAsync(function() { - var newUrl = $location.absUrl(); - var defaultPrevented = $rootScope.$broadcast('$locationChangeStart', newUrl, oldUrl, - $location.$$state, oldState).defaultPrevented; - - // if the location was changed by a `$locationChangeStart` handler then stop - // processing this location change - if ($location.absUrl() !== newUrl) return; - - if (defaultPrevented) { - $location.$$parse(oldUrl); - $location.$$state = oldState; - } else { - if (urlOrStateChanged) { - setBrowserUrlWithFallback(newUrl, currentReplace, - oldState === $location.$$state ? null : $location.$$state); - } - afterLocationChange(oldUrl, oldState); - } - }); - } - - $location.$$replace = false; - - // we don't need to return anything because $evalAsync will make the digest loop dirty when - // there is a change - }); - - return $location; - - function afterLocationChange(oldUrl, oldState) { - $rootScope.$broadcast('$locationChangeSuccess', $location.absUrl(), oldUrl, - $location.$$state, oldState); - } -}]; -} - -/** - * @ngdoc service - * @name $log - * @requires $window - * - * @description - * Simple service for logging. Default implementation safely writes the message - * into the browser's console (if present). - * - * The main purpose of this service is to simplify debugging and troubleshooting. - * - * The default is to log `debug` messages. You can use - * {@link ng.$logProvider ng.$logProvider#debugEnabled} to change this. - * - * @example - - - angular.module('logExample', []) - .controller('LogController', ['$scope', '$log', function($scope, $log) { - $scope.$log = $log; - $scope.message = 'Hello World!'; - }]); - - -
    -

    Reload this page with open console, enter text and hit the log button...

    - - - - - - -
    -
    -
    - */ - -/** - * @ngdoc provider - * @name $logProvider - * @description - * Use the `$logProvider` to configure how the application logs messages - */ -function $LogProvider() { - var debug = true, - self = this; - - /** - * @ngdoc method - * @name $logProvider#debugEnabled - * @description - * @param {boolean=} flag enable or disable debug level messages - * @returns {*} current value if used as getter or itself (chaining) if used as setter - */ - this.debugEnabled = function(flag) { - if (isDefined(flag)) { - debug = flag; - return this; - } else { - return debug; - } - }; - - this.$get = ['$window', function($window) { - return { - /** - * @ngdoc method - * @name $log#log - * - * @description - * Write a log message - */ - log: consoleLog('log'), - - /** - * @ngdoc method - * @name $log#info - * - * @description - * Write an information message - */ - info: consoleLog('info'), - - /** - * @ngdoc method - * @name $log#warn - * - * @description - * Write a warning message - */ - warn: consoleLog('warn'), - - /** - * @ngdoc method - * @name $log#error - * - * @description - * Write an error message - */ - error: consoleLog('error'), - - /** - * @ngdoc method - * @name $log#debug - * - * @description - * Write a debug message - */ - debug: (function() { - var fn = consoleLog('debug'); - - return function() { - if (debug) { - fn.apply(self, arguments); - } - }; - }()) - }; - - function formatError(arg) { - if (arg instanceof Error) { - if (arg.stack) { - arg = (arg.message && arg.stack.indexOf(arg.message) === -1) - ? 'Error: ' + arg.message + '\n' + arg.stack - : arg.stack; - } else if (arg.sourceURL) { - arg = arg.message + '\n' + arg.sourceURL + ':' + arg.line; - } - } - return arg; - } - - function consoleLog(type) { - var console = $window.console || {}, - logFn = console[type] || console.log || noop, - hasApply = false; - - // Note: reading logFn.apply throws an error in IE11 in IE8 document mode. - // The reason behind this is that console.log has type "object" in IE8... - try { - hasApply = !!logFn.apply; - } catch (e) {} - - if (hasApply) { - return function() { - var args = []; - forEach(arguments, function(arg) { - args.push(formatError(arg)); - }); - return logFn.apply(console, args); - }; - } - - // we are IE which either doesn't have window.console => this is noop and we do nothing, - // or we are IE where console.log doesn't have apply so we log at least first 2 args - return function(arg1, arg2) { - logFn(arg1, arg2 == null ? '' : arg2); - }; - } - }]; -} - -/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * - * Any commits to this file should be reviewed with security in mind. * - * Changes to this file can potentially create security vulnerabilities. * - * An approval from 2 Core members with history of modifying * - * this file is required. * - * * - * Does the change somehow allow for arbitrary javascript to be executed? * - * Or allows for someone to change the prototype of built-in objects? * - * Or gives undesired access to variables likes document or window? * - * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ - -var $parseMinErr = minErr('$parse'); - -// Sandboxing Angular Expressions -// ------------------------------ -// Angular expressions are generally considered safe because these expressions only have direct -// access to `$scope` and locals. However, one can obtain the ability to execute arbitrary JS code by -// obtaining a reference to native JS functions such as the Function constructor. -// -// As an example, consider the following Angular expression: -// -// {}.toString.constructor('alert("evil JS code")') -// -// This sandboxing technique is not perfect and doesn't aim to be. The goal is to prevent exploits -// against the expression language, but not to prevent exploits that were enabled by exposing -// sensitive JavaScript or browser APIs on Scope. Exposing such objects on a Scope is never a good -// practice and therefore we are not even trying to protect against interaction with an object -// explicitly exposed in this way. -// -// In general, it is not possible to access a Window object from an angular expression unless a -// window or some DOM object that has a reference to window is published onto a Scope. -// Similarly we prevent invocations of function known to be dangerous, as well as assignments to -// native objects. -// -// See https://docs.angularjs.org/guide/security - - -function ensureSafeMemberName(name, fullExpression) { - if (name === "__defineGetter__" || name === "__defineSetter__" - || name === "__lookupGetter__" || name === "__lookupSetter__" - || name === "__proto__") { - throw $parseMinErr('isecfld', - 'Attempting to access a disallowed field in Angular expressions! ' - + 'Expression: {0}', fullExpression); - } - return name; -} - -function ensureSafeObject(obj, fullExpression) { - // nifty check if obj is Function that is fast and works across iframes and other contexts - if (obj) { - if (obj.constructor === obj) { - throw $parseMinErr('isecfn', - 'Referencing Function in Angular expressions is disallowed! Expression: {0}', - fullExpression); - } else if (// isWindow(obj) - obj.window === obj) { - throw $parseMinErr('isecwindow', - 'Referencing the Window in Angular expressions is disallowed! Expression: {0}', - fullExpression); - } else if (// isElement(obj) - obj.children && (obj.nodeName || (obj.prop && obj.attr && obj.find))) { - throw $parseMinErr('isecdom', - 'Referencing DOM nodes in Angular expressions is disallowed! Expression: {0}', - fullExpression); - } else if (// block Object so that we can't get hold of dangerous Object.* methods - obj === Object) { - throw $parseMinErr('isecobj', - 'Referencing Object in Angular expressions is disallowed! Expression: {0}', - fullExpression); - } - } - return obj; -} - -var CALL = Function.prototype.call; -var APPLY = Function.prototype.apply; -var BIND = Function.prototype.bind; - -function ensureSafeFunction(obj, fullExpression) { - if (obj) { - if (obj.constructor === obj) { - throw $parseMinErr('isecfn', - 'Referencing Function in Angular expressions is disallowed! Expression: {0}', - fullExpression); - } else if (obj === CALL || obj === APPLY || obj === BIND) { - throw $parseMinErr('isecff', - 'Referencing call, apply or bind in Angular expressions is disallowed! Expression: {0}', - fullExpression); - } - } -} - -var OPERATORS = createMap(); -forEach('+ - * / % === !== == != < > <= >= && || ! = |'.split(' '), function(operator) { OPERATORS[operator] = true; }); -var ESCAPE = {"n":"\n", "f":"\f", "r":"\r", "t":"\t", "v":"\v", "'":"'", '"':'"'}; - - -///////////////////////////////////////// - - -/** - * @constructor - */ -var Lexer = function(options) { - this.options = options; -}; - -Lexer.prototype = { - constructor: Lexer, - - lex: function(text) { - this.text = text; - this.index = 0; - this.tokens = []; - - while (this.index < this.text.length) { - var ch = this.text.charAt(this.index); - if (ch === '"' || ch === "'") { - this.readString(ch); - } else if (this.isNumber(ch) || ch === '.' && this.isNumber(this.peek())) { - this.readNumber(); - } else if (this.isIdent(ch)) { - this.readIdent(); - } else if (this.is(ch, '(){}[].,;:?')) { - this.tokens.push({index: this.index, text: ch}); - this.index++; - } else if (this.isWhitespace(ch)) { - this.index++; - } else { - var ch2 = ch + this.peek(); - var ch3 = ch2 + this.peek(2); - var op1 = OPERATORS[ch]; - var op2 = OPERATORS[ch2]; - var op3 = OPERATORS[ch3]; - if (op1 || op2 || op3) { - var token = op3 ? ch3 : (op2 ? ch2 : ch); - this.tokens.push({index: this.index, text: token, operator: true}); - this.index += token.length; - } else { - this.throwError('Unexpected next character ', this.index, this.index + 1); - } - } - } - return this.tokens; - }, - - is: function(ch, chars) { - return chars.indexOf(ch) !== -1; - }, - - peek: function(i) { - var num = i || 1; - return (this.index + num < this.text.length) ? this.text.charAt(this.index + num) : false; - }, - - isNumber: function(ch) { - return ('0' <= ch && ch <= '9') && typeof ch === "string"; - }, - - isWhitespace: function(ch) { - // IE treats non-breaking space as \u00A0 - return (ch === ' ' || ch === '\r' || ch === '\t' || - ch === '\n' || ch === '\v' || ch === '\u00A0'); - }, - - isIdent: function(ch) { - return ('a' <= ch && ch <= 'z' || - 'A' <= ch && ch <= 'Z' || - '_' === ch || ch === '$'); - }, - - isExpOperator: function(ch) { - return (ch === '-' || ch === '+' || this.isNumber(ch)); - }, - - throwError: function(error, start, end) { - end = end || this.index; - var colStr = (isDefined(start) - ? 's ' + start + '-' + this.index + ' [' + this.text.substring(start, end) + ']' - : ' ' + end); - throw $parseMinErr('lexerr', 'Lexer Error: {0} at column{1} in expression [{2}].', - error, colStr, this.text); - }, - - readNumber: function() { - var number = ''; - var start = this.index; - while (this.index < this.text.length) { - var ch = lowercase(this.text.charAt(this.index)); - if (ch == '.' || this.isNumber(ch)) { - number += ch; - } else { - var peekCh = this.peek(); - if (ch == 'e' && this.isExpOperator(peekCh)) { - number += ch; - } else if (this.isExpOperator(ch) && - peekCh && this.isNumber(peekCh) && - number.charAt(number.length - 1) == 'e') { - number += ch; - } else if (this.isExpOperator(ch) && - (!peekCh || !this.isNumber(peekCh)) && - number.charAt(number.length - 1) == 'e') { - this.throwError('Invalid exponent'); - } else { - break; - } - } - this.index++; - } - this.tokens.push({ - index: start, - text: number, - constant: true, - value: Number(number) - }); - }, - - readIdent: function() { - var start = this.index; - while (this.index < this.text.length) { - var ch = this.text.charAt(this.index); - if (!(this.isIdent(ch) || this.isNumber(ch))) { - break; - } - this.index++; - } - this.tokens.push({ - index: start, - text: this.text.slice(start, this.index), - identifier: true - }); - }, - - readString: function(quote) { - var start = this.index; - this.index++; - var string = ''; - var rawString = quote; - var escape = false; - while (this.index < this.text.length) { - var ch = this.text.charAt(this.index); - rawString += ch; - if (escape) { - if (ch === 'u') { - var hex = this.text.substring(this.index + 1, this.index + 5); - if (!hex.match(/[\da-f]{4}/i)) { - this.throwError('Invalid unicode escape [\\u' + hex + ']'); - } - this.index += 4; - string += String.fromCharCode(parseInt(hex, 16)); - } else { - var rep = ESCAPE[ch]; - string = string + (rep || ch); - } - escape = false; - } else if (ch === '\\') { - escape = true; - } else if (ch === quote) { - this.index++; - this.tokens.push({ - index: start, - text: rawString, - constant: true, - value: string - }); - return; - } else { - string += ch; - } - this.index++; - } - this.throwError('Unterminated quote', start); - } -}; - -var AST = function(lexer, options) { - this.lexer = lexer; - this.options = options; -}; - -AST.Program = 'Program'; -AST.ExpressionStatement = 'ExpressionStatement'; -AST.AssignmentExpression = 'AssignmentExpression'; -AST.ConditionalExpression = 'ConditionalExpression'; -AST.LogicalExpression = 'LogicalExpression'; -AST.BinaryExpression = 'BinaryExpression'; -AST.UnaryExpression = 'UnaryExpression'; -AST.CallExpression = 'CallExpression'; -AST.MemberExpression = 'MemberExpression'; -AST.Identifier = 'Identifier'; -AST.Literal = 'Literal'; -AST.ArrayExpression = 'ArrayExpression'; -AST.Property = 'Property'; -AST.ObjectExpression = 'ObjectExpression'; -AST.ThisExpression = 'ThisExpression'; - -// Internal use only -AST.NGValueParameter = 'NGValueParameter'; - -AST.prototype = { - ast: function(text) { - this.text = text; - this.tokens = this.lexer.lex(text); - - var value = this.program(); - - if (this.tokens.length !== 0) { - this.throwError('is an unexpected token', this.tokens[0]); - } - - return value; - }, - - program: function() { - var body = []; - while (true) { - if (this.tokens.length > 0 && !this.peek('}', ')', ';', ']')) - body.push(this.expressionStatement()); - if (!this.expect(';')) { - return { type: AST.Program, body: body}; - } - } - }, - - expressionStatement: function() { - return { type: AST.ExpressionStatement, expression: this.filterChain() }; - }, - - filterChain: function() { - var left = this.expression(); - var token; - while ((token = this.expect('|'))) { - left = this.filter(left); - } - return left; - }, - - expression: function() { - return this.assignment(); - }, - - assignment: function() { - var result = this.ternary(); - if (this.expect('=')) { - result = { type: AST.AssignmentExpression, left: result, right: this.assignment(), operator: '='}; - } - return result; - }, - - ternary: function() { - var test = this.logicalOR(); - var alternate; - var consequent; - if (this.expect('?')) { - alternate = this.expression(); - if (this.consume(':')) { - consequent = this.expression(); - return { type: AST.ConditionalExpression, test: test, alternate: alternate, consequent: consequent}; - } - } - return test; - }, - - logicalOR: function() { - var left = this.logicalAND(); - while (this.expect('||')) { - left = { type: AST.LogicalExpression, operator: '||', left: left, right: this.logicalAND() }; - } - return left; - }, - - logicalAND: function() { - var left = this.equality(); - while (this.expect('&&')) { - left = { type: AST.LogicalExpression, operator: '&&', left: left, right: this.equality()}; - } - return left; - }, - - equality: function() { - var left = this.relational(); - var token; - while ((token = this.expect('==','!=','===','!=='))) { - left = { type: AST.BinaryExpression, operator: token.text, left: left, right: this.relational() }; - } - return left; - }, - - relational: function() { - var left = this.additive(); - var token; - while ((token = this.expect('<', '>', '<=', '>='))) { - left = { type: AST.BinaryExpression, operator: token.text, left: left, right: this.additive() }; - } - return left; - }, - - additive: function() { - var left = this.multiplicative(); - var token; - while ((token = this.expect('+','-'))) { - left = { type: AST.BinaryExpression, operator: token.text, left: left, right: this.multiplicative() }; - } - return left; - }, - - multiplicative: function() { - var left = this.unary(); - var token; - while ((token = this.expect('*','/','%'))) { - left = { type: AST.BinaryExpression, operator: token.text, left: left, right: this.unary() }; - } - return left; - }, - - unary: function() { - var token; - if ((token = this.expect('+', '-', '!'))) { - return { type: AST.UnaryExpression, operator: token.text, prefix: true, argument: this.unary() }; - } else { - return this.primary(); - } - }, - - primary: function() { - var primary; - if (this.expect('(')) { - primary = this.filterChain(); - this.consume(')'); - } else if (this.expect('[')) { - primary = this.arrayDeclaration(); - } else if (this.expect('{')) { - primary = this.object(); - } else if (this.constants.hasOwnProperty(this.peek().text)) { - primary = copy(this.constants[this.consume().text]); - } else if (this.peek().identifier) { - primary = this.identifier(); - } else if (this.peek().constant) { - primary = this.constant(); - } else { - this.throwError('not a primary expression', this.peek()); - } - - var next; - while ((next = this.expect('(', '[', '.'))) { - if (next.text === '(') { - primary = {type: AST.CallExpression, callee: primary, arguments: this.parseArguments() }; - this.consume(')'); - } else if (next.text === '[') { - primary = { type: AST.MemberExpression, object: primary, property: this.expression(), computed: true }; - this.consume(']'); - } else if (next.text === '.') { - primary = { type: AST.MemberExpression, object: primary, property: this.identifier(), computed: false }; - } else { - this.throwError('IMPOSSIBLE'); - } - } - return primary; - }, - - filter: function(baseExpression) { - var args = [baseExpression]; - var result = {type: AST.CallExpression, callee: this.identifier(), arguments: args, filter: true}; - - while (this.expect(':')) { - args.push(this.expression()); - } - - return result; - }, - - parseArguments: function() { - var args = []; - if (this.peekToken().text !== ')') { - do { - args.push(this.expression()); - } while (this.expect(',')); - } - return args; - }, - - identifier: function() { - var token = this.consume(); - if (!token.identifier) { - this.throwError('is not a valid identifier', token); - } - return { type: AST.Identifier, name: token.text }; - }, - - constant: function() { - // TODO check that it is a constant - return { type: AST.Literal, value: this.consume().value }; - }, - - arrayDeclaration: function() { - var elements = []; - if (this.peekToken().text !== ']') { - do { - if (this.peek(']')) { - // Support trailing commas per ES5.1. - break; - } - elements.push(this.expression()); - } while (this.expect(',')); - } - this.consume(']'); - - return { type: AST.ArrayExpression, elements: elements }; - }, - - object: function() { - var properties = [], property; - if (this.peekToken().text !== '}') { - do { - if (this.peek('}')) { - // Support trailing commas per ES5.1. - break; - } - property = {type: AST.Property, kind: 'init'}; - if (this.peek().constant) { - property.key = this.constant(); - } else if (this.peek().identifier) { - property.key = this.identifier(); - } else { - this.throwError("invalid key", this.peek()); - } - this.consume(':'); - property.value = this.expression(); - properties.push(property); - } while (this.expect(',')); - } - this.consume('}'); - - return {type: AST.ObjectExpression, properties: properties }; - }, - - throwError: function(msg, token) { - throw $parseMinErr('syntax', - 'Syntax Error: Token \'{0}\' {1} at column {2} of the expression [{3}] starting at [{4}].', - token.text, msg, (token.index + 1), this.text, this.text.substring(token.index)); - }, - - consume: function(e1) { - if (this.tokens.length === 0) { - throw $parseMinErr('ueoe', 'Unexpected end of expression: {0}', this.text); - } - - var token = this.expect(e1); - if (!token) { - this.throwError('is unexpected, expecting [' + e1 + ']', this.peek()); - } - return token; - }, - - peekToken: function() { - if (this.tokens.length === 0) { - throw $parseMinErr('ueoe', 'Unexpected end of expression: {0}', this.text); - } - return this.tokens[0]; - }, - - peek: function(e1, e2, e3, e4) { - return this.peekAhead(0, e1, e2, e3, e4); - }, - - peekAhead: function(i, e1, e2, e3, e4) { - if (this.tokens.length > i) { - var token = this.tokens[i]; - var t = token.text; - if (t === e1 || t === e2 || t === e3 || t === e4 || - (!e1 && !e2 && !e3 && !e4)) { - return token; - } - } - return false; - }, - - expect: function(e1, e2, e3, e4) { - var token = this.peek(e1, e2, e3, e4); - if (token) { - this.tokens.shift(); - return token; - } - return false; - }, - - - /* `undefined` is not a constant, it is an identifier, - * but using it as an identifier is not supported - */ - constants: { - 'true': { type: AST.Literal, value: true }, - 'false': { type: AST.Literal, value: false }, - 'null': { type: AST.Literal, value: null }, - 'undefined': {type: AST.Literal, value: undefined }, - 'this': {type: AST.ThisExpression } - } -}; - -function ifDefined(v, d) { - return typeof v !== 'undefined' ? v : d; -} - -function plusFn(l, r) { - if (typeof l === 'undefined') return r; - if (typeof r === 'undefined') return l; - return l + r; -} - -function isStateless($filter, filterName) { - var fn = $filter(filterName); - return !fn.$stateful; -} - -function findConstantAndWatchExpressions(ast, $filter) { - var allConstants; - var argsToWatch; - switch (ast.type) { - case AST.Program: - allConstants = true; - forEach(ast.body, function(expr) { - findConstantAndWatchExpressions(expr.expression, $filter); - allConstants = allConstants && expr.expression.constant; - }); - ast.constant = allConstants; - break; - case AST.Literal: - ast.constant = true; - ast.toWatch = []; - break; - case AST.UnaryExpression: - findConstantAndWatchExpressions(ast.argument, $filter); - ast.constant = ast.argument.constant; - ast.toWatch = ast.argument.toWatch; - break; - case AST.BinaryExpression: - findConstantAndWatchExpressions(ast.left, $filter); - findConstantAndWatchExpressions(ast.right, $filter); - ast.constant = ast.left.constant && ast.right.constant; - ast.toWatch = ast.left.toWatch.concat(ast.right.toWatch); - break; - case AST.LogicalExpression: - findConstantAndWatchExpressions(ast.left, $filter); - findConstantAndWatchExpressions(ast.right, $filter); - ast.constant = ast.left.constant && ast.right.constant; - ast.toWatch = ast.constant ? [] : [ast]; - break; - case AST.ConditionalExpression: - findConstantAndWatchExpressions(ast.test, $filter); - findConstantAndWatchExpressions(ast.alternate, $filter); - findConstantAndWatchExpressions(ast.consequent, $filter); - ast.constant = ast.test.constant && ast.alternate.constant && ast.consequent.constant; - ast.toWatch = ast.constant ? [] : [ast]; - break; - case AST.Identifier: - ast.constant = false; - ast.toWatch = [ast]; - break; - case AST.MemberExpression: - findConstantAndWatchExpressions(ast.object, $filter); - if (ast.computed) { - findConstantAndWatchExpressions(ast.property, $filter); - } - ast.constant = ast.object.constant && (!ast.computed || ast.property.constant); - ast.toWatch = [ast]; - break; - case AST.CallExpression: - allConstants = ast.filter ? isStateless($filter, ast.callee.name) : false; - argsToWatch = []; - forEach(ast.arguments, function(expr) { - findConstantAndWatchExpressions(expr, $filter); - allConstants = allConstants && expr.constant; - if (!expr.constant) { - argsToWatch.push.apply(argsToWatch, expr.toWatch); - } - }); - ast.constant = allConstants; - ast.toWatch = ast.filter && isStateless($filter, ast.callee.name) ? argsToWatch : [ast]; - break; - case AST.AssignmentExpression: - findConstantAndWatchExpressions(ast.left, $filter); - findConstantAndWatchExpressions(ast.right, $filter); - ast.constant = ast.left.constant && ast.right.constant; - ast.toWatch = [ast]; - break; - case AST.ArrayExpression: - allConstants = true; - argsToWatch = []; - forEach(ast.elements, function(expr) { - findConstantAndWatchExpressions(expr, $filter); - allConstants = allConstants && expr.constant; - if (!expr.constant) { - argsToWatch.push.apply(argsToWatch, expr.toWatch); - } - }); - ast.constant = allConstants; - ast.toWatch = argsToWatch; - break; - case AST.ObjectExpression: - allConstants = true; - argsToWatch = []; - forEach(ast.properties, function(property) { - findConstantAndWatchExpressions(property.value, $filter); - allConstants = allConstants && property.value.constant; - if (!property.value.constant) { - argsToWatch.push.apply(argsToWatch, property.value.toWatch); - } - }); - ast.constant = allConstants; - ast.toWatch = argsToWatch; - break; - case AST.ThisExpression: - ast.constant = false; - ast.toWatch = []; - break; - } -} - -function getInputs(body) { - if (body.length != 1) return; - var lastExpression = body[0].expression; - var candidate = lastExpression.toWatch; - if (candidate.length !== 1) return candidate; - return candidate[0] !== lastExpression ? candidate : undefined; -} - -function isAssignable(ast) { - return ast.type === AST.Identifier || ast.type === AST.MemberExpression; -} - -function assignableAST(ast) { - if (ast.body.length === 1 && isAssignable(ast.body[0].expression)) { - return {type: AST.AssignmentExpression, left: ast.body[0].expression, right: {type: AST.NGValueParameter}, operator: '='}; - } -} - -function isLiteral(ast) { - return ast.body.length === 0 || - ast.body.length === 1 && ( - ast.body[0].expression.type === AST.Literal || - ast.body[0].expression.type === AST.ArrayExpression || - ast.body[0].expression.type === AST.ObjectExpression); -} - -function isConstant(ast) { - return ast.constant; -} - -function ASTCompiler(astBuilder, $filter) { - this.astBuilder = astBuilder; - this.$filter = $filter; -} - -ASTCompiler.prototype = { - compile: function(expression, expensiveChecks) { - var self = this; - var ast = this.astBuilder.ast(expression); - this.state = { - nextId: 0, - filters: {}, - expensiveChecks: expensiveChecks, - fn: {vars: [], body: [], own: {}}, - assign: {vars: [], body: [], own: {}}, - inputs: [] - }; - findConstantAndWatchExpressions(ast, self.$filter); - var extra = ''; - var assignable; - this.stage = 'assign'; - if ((assignable = assignableAST(ast))) { - this.state.computing = 'assign'; - var result = this.nextId(); - this.recurse(assignable, result); - extra = 'fn.assign=' + this.generateFunction('assign', 's,v,l'); - } - var toWatch = getInputs(ast.body); - self.stage = 'inputs'; - forEach(toWatch, function(watch, key) { - var fnKey = 'fn' + key; - self.state[fnKey] = {vars: [], body: [], own: {}}; - self.state.computing = fnKey; - var intoId = self.nextId(); - self.recurse(watch, intoId); - self.return_(intoId); - self.state.inputs.push(fnKey); - watch.watchId = key; - }); - this.state.computing = 'fn'; - this.stage = 'main'; - this.recurse(ast); - var fnString = - // The build and minification steps remove the string "use strict" from the code, but this is done using a regex. - // This is a workaround for this until we do a better job at only removing the prefix only when we should. - '"' + this.USE + ' ' + this.STRICT + '";\n' + - this.filterPrefix() + - 'var fn=' + this.generateFunction('fn', 's,l,a,i') + - extra + - this.watchFns() + - 'return fn;'; - - /* jshint -W054 */ - var fn = (new Function('$filter', - 'ensureSafeMemberName', - 'ensureSafeObject', - 'ensureSafeFunction', - 'ifDefined', - 'plus', - 'text', - fnString))( - this.$filter, - ensureSafeMemberName, - ensureSafeObject, - ensureSafeFunction, - ifDefined, - plusFn, - expression); - /* jshint +W054 */ - this.state = this.stage = undefined; - fn.literal = isLiteral(ast); - fn.constant = isConstant(ast); - return fn; - }, - - USE: 'use', - - STRICT: 'strict', - - watchFns: function() { - var result = []; - var fns = this.state.inputs; - var self = this; - forEach(fns, function(name) { - result.push('var ' + name + '=' + self.generateFunction(name, 's')); - }); - if (fns.length) { - result.push('fn.inputs=[' + fns.join(',') + '];'); - } - return result.join(''); - }, - - generateFunction: function(name, params) { - return 'function(' + params + '){' + - this.varsPrefix(name) + - this.body(name) + - '};'; - }, - - filterPrefix: function() { - var parts = []; - var self = this; - forEach(this.state.filters, function(id, filter) { - parts.push(id + '=$filter(' + self.escape(filter) + ')'); - }); - if (parts.length) return 'var ' + parts.join(',') + ';'; - return ''; - }, - - varsPrefix: function(section) { - return this.state[section].vars.length ? 'var ' + this.state[section].vars.join(',') + ';' : ''; - }, - - body: function(section) { - return this.state[section].body.join(''); - }, - - recurse: function(ast, intoId, nameId, recursionFn, create, skipWatchIdCheck) { - var left, right, self = this, args, expression; - recursionFn = recursionFn || noop; - if (!skipWatchIdCheck && isDefined(ast.watchId)) { - intoId = intoId || this.nextId(); - this.if_('i', - this.lazyAssign(intoId, this.computedMember('i', ast.watchId)), - this.lazyRecurse(ast, intoId, nameId, recursionFn, create, true) - ); - return; - } - switch (ast.type) { - case AST.Program: - forEach(ast.body, function(expression, pos) { - self.recurse(expression.expression, undefined, undefined, function(expr) { right = expr; }); - if (pos !== ast.body.length - 1) { - self.current().body.push(right, ';'); - } else { - self.return_(right); - } - }); - break; - case AST.Literal: - expression = this.escape(ast.value); - this.assign(intoId, expression); - recursionFn(expression); - break; - case AST.UnaryExpression: - this.recurse(ast.argument, undefined, undefined, function(expr) { right = expr; }); - expression = ast.operator + '(' + this.ifDefined(right, 0) + ')'; - this.assign(intoId, expression); - recursionFn(expression); - break; - case AST.BinaryExpression: - this.recurse(ast.left, undefined, undefined, function(expr) { left = expr; }); - this.recurse(ast.right, undefined, undefined, function(expr) { right = expr; }); - if (ast.operator === '+') { - expression = this.plus(left, right); - } else if (ast.operator === '-') { - expression = this.ifDefined(left, 0) + ast.operator + this.ifDefined(right, 0); - } else { - expression = '(' + left + ')' + ast.operator + '(' + right + ')'; - } - this.assign(intoId, expression); - recursionFn(expression); - break; - case AST.LogicalExpression: - intoId = intoId || this.nextId(); - self.recurse(ast.left, intoId); - self.if_(ast.operator === '&&' ? intoId : self.not(intoId), self.lazyRecurse(ast.right, intoId)); - recursionFn(intoId); - break; - case AST.ConditionalExpression: - intoId = intoId || this.nextId(); - self.recurse(ast.test, intoId); - self.if_(intoId, self.lazyRecurse(ast.alternate, intoId), self.lazyRecurse(ast.consequent, intoId)); - recursionFn(intoId); - break; - case AST.Identifier: - intoId = intoId || this.nextId(); - if (nameId) { - nameId.context = self.stage === 'inputs' ? 's' : this.assign(this.nextId(), this.getHasOwnProperty('l', ast.name) + '?l:s'); - nameId.computed = false; - nameId.name = ast.name; - } - ensureSafeMemberName(ast.name); - self.if_(self.stage === 'inputs' || self.not(self.getHasOwnProperty('l', ast.name)), - function() { - self.if_(self.stage === 'inputs' || 's', function() { - if (create && create !== 1) { - self.if_( - self.not(self.nonComputedMember('s', ast.name)), - self.lazyAssign(self.nonComputedMember('s', ast.name), '{}')); - } - self.assign(intoId, self.nonComputedMember('s', ast.name)); - }); - }, intoId && self.lazyAssign(intoId, self.nonComputedMember('l', ast.name)) - ); - if (self.state.expensiveChecks || isPossiblyDangerousMemberName(ast.name)) { - self.addEnsureSafeObject(intoId); - } - recursionFn(intoId); - break; - case AST.MemberExpression: - left = nameId && (nameId.context = this.nextId()) || this.nextId(); - intoId = intoId || this.nextId(); - self.recurse(ast.object, left, undefined, function() { - self.if_(self.notNull(left), function() { - if (ast.computed) { - right = self.nextId(); - self.recurse(ast.property, right); - self.addEnsureSafeMemberName(right); - if (create && create !== 1) { - self.if_(self.not(self.computedMember(left, right)), self.lazyAssign(self.computedMember(left, right), '{}')); - } - expression = self.ensureSafeObject(self.computedMember(left, right)); - self.assign(intoId, expression); - if (nameId) { - nameId.computed = true; - nameId.name = right; - } - } else { - ensureSafeMemberName(ast.property.name); - if (create && create !== 1) { - self.if_(self.not(self.nonComputedMember(left, ast.property.name)), self.lazyAssign(self.nonComputedMember(left, ast.property.name), '{}')); - } - expression = self.nonComputedMember(left, ast.property.name); - if (self.state.expensiveChecks || isPossiblyDangerousMemberName(ast.property.name)) { - expression = self.ensureSafeObject(expression); - } - self.assign(intoId, expression); - if (nameId) { - nameId.computed = false; - nameId.name = ast.property.name; - } - } - }, function() { - self.assign(intoId, 'undefined'); - }); - recursionFn(intoId); - }, !!create); - break; - case AST.CallExpression: - intoId = intoId || this.nextId(); - if (ast.filter) { - right = self.filter(ast.callee.name); - args = []; - forEach(ast.arguments, function(expr) { - var argument = self.nextId(); - self.recurse(expr, argument); - args.push(argument); - }); - expression = right + '(' + args.join(',') + ')'; - self.assign(intoId, expression); - recursionFn(intoId); - } else { - right = self.nextId(); - left = {}; - args = []; - self.recurse(ast.callee, right, left, function() { - self.if_(self.notNull(right), function() { - self.addEnsureSafeFunction(right); - forEach(ast.arguments, function(expr) { - self.recurse(expr, self.nextId(), undefined, function(argument) { - args.push(self.ensureSafeObject(argument)); - }); - }); - if (left.name) { - if (!self.state.expensiveChecks) { - self.addEnsureSafeObject(left.context); - } - expression = self.member(left.context, left.name, left.computed) + '(' + args.join(',') + ')'; - } else { - expression = right + '(' + args.join(',') + ')'; - } - expression = self.ensureSafeObject(expression); - self.assign(intoId, expression); - }, function() { - self.assign(intoId, 'undefined'); - }); - recursionFn(intoId); - }); - } - break; - case AST.AssignmentExpression: - right = this.nextId(); - left = {}; - if (!isAssignable(ast.left)) { - throw $parseMinErr('lval', 'Trying to assing a value to a non l-value'); - } - this.recurse(ast.left, undefined, left, function() { - self.if_(self.notNull(left.context), function() { - self.recurse(ast.right, right); - self.addEnsureSafeObject(self.member(left.context, left.name, left.computed)); - expression = self.member(left.context, left.name, left.computed) + ast.operator + right; - self.assign(intoId, expression); - recursionFn(intoId || expression); - }); - }, 1); - break; - case AST.ArrayExpression: - args = []; - forEach(ast.elements, function(expr) { - self.recurse(expr, self.nextId(), undefined, function(argument) { - args.push(argument); - }); - }); - expression = '[' + args.join(',') + ']'; - this.assign(intoId, expression); - recursionFn(expression); - break; - case AST.ObjectExpression: - args = []; - forEach(ast.properties, function(property) { - self.recurse(property.value, self.nextId(), undefined, function(expr) { - args.push(self.escape( - property.key.type === AST.Identifier ? property.key.name : - ('' + property.key.value)) + - ':' + expr); - }); - }); - expression = '{' + args.join(',') + '}'; - this.assign(intoId, expression); - recursionFn(expression); - break; - case AST.ThisExpression: - this.assign(intoId, 's'); - recursionFn('s'); - break; - case AST.NGValueParameter: - this.assign(intoId, 'v'); - recursionFn('v'); - break; - } - }, - - getHasOwnProperty: function(element, property) { - var key = element + '.' + property; - var own = this.current().own; - if (!own.hasOwnProperty(key)) { - own[key] = this.nextId(false, element + '&&(' + this.escape(property) + ' in ' + element + ')'); - } - return own[key]; - }, - - assign: function(id, value) { - if (!id) return; - this.current().body.push(id, '=', value, ';'); - return id; - }, - - filter: function(filterName) { - if (!this.state.filters.hasOwnProperty(filterName)) { - this.state.filters[filterName] = this.nextId(true); - } - return this.state.filters[filterName]; - }, - - ifDefined: function(id, defaultValue) { - return 'ifDefined(' + id + ',' + this.escape(defaultValue) + ')'; - }, - - plus: function(left, right) { - return 'plus(' + left + ',' + right + ')'; - }, - - return_: function(id) { - this.current().body.push('return ', id, ';'); - }, - - if_: function(test, alternate, consequent) { - if (test === true) { - alternate(); - } else { - var body = this.current().body; - body.push('if(', test, '){'); - alternate(); - body.push('}'); - if (consequent) { - body.push('else{'); - consequent(); - body.push('}'); - } - } - }, - - not: function(expression) { - return '!(' + expression + ')'; - }, - - notNull: function(expression) { - return expression + '!=null'; - }, - - nonComputedMember: function(left, right) { - return left + '.' + right; - }, - - computedMember: function(left, right) { - return left + '[' + right + ']'; - }, - - member: function(left, right, computed) { - if (computed) return this.computedMember(left, right); - return this.nonComputedMember(left, right); - }, - - addEnsureSafeObject: function(item) { - this.current().body.push(this.ensureSafeObject(item), ';'); - }, - - addEnsureSafeMemberName: function(item) { - this.current().body.push(this.ensureSafeMemberName(item), ';'); - }, - - addEnsureSafeFunction: function(item) { - this.current().body.push(this.ensureSafeFunction(item), ';'); - }, - - ensureSafeObject: function(item) { - return 'ensureSafeObject(' + item + ',text)'; - }, - - ensureSafeMemberName: function(item) { - return 'ensureSafeMemberName(' + item + ',text)'; - }, - - ensureSafeFunction: function(item) { - return 'ensureSafeFunction(' + item + ',text)'; - }, - - lazyRecurse: function(ast, intoId, nameId, recursionFn, create, skipWatchIdCheck) { - var self = this; - return function() { - self.recurse(ast, intoId, nameId, recursionFn, create, skipWatchIdCheck); - }; - }, - - lazyAssign: function(id, value) { - var self = this; - return function() { - self.assign(id, value); - }; - }, - - stringEscapeRegex: /[^ a-zA-Z0-9]/g, - - stringEscapeFn: function(c) { - return '\\u' + ('0000' + c.charCodeAt(0).toString(16)).slice(-4); - }, - - escape: function(value) { - if (isString(value)) return "'" + value.replace(this.stringEscapeRegex, this.stringEscapeFn) + "'"; - if (isNumber(value)) return value.toString(); - if (value === true) return 'true'; - if (value === false) return 'false'; - if (value === null) return 'null'; - if (typeof value === 'undefined') return 'undefined'; - - throw $parseMinErr('esc', 'IMPOSSIBLE'); - }, - - nextId: function(skip, init) { - var id = 'v' + (this.state.nextId++); - if (!skip) { - this.current().vars.push(id + (init ? '=' + init : '')); - } - return id; - }, - - current: function() { - return this.state[this.state.computing]; - } -}; - - -function ASTInterpreter(astBuilder, $filter) { - this.astBuilder = astBuilder; - this.$filter = $filter; -} - -ASTInterpreter.prototype = { - compile: function(expression, expensiveChecks) { - var self = this; - var ast = this.astBuilder.ast(expression); - this.expression = expression; - this.expensiveChecks = expensiveChecks; - findConstantAndWatchExpressions(ast, self.$filter); - var assignable; - var assign; - if ((assignable = assignableAST(ast))) { - assign = this.recurse(assignable); - } - var toWatch = getInputs(ast.body); - var inputs; - if (toWatch) { - inputs = []; - forEach(toWatch, function(watch, key) { - var input = self.recurse(watch); - watch.input = input; - inputs.push(input); - watch.watchId = key; - }); - } - var expressions = []; - forEach(ast.body, function(expression) { - expressions.push(self.recurse(expression.expression)); - }); - var fn = ast.body.length === 0 ? function() {} : - ast.body.length === 1 ? expressions[0] : - function(scope, locals) { - var lastValue; - forEach(expressions, function(exp) { - lastValue = exp(scope, locals); - }); - return lastValue; - }; - if (assign) { - fn.assign = function(scope, value, locals) { - return assign(scope, locals, value); - }; - } - if (inputs) { - fn.inputs = inputs; - } - fn.literal = isLiteral(ast); - fn.constant = isConstant(ast); - return fn; - }, - - recurse: function(ast, context, create) { - var left, right, self = this, args, expression; - if (ast.input) { - return this.inputs(ast.input, ast.watchId); - } - switch (ast.type) { - case AST.Literal: - return this.value(ast.value, context); - case AST.UnaryExpression: - right = this.recurse(ast.argument); - return this['unary' + ast.operator](right, context); - case AST.BinaryExpression: - left = this.recurse(ast.left); - right = this.recurse(ast.right); - return this['binary' + ast.operator](left, right, context); - case AST.LogicalExpression: - left = this.recurse(ast.left); - right = this.recurse(ast.right); - return this['binary' + ast.operator](left, right, context); - case AST.ConditionalExpression: - return this['ternary?:']( - this.recurse(ast.test), - this.recurse(ast.alternate), - this.recurse(ast.consequent), - context - ); - case AST.Identifier: - ensureSafeMemberName(ast.name, self.expression); - return self.identifier(ast.name, - self.expensiveChecks || isPossiblyDangerousMemberName(ast.name), - context, create, self.expression); - case AST.MemberExpression: - left = this.recurse(ast.object, false, !!create); - if (!ast.computed) { - ensureSafeMemberName(ast.property.name, self.expression); - right = ast.property.name; - } - if (ast.computed) right = this.recurse(ast.property); - return ast.computed ? - this.computedMember(left, right, context, create, self.expression) : - this.nonComputedMember(left, right, self.expensiveChecks, context, create, self.expression); - case AST.CallExpression: - args = []; - forEach(ast.arguments, function(expr) { - args.push(self.recurse(expr)); - }); - if (ast.filter) right = this.$filter(ast.callee.name); - if (!ast.filter) right = this.recurse(ast.callee, true); - return ast.filter ? - function(scope, locals, assign, inputs) { - var values = []; - for (var i = 0; i < args.length; ++i) { - values.push(args[i](scope, locals, assign, inputs)); - } - var value = right.apply(undefined, values, inputs); - return context ? {context: undefined, name: undefined, value: value} : value; - } : - function(scope, locals, assign, inputs) { - var rhs = right(scope, locals, assign, inputs); - var value; - if (rhs.value != null) { - ensureSafeObject(rhs.context, self.expression); - ensureSafeFunction(rhs.value, self.expression); - var values = []; - for (var i = 0; i < args.length; ++i) { - values.push(ensureSafeObject(args[i](scope, locals, assign, inputs), self.expression)); - } - value = ensureSafeObject(rhs.value.apply(rhs.context, values), self.expression); - } - return context ? {value: value} : value; - }; - case AST.AssignmentExpression: - left = this.recurse(ast.left, true, 1); - right = this.recurse(ast.right); - return function(scope, locals, assign, inputs) { - var lhs = left(scope, locals, assign, inputs); - var rhs = right(scope, locals, assign, inputs); - ensureSafeObject(lhs.value, self.expression); - lhs.context[lhs.name] = rhs; - return context ? {value: rhs} : rhs; - }; - case AST.ArrayExpression: - args = []; - forEach(ast.elements, function(expr) { - args.push(self.recurse(expr)); - }); - return function(scope, locals, assign, inputs) { - var value = []; - for (var i = 0; i < args.length; ++i) { - value.push(args[i](scope, locals, assign, inputs)); - } - return context ? {value: value} : value; - }; - case AST.ObjectExpression: - args = []; - forEach(ast.properties, function(property) { - args.push({key: property.key.type === AST.Identifier ? - property.key.name : - ('' + property.key.value), - value: self.recurse(property.value) - }); - }); - return function(scope, locals, assign, inputs) { - var value = {}; - for (var i = 0; i < args.length; ++i) { - value[args[i].key] = args[i].value(scope, locals, assign, inputs); - } - return context ? {value: value} : value; - }; - case AST.ThisExpression: - return function(scope) { - return context ? {value: scope} : scope; - }; - case AST.NGValueParameter: - return function(scope, locals, assign, inputs) { - return context ? {value: assign} : assign; - }; - } - }, - - 'unary+': function(argument, context) { - return function(scope, locals, assign, inputs) { - var arg = argument(scope, locals, assign, inputs); - if (isDefined(arg)) { - arg = +arg; - } else { - arg = 0; - } - return context ? {value: arg} : arg; - }; - }, - 'unary-': function(argument, context) { - return function(scope, locals, assign, inputs) { - var arg = argument(scope, locals, assign, inputs); - if (isDefined(arg)) { - arg = -arg; - } else { - arg = 0; - } - return context ? {value: arg} : arg; - }; - }, - 'unary!': function(argument, context) { - return function(scope, locals, assign, inputs) { - var arg = !argument(scope, locals, assign, inputs); - return context ? {value: arg} : arg; - }; - }, - 'binary+': function(left, right, context) { - return function(scope, locals, assign, inputs) { - var lhs = left(scope, locals, assign, inputs); - var rhs = right(scope, locals, assign, inputs); - var arg = plusFn(lhs, rhs); - return context ? {value: arg} : arg; - }; - }, - 'binary-': function(left, right, context) { - return function(scope, locals, assign, inputs) { - var lhs = left(scope, locals, assign, inputs); - var rhs = right(scope, locals, assign, inputs); - var arg = (isDefined(lhs) ? lhs : 0) - (isDefined(rhs) ? rhs : 0); - return context ? {value: arg} : arg; - }; - }, - 'binary*': function(left, right, context) { - return function(scope, locals, assign, inputs) { - var arg = left(scope, locals, assign, inputs) * right(scope, locals, assign, inputs); - return context ? {value: arg} : arg; - }; - }, - 'binary/': function(left, right, context) { - return function(scope, locals, assign, inputs) { - var arg = left(scope, locals, assign, inputs) / right(scope, locals, assign, inputs); - return context ? {value: arg} : arg; - }; - }, - 'binary%': function(left, right, context) { - return function(scope, locals, assign, inputs) { - var arg = left(scope, locals, assign, inputs) % right(scope, locals, assign, inputs); - return context ? {value: arg} : arg; - }; - }, - 'binary===': function(left, right, context) { - return function(scope, locals, assign, inputs) { - var arg = left(scope, locals, assign, inputs) === right(scope, locals, assign, inputs); - return context ? {value: arg} : arg; - }; - }, - 'binary!==': function(left, right, context) { - return function(scope, locals, assign, inputs) { - var arg = left(scope, locals, assign, inputs) !== right(scope, locals, assign, inputs); - return context ? {value: arg} : arg; - }; - }, - 'binary==': function(left, right, context) { - return function(scope, locals, assign, inputs) { - var arg = left(scope, locals, assign, inputs) == right(scope, locals, assign, inputs); - return context ? {value: arg} : arg; - }; - }, - 'binary!=': function(left, right, context) { - return function(scope, locals, assign, inputs) { - var arg = left(scope, locals, assign, inputs) != right(scope, locals, assign, inputs); - return context ? {value: arg} : arg; - }; - }, - 'binary<': function(left, right, context) { - return function(scope, locals, assign, inputs) { - var arg = left(scope, locals, assign, inputs) < right(scope, locals, assign, inputs); - return context ? {value: arg} : arg; - }; - }, - 'binary>': function(left, right, context) { - return function(scope, locals, assign, inputs) { - var arg = left(scope, locals, assign, inputs) > right(scope, locals, assign, inputs); - return context ? {value: arg} : arg; - }; - }, - 'binary<=': function(left, right, context) { - return function(scope, locals, assign, inputs) { - var arg = left(scope, locals, assign, inputs) <= right(scope, locals, assign, inputs); - return context ? {value: arg} : arg; - }; - }, - 'binary>=': function(left, right, context) { - return function(scope, locals, assign, inputs) { - var arg = left(scope, locals, assign, inputs) >= right(scope, locals, assign, inputs); - return context ? {value: arg} : arg; - }; - }, - 'binary&&': function(left, right, context) { - return function(scope, locals, assign, inputs) { - var arg = left(scope, locals, assign, inputs) && right(scope, locals, assign, inputs); - return context ? {value: arg} : arg; - }; - }, - 'binary||': function(left, right, context) { - return function(scope, locals, assign, inputs) { - var arg = left(scope, locals, assign, inputs) || right(scope, locals, assign, inputs); - return context ? {value: arg} : arg; - }; - }, - 'ternary?:': function(test, alternate, consequent, context) { - return function(scope, locals, assign, inputs) { - var arg = test(scope, locals, assign, inputs) ? alternate(scope, locals, assign, inputs) : consequent(scope, locals, assign, inputs); - return context ? {value: arg} : arg; - }; - }, - value: function(value, context) { - return function() { return context ? {context: undefined, name: undefined, value: value} : value; }; - }, - identifier: function(name, expensiveChecks, context, create, expression) { - return function(scope, locals, assign, inputs) { - var base = locals && (name in locals) ? locals : scope; - if (create && create !== 1 && base && !(base[name])) { - base[name] = {}; - } - var value = base ? base[name] : undefined; - if (expensiveChecks) { - ensureSafeObject(value, expression); - } - if (context) { - return {context: base, name: name, value: value}; - } else { - return value; - } - }; - }, - computedMember: function(left, right, context, create, expression) { - return function(scope, locals, assign, inputs) { - var lhs = left(scope, locals, assign, inputs); - var rhs; - var value; - if (lhs != null) { - rhs = right(scope, locals, assign, inputs); - ensureSafeMemberName(rhs, expression); - if (create && create !== 1 && lhs && !(lhs[rhs])) { - lhs[rhs] = {}; - } - value = lhs[rhs]; - ensureSafeObject(value, expression); - } - if (context) { - return {context: lhs, name: rhs, value: value}; - } else { - return value; - } - }; - }, - nonComputedMember: function(left, right, expensiveChecks, context, create, expression) { - return function(scope, locals, assign, inputs) { - var lhs = left(scope, locals, assign, inputs); - if (create && create !== 1 && lhs && !(lhs[right])) { - lhs[right] = {}; - } - var value = lhs != null ? lhs[right] : undefined; - if (expensiveChecks || isPossiblyDangerousMemberName(right)) { - ensureSafeObject(value, expression); - } - if (context) { - return {context: lhs, name: right, value: value}; - } else { - return value; - } - }; - }, - inputs: function(input, watchId) { - return function(scope, value, locals, inputs) { - if (inputs) return inputs[watchId]; - return input(scope, value, locals); - }; - } -}; - -/** - * @constructor - */ -var Parser = function(lexer, $filter, options) { - this.lexer = lexer; - this.$filter = $filter; - this.options = options; - this.ast = new AST(this.lexer); - this.astCompiler = options.csp ? new ASTInterpreter(this.ast, $filter) : - new ASTCompiler(this.ast, $filter); -}; - -Parser.prototype = { - constructor: Parser, - - parse: function(text) { - return this.astCompiler.compile(text, this.options.expensiveChecks); - } -}; - -var getterFnCacheDefault = createMap(); -var getterFnCacheExpensive = createMap(); - -function isPossiblyDangerousMemberName(name) { - return name == 'constructor'; -} - -var objectValueOf = Object.prototype.valueOf; - -function getValueOf(value) { - return isFunction(value.valueOf) ? value.valueOf() : objectValueOf.call(value); -} - -/////////////////////////////////// - -/** - * @ngdoc service - * @name $parse - * @kind function - * - * @description - * - * Converts Angular {@link guide/expression expression} into a function. - * - * ```js - * var getter = $parse('user.name'); - * var setter = getter.assign; - * var context = {user:{name:'angular'}}; - * var locals = {user:{name:'local'}}; - * - * expect(getter(context)).toEqual('angular'); - * setter(context, 'newValue'); - * expect(context.user.name).toEqual('newValue'); - * expect(getter(context, locals)).toEqual('local'); - * ``` - * - * - * @param {string} expression String expression to compile. - * @returns {function(context, locals)} a function which represents the compiled expression: - * - * * `context` – `{object}` – an object against which any expressions embedded in the strings - * are evaluated against (typically a scope object). - * * `locals` – `{object=}` – local variables context object, useful for overriding values in - * `context`. - * - * The returned function also has the following properties: - * * `literal` – `{boolean}` – whether the expression's top-level node is a JavaScript - * literal. - * * `constant` – `{boolean}` – whether the expression is made entirely of JavaScript - * constant literals. - * * `assign` – `{?function(context, value)}` – if the expression is assignable, this will be - * set to a function to change its value on the given context. - * - */ - - -/** - * @ngdoc provider - * @name $parseProvider - * - * @description - * `$parseProvider` can be used for configuring the default behavior of the {@link ng.$parse $parse} - * service. - */ -function $ParseProvider() { - var cacheDefault = createMap(); - var cacheExpensive = createMap(); - - this.$get = ['$filter', function($filter) { - var noUnsafeEval = csp().noUnsafeEval; - var $parseOptions = { - csp: noUnsafeEval, - expensiveChecks: false - }, - $parseOptionsExpensive = { - csp: noUnsafeEval, - expensiveChecks: true - }; - - return function $parse(exp, interceptorFn, expensiveChecks) { - var parsedExpression, oneTime, cacheKey; - - switch (typeof exp) { - case 'string': - exp = exp.trim(); - cacheKey = exp; - - var cache = (expensiveChecks ? cacheExpensive : cacheDefault); - parsedExpression = cache[cacheKey]; - - if (!parsedExpression) { - if (exp.charAt(0) === ':' && exp.charAt(1) === ':') { - oneTime = true; - exp = exp.substring(2); - } - var parseOptions = expensiveChecks ? $parseOptionsExpensive : $parseOptions; - var lexer = new Lexer(parseOptions); - var parser = new Parser(lexer, $filter, parseOptions); - parsedExpression = parser.parse(exp); - if (parsedExpression.constant) { - parsedExpression.$$watchDelegate = constantWatchDelegate; - } else if (oneTime) { - parsedExpression.$$watchDelegate = parsedExpression.literal ? - oneTimeLiteralWatchDelegate : oneTimeWatchDelegate; - } else if (parsedExpression.inputs) { - parsedExpression.$$watchDelegate = inputsWatchDelegate; - } - cache[cacheKey] = parsedExpression; - } - return addInterceptor(parsedExpression, interceptorFn); - - case 'function': - return addInterceptor(exp, interceptorFn); - - default: - return noop; - } - }; - - function expressionInputDirtyCheck(newValue, oldValueOfValue) { - - if (newValue == null || oldValueOfValue == null) { // null/undefined - return newValue === oldValueOfValue; - } - - if (typeof newValue === 'object') { - - // attempt to convert the value to a primitive type - // TODO(docs): add a note to docs that by implementing valueOf even objects and arrays can - // be cheaply dirty-checked - newValue = getValueOf(newValue); - - if (typeof newValue === 'object') { - // objects/arrays are not supported - deep-watching them would be too expensive - return false; - } - - // fall-through to the primitive equality check - } - - //Primitive or NaN - return newValue === oldValueOfValue || (newValue !== newValue && oldValueOfValue !== oldValueOfValue); - } - - function inputsWatchDelegate(scope, listener, objectEquality, parsedExpression, prettyPrintExpression) { - var inputExpressions = parsedExpression.inputs; - var lastResult; - - if (inputExpressions.length === 1) { - var oldInputValueOf = expressionInputDirtyCheck; // init to something unique so that equals check fails - inputExpressions = inputExpressions[0]; - return scope.$watch(function expressionInputWatch(scope) { - var newInputValue = inputExpressions(scope); - if (!expressionInputDirtyCheck(newInputValue, oldInputValueOf)) { - lastResult = parsedExpression(scope, undefined, undefined, [newInputValue]); - oldInputValueOf = newInputValue && getValueOf(newInputValue); - } - return lastResult; - }, listener, objectEquality, prettyPrintExpression); - } - - var oldInputValueOfValues = []; - var oldInputValues = []; - for (var i = 0, ii = inputExpressions.length; i < ii; i++) { - oldInputValueOfValues[i] = expressionInputDirtyCheck; // init to something unique so that equals check fails - oldInputValues[i] = null; - } - - return scope.$watch(function expressionInputsWatch(scope) { - var changed = false; - - for (var i = 0, ii = inputExpressions.length; i < ii; i++) { - var newInputValue = inputExpressions[i](scope); - if (changed || (changed = !expressionInputDirtyCheck(newInputValue, oldInputValueOfValues[i]))) { - oldInputValues[i] = newInputValue; - oldInputValueOfValues[i] = newInputValue && getValueOf(newInputValue); - } - } - - if (changed) { - lastResult = parsedExpression(scope, undefined, undefined, oldInputValues); - } - - return lastResult; - }, listener, objectEquality, prettyPrintExpression); - } - - function oneTimeWatchDelegate(scope, listener, objectEquality, parsedExpression) { - var unwatch, lastValue; - return unwatch = scope.$watch(function oneTimeWatch(scope) { - return parsedExpression(scope); - }, function oneTimeListener(value, old, scope) { - lastValue = value; - if (isFunction(listener)) { - listener.apply(this, arguments); - } - if (isDefined(value)) { - scope.$$postDigest(function() { - if (isDefined(lastValue)) { - unwatch(); - } - }); - } - }, objectEquality); - } - - function oneTimeLiteralWatchDelegate(scope, listener, objectEquality, parsedExpression) { - var unwatch, lastValue; - return unwatch = scope.$watch(function oneTimeWatch(scope) { - return parsedExpression(scope); - }, function oneTimeListener(value, old, scope) { - lastValue = value; - if (isFunction(listener)) { - listener.call(this, value, old, scope); - } - if (isAllDefined(value)) { - scope.$$postDigest(function() { - if (isAllDefined(lastValue)) unwatch(); - }); - } - }, objectEquality); - - function isAllDefined(value) { - var allDefined = true; - forEach(value, function(val) { - if (!isDefined(val)) allDefined = false; - }); - return allDefined; - } - } - - function constantWatchDelegate(scope, listener, objectEquality, parsedExpression) { - var unwatch; - return unwatch = scope.$watch(function constantWatch(scope) { - return parsedExpression(scope); - }, function constantListener(value, old, scope) { - if (isFunction(listener)) { - listener.apply(this, arguments); - } - unwatch(); - }, objectEquality); - } - - function addInterceptor(parsedExpression, interceptorFn) { - if (!interceptorFn) return parsedExpression; - var watchDelegate = parsedExpression.$$watchDelegate; - - var regularWatch = - watchDelegate !== oneTimeLiteralWatchDelegate && - watchDelegate !== oneTimeWatchDelegate; - - var fn = regularWatch ? function regularInterceptedExpression(scope, locals, assign, inputs) { - var value = parsedExpression(scope, locals, assign, inputs); - return interceptorFn(value, scope, locals); - } : function oneTimeInterceptedExpression(scope, locals, assign, inputs) { - var value = parsedExpression(scope, locals, assign, inputs); - var result = interceptorFn(value, scope, locals); - // we only return the interceptor's result if the - // initial value is defined (for bind-once) - return isDefined(value) ? result : value; - }; - - // Propagate $$watchDelegates other then inputsWatchDelegate - if (parsedExpression.$$watchDelegate && - parsedExpression.$$watchDelegate !== inputsWatchDelegate) { - fn.$$watchDelegate = parsedExpression.$$watchDelegate; - } else if (!interceptorFn.$stateful) { - // If there is an interceptor, but no watchDelegate then treat the interceptor like - // we treat filters - it is assumed to be a pure function unless flagged with $stateful - fn.$$watchDelegate = inputsWatchDelegate; - fn.inputs = parsedExpression.inputs ? parsedExpression.inputs : [parsedExpression]; - } - - return fn; - } - }]; -} - -/** - * @ngdoc service - * @name $q - * @requires $rootScope - * - * @description - * A service that helps you run functions asynchronously, and use their return values (or exceptions) - * when they are done processing. - * - * This is an implementation of promises/deferred objects inspired by - * [Kris Kowal's Q](https://github.com/kriskowal/q). - * - * $q can be used in two fashions --- one which is more similar to Kris Kowal's Q or jQuery's Deferred - * implementations, and the other which resembles ES6 promises to some degree. - * - * # $q constructor - * - * The streamlined ES6 style promise is essentially just using $q as a constructor which takes a `resolver` - * function as the first argument. This is similar to the native Promise implementation from ES6 Harmony, - * see [MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise). - * - * While the constructor-style use is supported, not all of the supporting methods from ES6 Harmony promises are - * available yet. - * - * It can be used like so: - * - * ```js - * // for the purpose of this example let's assume that variables `$q` and `okToGreet` - * // are available in the current lexical scope (they could have been injected or passed in). - * - * function asyncGreet(name) { - * // perform some asynchronous operation, resolve or reject the promise when appropriate. - * return $q(function(resolve, reject) { - * setTimeout(function() { - * if (okToGreet(name)) { - * resolve('Hello, ' + name + '!'); - * } else { - * reject('Greeting ' + name + ' is not allowed.'); - * } - * }, 1000); - * }); - * } - * - * var promise = asyncGreet('Robin Hood'); - * promise.then(function(greeting) { - * alert('Success: ' + greeting); - * }, function(reason) { - * alert('Failed: ' + reason); - * }); - * ``` - * - * Note: progress/notify callbacks are not currently supported via the ES6-style interface. - * - * However, the more traditional CommonJS-style usage is still available, and documented below. - * - * [The CommonJS Promise proposal](http://wiki.commonjs.org/wiki/Promises) describes a promise as an - * interface for interacting with an object that represents the result of an action that is - * performed asynchronously, and may or may not be finished at any given point in time. - * - * From the perspective of dealing with error handling, deferred and promise APIs are to - * asynchronous programming what `try`, `catch` and `throw` keywords are to synchronous programming. - * - * ```js - * // for the purpose of this example let's assume that variables `$q` and `okToGreet` - * // are available in the current lexical scope (they could have been injected or passed in). - * - * function asyncGreet(name) { - * var deferred = $q.defer(); - * - * setTimeout(function() { - * deferred.notify('About to greet ' + name + '.'); - * - * if (okToGreet(name)) { - * deferred.resolve('Hello, ' + name + '!'); - * } else { - * deferred.reject('Greeting ' + name + ' is not allowed.'); - * } - * }, 1000); - * - * return deferred.promise; - * } - * - * var promise = asyncGreet('Robin Hood'); - * promise.then(function(greeting) { - * alert('Success: ' + greeting); - * }, function(reason) { - * alert('Failed: ' + reason); - * }, function(update) { - * alert('Got notification: ' + update); - * }); - * ``` - * - * At first it might not be obvious why this extra complexity is worth the trouble. The payoff - * comes in the way of guarantees that promise and deferred APIs make, see - * https://github.com/kriskowal/uncommonjs/blob/master/promises/specification.md. - * - * Additionally the promise api allows for composition that is very hard to do with the - * traditional callback ([CPS](http://en.wikipedia.org/wiki/Continuation-passing_style)) approach. - * For more on this please see the [Q documentation](https://github.com/kriskowal/q) especially the - * section on serial or parallel joining of promises. - * - * # The Deferred API - * - * A new instance of deferred is constructed by calling `$q.defer()`. - * - * The purpose of the deferred object is to expose the associated Promise instance as well as APIs - * that can be used for signaling the successful or unsuccessful completion, as well as the status - * of the task. - * - * **Methods** - * - * - `resolve(value)` – resolves the derived promise with the `value`. If the value is a rejection - * constructed via `$q.reject`, the promise will be rejected instead. - * - `reject(reason)` – rejects the derived promise with the `reason`. This is equivalent to - * resolving it with a rejection constructed via `$q.reject`. - * - `notify(value)` - provides updates on the status of the promise's execution. This may be called - * multiple times before the promise is either resolved or rejected. - * - * **Properties** - * - * - promise – `{Promise}` – promise object associated with this deferred. - * - * - * # The Promise API - * - * A new promise instance is created when a deferred instance is created and can be retrieved by - * calling `deferred.promise`. - * - * The purpose of the promise object is to allow for interested parties to get access to the result - * of the deferred task when it completes. - * - * **Methods** - * - * - `then(successCallback, errorCallback, notifyCallback)` – regardless of when the promise was or - * will be resolved or rejected, `then` calls one of the success or error callbacks asynchronously - * as soon as the result is available. The callbacks are called with a single argument: the result - * or rejection reason. Additionally, the notify callback may be called zero or more times to - * provide a progress indication, before the promise is resolved or rejected. - * - * This method *returns a new promise* which is resolved or rejected via the return value of the - * `successCallback`, `errorCallback` (unless that value is a promise, in which case it is resolved - * with the value which is resolved in that promise using - * [promise chaining](http://www.html5rocks.com/en/tutorials/es6/promises/#toc-promises-queues)). - * It also notifies via the return value of the `notifyCallback` method. The promise cannot be - * resolved or rejected from the notifyCallback method. - * - * - `catch(errorCallback)` – shorthand for `promise.then(null, errorCallback)` - * - * - `finally(callback, notifyCallback)` – allows you to observe either the fulfillment or rejection of a promise, - * but to do so without modifying the final value. This is useful to release resources or do some - * clean-up that needs to be done whether the promise was rejected or resolved. See the [full - * specification](https://github.com/kriskowal/q/wiki/API-Reference#promisefinallycallback) for - * more information. - * - * # Chaining promises - * - * Because calling the `then` method of a promise returns a new derived promise, it is easily - * possible to create a chain of promises: - * - * ```js - * promiseB = promiseA.then(function(result) { - * return result + 1; - * }); - * - * // promiseB will be resolved immediately after promiseA is resolved and its value - * // will be the result of promiseA incremented by 1 - * ``` - * - * It is possible to create chains of any length and since a promise can be resolved with another - * promise (which will defer its resolution further), it is possible to pause/defer resolution of - * the promises at any point in the chain. This makes it possible to implement powerful APIs like - * $http's response interceptors. - * - * - * # Differences between Kris Kowal's Q and $q - * - * There are two main differences: - * - * - $q is integrated with the {@link ng.$rootScope.Scope} Scope model observation - * mechanism in angular, which means faster propagation of resolution or rejection into your - * models and avoiding unnecessary browser repaints, which would result in flickering UI. - * - Q has many more features than $q, but that comes at a cost of bytes. $q is tiny, but contains - * all the important functionality needed for common async tasks. - * - * # Testing - * - * ```js - * it('should simulate promise', inject(function($q, $rootScope) { - * var deferred = $q.defer(); - * var promise = deferred.promise; - * var resolvedValue; - * - * promise.then(function(value) { resolvedValue = value; }); - * expect(resolvedValue).toBeUndefined(); - * - * // Simulate resolving of promise - * deferred.resolve(123); - * // Note that the 'then' function does not get called synchronously. - * // This is because we want the promise API to always be async, whether or not - * // it got called synchronously or asynchronously. - * expect(resolvedValue).toBeUndefined(); - * - * // Propagate promise resolution to 'then' functions using $apply(). - * $rootScope.$apply(); - * expect(resolvedValue).toEqual(123); - * })); - * ``` - * - * @param {function(function, function)} resolver Function which is responsible for resolving or - * rejecting the newly created promise. The first parameter is a function which resolves the - * promise, the second parameter is a function which rejects the promise. - * - * @returns {Promise} The newly created promise. - */ -function $QProvider() { - - this.$get = ['$rootScope', '$exceptionHandler', function($rootScope, $exceptionHandler) { - return qFactory(function(callback) { - $rootScope.$evalAsync(callback); - }, $exceptionHandler); - }]; -} - -function $$QProvider() { - this.$get = ['$browser', '$exceptionHandler', function($browser, $exceptionHandler) { - return qFactory(function(callback) { - $browser.defer(callback); - }, $exceptionHandler); - }]; -} - -/** - * Constructs a promise manager. - * - * @param {function(function)} nextTick Function for executing functions in the next turn. - * @param {function(...*)} exceptionHandler Function into which unexpected exceptions are passed for - * debugging purposes. - * @returns {object} Promise manager. - */ -function qFactory(nextTick, exceptionHandler) { - var $qMinErr = minErr('$q', TypeError); - function callOnce(self, resolveFn, rejectFn) { - var called = false; - function wrap(fn) { - return function(value) { - if (called) return; - called = true; - fn.call(self, value); - }; - } - - return [wrap(resolveFn), wrap(rejectFn)]; - } - - /** - * @ngdoc method - * @name ng.$q#defer - * @kind function - * - * @description - * Creates a `Deferred` object which represents a task which will finish in the future. - * - * @returns {Deferred} Returns a new instance of deferred. - */ - var defer = function() { - return new Deferred(); - }; - - function Promise() { - this.$$state = { status: 0 }; - } - - extend(Promise.prototype, { - then: function(onFulfilled, onRejected, progressBack) { - if (isUndefined(onFulfilled) && isUndefined(onRejected) && isUndefined(progressBack)) { - return this; - } - var result = new Deferred(); - - this.$$state.pending = this.$$state.pending || []; - this.$$state.pending.push([result, onFulfilled, onRejected, progressBack]); - if (this.$$state.status > 0) scheduleProcessQueue(this.$$state); - - return result.promise; - }, - - "catch": function(callback) { - return this.then(null, callback); - }, - - "finally": function(callback, progressBack) { - return this.then(function(value) { - return handleCallback(value, true, callback); - }, function(error) { - return handleCallback(error, false, callback); - }, progressBack); - } - }); - - //Faster, more basic than angular.bind http://jsperf.com/angular-bind-vs-custom-vs-native - function simpleBind(context, fn) { - return function(value) { - fn.call(context, value); - }; - } - - function processQueue(state) { - var fn, deferred, pending; - - pending = state.pending; - state.processScheduled = false; - state.pending = undefined; - for (var i = 0, ii = pending.length; i < ii; ++i) { - deferred = pending[i][0]; - fn = pending[i][state.status]; - try { - if (isFunction(fn)) { - deferred.resolve(fn(state.value)); - } else if (state.status === 1) { - deferred.resolve(state.value); - } else { - deferred.reject(state.value); - } - } catch (e) { - deferred.reject(e); - exceptionHandler(e); - } - } - } - - function scheduleProcessQueue(state) { - if (state.processScheduled || !state.pending) return; - state.processScheduled = true; - nextTick(function() { processQueue(state); }); - } - - function Deferred() { - this.promise = new Promise(); - //Necessary to support unbound execution :/ - this.resolve = simpleBind(this, this.resolve); - this.reject = simpleBind(this, this.reject); - this.notify = simpleBind(this, this.notify); - } - - extend(Deferred.prototype, { - resolve: function(val) { - if (this.promise.$$state.status) return; - if (val === this.promise) { - this.$$reject($qMinErr( - 'qcycle', - "Expected promise to be resolved with value other than itself '{0}'", - val)); - } else { - this.$$resolve(val); - } - - }, - - $$resolve: function(val) { - var then, fns; - - fns = callOnce(this, this.$$resolve, this.$$reject); - try { - if ((isObject(val) || isFunction(val))) then = val && val.then; - if (isFunction(then)) { - this.promise.$$state.status = -1; - then.call(val, fns[0], fns[1], this.notify); - } else { - this.promise.$$state.value = val; - this.promise.$$state.status = 1; - scheduleProcessQueue(this.promise.$$state); - } - } catch (e) { - fns[1](e); - exceptionHandler(e); - } - }, - - reject: function(reason) { - if (this.promise.$$state.status) return; - this.$$reject(reason); - }, - - $$reject: function(reason) { - this.promise.$$state.value = reason; - this.promise.$$state.status = 2; - scheduleProcessQueue(this.promise.$$state); - }, - - notify: function(progress) { - var callbacks = this.promise.$$state.pending; - - if ((this.promise.$$state.status <= 0) && callbacks && callbacks.length) { - nextTick(function() { - var callback, result; - for (var i = 0, ii = callbacks.length; i < ii; i++) { - result = callbacks[i][0]; - callback = callbacks[i][3]; - try { - result.notify(isFunction(callback) ? callback(progress) : progress); - } catch (e) { - exceptionHandler(e); - } - } - }); - } - } - }); - - /** - * @ngdoc method - * @name $q#reject - * @kind function - * - * @description - * Creates a promise that is resolved as rejected with the specified `reason`. This api should be - * used to forward rejection in a chain of promises. If you are dealing with the last promise in - * a promise chain, you don't need to worry about it. - * - * When comparing deferreds/promises to the familiar behavior of try/catch/throw, think of - * `reject` as the `throw` keyword in JavaScript. This also means that if you "catch" an error via - * a promise error callback and you want to forward the error to the promise derived from the - * current promise, you have to "rethrow" the error by returning a rejection constructed via - * `reject`. - * - * ```js - * promiseB = promiseA.then(function(result) { - * // success: do something and resolve promiseB - * // with the old or a new result - * return result; - * }, function(reason) { - * // error: handle the error if possible and - * // resolve promiseB with newPromiseOrValue, - * // otherwise forward the rejection to promiseB - * if (canHandle(reason)) { - * // handle the error and recover - * return newPromiseOrValue; - * } - * return $q.reject(reason); - * }); - * ``` - * - * @param {*} reason Constant, message, exception or an object representing the rejection reason. - * @returns {Promise} Returns a promise that was already resolved as rejected with the `reason`. - */ - var reject = function(reason) { - var result = new Deferred(); - result.reject(reason); - return result.promise; - }; - - var makePromise = function makePromise(value, resolved) { - var result = new Deferred(); - if (resolved) { - result.resolve(value); - } else { - result.reject(value); - } - return result.promise; - }; - - var handleCallback = function handleCallback(value, isResolved, callback) { - var callbackOutput = null; - try { - if (isFunction(callback)) callbackOutput = callback(); - } catch (e) { - return makePromise(e, false); - } - if (isPromiseLike(callbackOutput)) { - return callbackOutput.then(function() { - return makePromise(value, isResolved); - }, function(error) { - return makePromise(error, false); - }); - } else { - return makePromise(value, isResolved); - } - }; - - /** - * @ngdoc method - * @name $q#when - * @kind function - * - * @description - * Wraps an object that might be a value or a (3rd party) then-able promise into a $q promise. - * This is useful when you are dealing with an object that might or might not be a promise, or if - * the promise comes from a source that can't be trusted. - * - * @param {*} value Value or a promise - * @param {Function=} successCallback - * @param {Function=} errorCallback - * @param {Function=} progressCallback - * @returns {Promise} Returns a promise of the passed value or promise - */ - - - var when = function(value, callback, errback, progressBack) { - var result = new Deferred(); - result.resolve(value); - return result.promise.then(callback, errback, progressBack); - }; - - /** - * @ngdoc method - * @name $q#resolve - * @kind function - * - * @description - * Alias of {@link ng.$q#when when} to maintain naming consistency with ES6. - * - * @param {*} value Value or a promise - * @param {Function=} successCallback - * @param {Function=} errorCallback - * @param {Function=} progressCallback - * @returns {Promise} Returns a promise of the passed value or promise - */ - var resolve = when; - - /** - * @ngdoc method - * @name $q#all - * @kind function - * - * @description - * Combines multiple promises into a single promise that is resolved when all of the input - * promises are resolved. - * - * @param {Array.|Object.} promises An array or hash of promises. - * @returns {Promise} Returns a single promise that will be resolved with an array/hash of values, - * each value corresponding to the promise at the same index/key in the `promises` array/hash. - * If any of the promises is resolved with a rejection, this resulting promise will be rejected - * with the same rejection value. - */ - - function all(promises) { - var deferred = new Deferred(), - counter = 0, - results = isArray(promises) ? [] : {}; - - forEach(promises, function(promise, key) { - counter++; - when(promise).then(function(value) { - if (results.hasOwnProperty(key)) return; - results[key] = value; - if (!(--counter)) deferred.resolve(results); - }, function(reason) { - if (results.hasOwnProperty(key)) return; - deferred.reject(reason); - }); - }); - - if (counter === 0) { - deferred.resolve(results); - } - - return deferred.promise; - } - - var $Q = function Q(resolver) { - if (!isFunction(resolver)) { - throw $qMinErr('norslvr', "Expected resolverFn, got '{0}'", resolver); - } - - if (!(this instanceof Q)) { - // More useful when $Q is the Promise itself. - return new Q(resolver); - } - - var deferred = new Deferred(); - - function resolveFn(value) { - deferred.resolve(value); - } - - function rejectFn(reason) { - deferred.reject(reason); - } - - resolver(resolveFn, rejectFn); - - return deferred.promise; - }; - - $Q.defer = defer; - $Q.reject = reject; - $Q.when = when; - $Q.resolve = resolve; - $Q.all = all; - - return $Q; -} - -function $$RAFProvider() { //rAF - this.$get = ['$window', '$timeout', function($window, $timeout) { - var requestAnimationFrame = $window.requestAnimationFrame || - $window.webkitRequestAnimationFrame; - - var cancelAnimationFrame = $window.cancelAnimationFrame || - $window.webkitCancelAnimationFrame || - $window.webkitCancelRequestAnimationFrame; - - var rafSupported = !!requestAnimationFrame; - var rafFn = rafSupported - ? function(fn) { - var id = requestAnimationFrame(fn); - return function() { - cancelAnimationFrame(id); - }; - } - : function(fn) { - var timer = $timeout(fn, 16.66, false); // 1000 / 60 = 16.666 - return function() { - $timeout.cancel(timer); - }; - }; - - queueFn.supported = rafSupported; - - var cancelLastRAF; - var taskCount = 0; - var taskQueue = []; - return queueFn; - - function flush() { - for (var i = 0; i < taskQueue.length; i++) { - var task = taskQueue[i]; - if (task) { - taskQueue[i] = null; - task(); - } - } - taskCount = taskQueue.length = 0; - } - - function queueFn(asyncFn) { - var index = taskQueue.length; - - taskCount++; - taskQueue.push(asyncFn); - - if (index === 0) { - cancelLastRAF = rafFn(flush); - } - - return function cancelQueueFn() { - if (index >= 0) { - taskQueue[index] = null; - index = null; - - if (--taskCount === 0 && cancelLastRAF) { - cancelLastRAF(); - cancelLastRAF = null; - taskQueue.length = 0; - } - } - }; - } - }]; -} - -/** - * DESIGN NOTES - * - * The design decisions behind the scope are heavily favored for speed and memory consumption. - * - * The typical use of scope is to watch the expressions, which most of the time return the same - * value as last time so we optimize the operation. - * - * Closures construction is expensive in terms of speed as well as memory: - * - No closures, instead use prototypical inheritance for API - * - Internal state needs to be stored on scope directly, which means that private state is - * exposed as $$____ properties - * - * Loop operations are optimized by using while(count--) { ... } - * - this means that in order to keep the same order of execution as addition we have to add - * items to the array at the beginning (unshift) instead of at the end (push) - * - * Child scopes are created and removed often - * - Using an array would be slow since inserts in middle are expensive so we use linked list - * - * There are few watches then a lot of observers. This is why you don't want the observer to be - * implemented in the same way as watch. Watch requires return of initialization function which - * are expensive to construct. - */ - - -/** - * @ngdoc provider - * @name $rootScopeProvider - * @description - * - * Provider for the $rootScope service. - */ - -/** - * @ngdoc method - * @name $rootScopeProvider#digestTtl - * @description - * - * Sets the number of `$digest` iterations the scope should attempt to execute before giving up and - * assuming that the model is unstable. - * - * The current default is 10 iterations. - * - * In complex applications it's possible that the dependencies between `$watch`s will result in - * several digest iterations. However if an application needs more than the default 10 digest - * iterations for its model to stabilize then you should investigate what is causing the model to - * continuously change during the digest. - * - * Increasing the TTL could have performance implications, so you should not change it without - * proper justification. - * - * @param {number} limit The number of digest iterations. - */ - - -/** - * @ngdoc service - * @name $rootScope - * @description - * - * Every application has a single root {@link ng.$rootScope.Scope scope}. - * All other scopes are descendant scopes of the root scope. Scopes provide separation - * between the model and the view, via a mechanism for watching the model for changes. - * They also provide an event emission/broadcast and subscription facility. See the - * {@link guide/scope developer guide on scopes}. - */ -function $RootScopeProvider() { - var TTL = 10; - var $rootScopeMinErr = minErr('$rootScope'); - var lastDirtyWatch = null; - var applyAsyncId = null; - - this.digestTtl = function(value) { - if (arguments.length) { - TTL = value; - } - return TTL; - }; - - function createChildScopeClass(parent) { - function ChildScope() { - this.$$watchers = this.$$nextSibling = - this.$$childHead = this.$$childTail = null; - this.$$listeners = {}; - this.$$listenerCount = {}; - this.$$watchersCount = 0; - this.$id = nextUid(); - this.$$ChildScope = null; - } - ChildScope.prototype = parent; - return ChildScope; - } - - this.$get = ['$injector', '$exceptionHandler', '$parse', '$browser', - function($injector, $exceptionHandler, $parse, $browser) { - - function destroyChildScope($event) { - $event.currentScope.$$destroyed = true; - } - - /** - * @ngdoc type - * @name $rootScope.Scope - * - * @description - * A root scope can be retrieved using the {@link ng.$rootScope $rootScope} key from the - * {@link auto.$injector $injector}. Child scopes are created using the - * {@link ng.$rootScope.Scope#$new $new()} method. (Most scopes are created automatically when - * compiled HTML template is executed.) See also the {@link guide/scope Scopes guide} for - * an in-depth introduction and usage examples. - * - * - * # Inheritance - * A scope can inherit from a parent scope, as in this example: - * ```js - var parent = $rootScope; - var child = parent.$new(); - - parent.salutation = "Hello"; - expect(child.salutation).toEqual('Hello'); - - child.salutation = "Welcome"; - expect(child.salutation).toEqual('Welcome'); - expect(parent.salutation).toEqual('Hello'); - * ``` - * - * When interacting with `Scope` in tests, additional helper methods are available on the - * instances of `Scope` type. See {@link ngMock.$rootScope.Scope ngMock Scope} for additional - * details. - * - * - * @param {Object.=} providers Map of service factory which need to be - * provided for the current scope. Defaults to {@link ng}. - * @param {Object.=} instanceCache Provides pre-instantiated services which should - * append/override services provided by `providers`. This is handy - * when unit-testing and having the need to override a default - * service. - * @returns {Object} Newly created scope. - * - */ - function Scope() { - this.$id = nextUid(); - this.$$phase = this.$parent = this.$$watchers = - this.$$nextSibling = this.$$prevSibling = - this.$$childHead = this.$$childTail = null; - this.$root = this; - this.$$destroyed = false; - this.$$listeners = {}; - this.$$listenerCount = {}; - this.$$watchersCount = 0; - this.$$isolateBindings = null; - } - - /** - * @ngdoc property - * @name $rootScope.Scope#$id - * - * @description - * Unique scope ID (monotonically increasing) useful for debugging. - */ - - /** - * @ngdoc property - * @name $rootScope.Scope#$parent - * - * @description - * Reference to the parent scope. - */ - - /** - * @ngdoc property - * @name $rootScope.Scope#$root - * - * @description - * Reference to the root scope. - */ - - Scope.prototype = { - constructor: Scope, - /** - * @ngdoc method - * @name $rootScope.Scope#$new - * @kind function - * - * @description - * Creates a new child {@link ng.$rootScope.Scope scope}. - * - * The parent scope will propagate the {@link ng.$rootScope.Scope#$digest $digest()} event. - * The scope can be removed from the scope hierarchy using {@link ng.$rootScope.Scope#$destroy $destroy()}. - * - * {@link ng.$rootScope.Scope#$destroy $destroy()} must be called on a scope when it is - * desired for the scope and its child scopes to be permanently detached from the parent and - * thus stop participating in model change detection and listener notification by invoking. - * - * @param {boolean} isolate If true, then the scope does not prototypically inherit from the - * parent scope. The scope is isolated, as it can not see parent scope properties. - * When creating widgets, it is useful for the widget to not accidentally read parent - * state. - * - * @param {Scope} [parent=this] The {@link ng.$rootScope.Scope `Scope`} that will be the `$parent` - * of the newly created scope. Defaults to `this` scope if not provided. - * This is used when creating a transclude scope to correctly place it - * in the scope hierarchy while maintaining the correct prototypical - * inheritance. - * - * @returns {Object} The newly created child scope. - * - */ - $new: function(isolate, parent) { - var child; - - parent = parent || this; - - if (isolate) { - child = new Scope(); - child.$root = this.$root; - } else { - // Only create a child scope class if somebody asks for one, - // but cache it to allow the VM to optimize lookups. - if (!this.$$ChildScope) { - this.$$ChildScope = createChildScopeClass(this); - } - child = new this.$$ChildScope(); - } - child.$parent = parent; - child.$$prevSibling = parent.$$childTail; - if (parent.$$childHead) { - parent.$$childTail.$$nextSibling = child; - parent.$$childTail = child; - } else { - parent.$$childHead = parent.$$childTail = child; - } - - // When the new scope is not isolated or we inherit from `this`, and - // the parent scope is destroyed, the property `$$destroyed` is inherited - // prototypically. In all other cases, this property needs to be set - // when the parent scope is destroyed. - // The listener needs to be added after the parent is set - if (isolate || parent != this) child.$on('$destroy', destroyChildScope); - - return child; - }, - - /** - * @ngdoc method - * @name $rootScope.Scope#$watch - * @kind function - * - * @description - * Registers a `listener` callback to be executed whenever the `watchExpression` changes. - * - * - The `watchExpression` is called on every call to {@link ng.$rootScope.Scope#$digest - * $digest()} and should return the value that will be watched. (Since - * {@link ng.$rootScope.Scope#$digest $digest()} reruns when it detects changes the - * `watchExpression` can execute multiple times per - * {@link ng.$rootScope.Scope#$digest $digest()} and should be idempotent.) - * - The `listener` is called only when the value from the current `watchExpression` and the - * previous call to `watchExpression` are not equal (with the exception of the initial run, - * see below). Inequality is determined according to reference inequality, - * [strict comparison](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Comparison_Operators) - * via the `!==` Javascript operator, unless `objectEquality == true` - * (see next point) - * - When `objectEquality == true`, inequality of the `watchExpression` is determined - * according to the {@link angular.equals} function. To save the value of the object for - * later comparison, the {@link angular.copy} function is used. This therefore means that - * watching complex objects will have adverse memory and performance implications. - * - The watch `listener` may change the model, which may trigger other `listener`s to fire. - * This is achieved by rerunning the watchers until no changes are detected. The rerun - * iteration limit is 10 to prevent an infinite loop deadlock. - * - * - * If you want to be notified whenever {@link ng.$rootScope.Scope#$digest $digest} is called, - * you can register a `watchExpression` function with no `listener`. (Be prepared for - * multiple calls to your `watchExpression` because it will execute multiple times in a - * single {@link ng.$rootScope.Scope#$digest $digest} cycle if a change is detected.) - * - * After a watcher is registered with the scope, the `listener` fn is called asynchronously - * (via {@link ng.$rootScope.Scope#$evalAsync $evalAsync}) to initialize the - * watcher. In rare cases, this is undesirable because the listener is called when the result - * of `watchExpression` didn't change. To detect this scenario within the `listener` fn, you - * can compare the `newVal` and `oldVal`. If these two values are identical (`===`) then the - * listener was called due to initialization. - * - * - * - * # Example - * ```js - // let's assume that scope was dependency injected as the $rootScope - var scope = $rootScope; - scope.name = 'misko'; - scope.counter = 0; - - expect(scope.counter).toEqual(0); - scope.$watch('name', function(newValue, oldValue) { - scope.counter = scope.counter + 1; - }); - expect(scope.counter).toEqual(0); - - scope.$digest(); - // the listener is always called during the first $digest loop after it was registered - expect(scope.counter).toEqual(1); - - scope.$digest(); - // but now it will not be called unless the value changes - expect(scope.counter).toEqual(1); - - scope.name = 'adam'; - scope.$digest(); - expect(scope.counter).toEqual(2); - - - - // Using a function as a watchExpression - var food; - scope.foodCounter = 0; - expect(scope.foodCounter).toEqual(0); - scope.$watch( - // This function returns the value being watched. It is called for each turn of the $digest loop - function() { return food; }, - // This is the change listener, called when the value returned from the above function changes - function(newValue, oldValue) { - if ( newValue !== oldValue ) { - // Only increment the counter if the value changed - scope.foodCounter = scope.foodCounter + 1; - } - } - ); - // No digest has been run so the counter will be zero - expect(scope.foodCounter).toEqual(0); - - // Run the digest but since food has not changed count will still be zero - scope.$digest(); - expect(scope.foodCounter).toEqual(0); - - // Update food and run digest. Now the counter will increment - food = 'cheeseburger'; - scope.$digest(); - expect(scope.foodCounter).toEqual(1); - - * ``` - * - * - * - * @param {(function()|string)} watchExpression Expression that is evaluated on each - * {@link ng.$rootScope.Scope#$digest $digest} cycle. A change in the return value triggers - * a call to the `listener`. - * - * - `string`: Evaluated as {@link guide/expression expression} - * - `function(scope)`: called with current `scope` as a parameter. - * @param {function(newVal, oldVal, scope)} listener Callback called whenever the value - * of `watchExpression` changes. - * - * - `newVal` contains the current value of the `watchExpression` - * - `oldVal` contains the previous value of the `watchExpression` - * - `scope` refers to the current scope - * @param {boolean=} objectEquality Compare for object equality using {@link angular.equals} instead of - * comparing for reference equality. - * @returns {function()} Returns a deregistration function for this listener. - */ - $watch: function(watchExp, listener, objectEquality, prettyPrintExpression) { - var get = $parse(watchExp); - - if (get.$$watchDelegate) { - return get.$$watchDelegate(this, listener, objectEquality, get, watchExp); - } - var scope = this, - array = scope.$$watchers, - watcher = { - fn: listener, - last: initWatchVal, - get: get, - exp: prettyPrintExpression || watchExp, - eq: !!objectEquality - }; - - lastDirtyWatch = null; - - if (!isFunction(listener)) { - watcher.fn = noop; - } - - if (!array) { - array = scope.$$watchers = []; - } - // we use unshift since we use a while loop in $digest for speed. - // the while loop reads in reverse order. - array.unshift(watcher); - incrementWatchersCount(this, 1); - - return function deregisterWatch() { - if (arrayRemove(array, watcher) >= 0) { - incrementWatchersCount(scope, -1); - } - lastDirtyWatch = null; - }; - }, - - /** - * @ngdoc method - * @name $rootScope.Scope#$watchGroup - * @kind function - * - * @description - * A variant of {@link ng.$rootScope.Scope#$watch $watch()} where it watches an array of `watchExpressions`. - * If any one expression in the collection changes the `listener` is executed. - * - * - The items in the `watchExpressions` array are observed via standard $watch operation and are examined on every - * call to $digest() to see if any items changes. - * - The `listener` is called whenever any expression in the `watchExpressions` array changes. - * - * @param {Array.} watchExpressions Array of expressions that will be individually - * watched using {@link ng.$rootScope.Scope#$watch $watch()} - * - * @param {function(newValues, oldValues, scope)} listener Callback called whenever the return value of any - * expression in `watchExpressions` changes - * The `newValues` array contains the current values of the `watchExpressions`, with the indexes matching - * those of `watchExpression` - * and the `oldValues` array contains the previous values of the `watchExpressions`, with the indexes matching - * those of `watchExpression` - * The `scope` refers to the current scope. - * @returns {function()} Returns a de-registration function for all listeners. - */ - $watchGroup: function(watchExpressions, listener) { - var oldValues = new Array(watchExpressions.length); - var newValues = new Array(watchExpressions.length); - var deregisterFns = []; - var self = this; - var changeReactionScheduled = false; - var firstRun = true; - - if (!watchExpressions.length) { - // No expressions means we call the listener ASAP - var shouldCall = true; - self.$evalAsync(function() { - if (shouldCall) listener(newValues, newValues, self); - }); - return function deregisterWatchGroup() { - shouldCall = false; - }; - } - - if (watchExpressions.length === 1) { - // Special case size of one - return this.$watch(watchExpressions[0], function watchGroupAction(value, oldValue, scope) { - newValues[0] = value; - oldValues[0] = oldValue; - listener(newValues, (value === oldValue) ? newValues : oldValues, scope); - }); - } - - forEach(watchExpressions, function(expr, i) { - var unwatchFn = self.$watch(expr, function watchGroupSubAction(value, oldValue) { - newValues[i] = value; - oldValues[i] = oldValue; - if (!changeReactionScheduled) { - changeReactionScheduled = true; - self.$evalAsync(watchGroupAction); - } - }); - deregisterFns.push(unwatchFn); - }); - - function watchGroupAction() { - changeReactionScheduled = false; - - if (firstRun) { - firstRun = false; - listener(newValues, newValues, self); - } else { - listener(newValues, oldValues, self); - } - } - - return function deregisterWatchGroup() { - while (deregisterFns.length) { - deregisterFns.shift()(); - } - }; - }, - - - /** - * @ngdoc method - * @name $rootScope.Scope#$watchCollection - * @kind function - * - * @description - * Shallow watches the properties of an object and fires whenever any of the properties change - * (for arrays, this implies watching the array items; for object maps, this implies watching - * the properties). If a change is detected, the `listener` callback is fired. - * - * - The `obj` collection is observed via standard $watch operation and is examined on every - * call to $digest() to see if any items have been added, removed, or moved. - * - The `listener` is called whenever anything within the `obj` has changed. Examples include - * adding, removing, and moving items belonging to an object or array. - * - * - * # Example - * ```js - $scope.names = ['igor', 'matias', 'misko', 'james']; - $scope.dataCount = 4; - - $scope.$watchCollection('names', function(newNames, oldNames) { - $scope.dataCount = newNames.length; - }); - - expect($scope.dataCount).toEqual(4); - $scope.$digest(); - - //still at 4 ... no changes - expect($scope.dataCount).toEqual(4); - - $scope.names.pop(); - $scope.$digest(); - - //now there's been a change - expect($scope.dataCount).toEqual(3); - * ``` - * - * - * @param {string|function(scope)} obj Evaluated as {@link guide/expression expression}. The - * expression value should evaluate to an object or an array which is observed on each - * {@link ng.$rootScope.Scope#$digest $digest} cycle. Any shallow change within the - * collection will trigger a call to the `listener`. - * - * @param {function(newCollection, oldCollection, scope)} listener a callback function called - * when a change is detected. - * - The `newCollection` object is the newly modified data obtained from the `obj` expression - * - The `oldCollection` object is a copy of the former collection data. - * Due to performance considerations, the`oldCollection` value is computed only if the - * `listener` function declares two or more arguments. - * - The `scope` argument refers to the current scope. - * - * @returns {function()} Returns a de-registration function for this listener. When the - * de-registration function is executed, the internal watch operation is terminated. - */ - $watchCollection: function(obj, listener) { - $watchCollectionInterceptor.$stateful = true; - - var self = this; - // the current value, updated on each dirty-check run - var newValue; - // a shallow copy of the newValue from the last dirty-check run, - // updated to match newValue during dirty-check run - var oldValue; - // a shallow copy of the newValue from when the last change happened - var veryOldValue; - // only track veryOldValue if the listener is asking for it - var trackVeryOldValue = (listener.length > 1); - var changeDetected = 0; - var changeDetector = $parse(obj, $watchCollectionInterceptor); - var internalArray = []; - var internalObject = {}; - var initRun = true; - var oldLength = 0; - - function $watchCollectionInterceptor(_value) { - newValue = _value; - var newLength, key, bothNaN, newItem, oldItem; - - // If the new value is undefined, then return undefined as the watch may be a one-time watch - if (isUndefined(newValue)) return; - - if (!isObject(newValue)) { // if primitive - if (oldValue !== newValue) { - oldValue = newValue; - changeDetected++; - } - } else if (isArrayLike(newValue)) { - if (oldValue !== internalArray) { - // we are transitioning from something which was not an array into array. - oldValue = internalArray; - oldLength = oldValue.length = 0; - changeDetected++; - } - - newLength = newValue.length; - - if (oldLength !== newLength) { - // if lengths do not match we need to trigger change notification - changeDetected++; - oldValue.length = oldLength = newLength; - } - // copy the items to oldValue and look for changes. - for (var i = 0; i < newLength; i++) { - oldItem = oldValue[i]; - newItem = newValue[i]; - - bothNaN = (oldItem !== oldItem) && (newItem !== newItem); - if (!bothNaN && (oldItem !== newItem)) { - changeDetected++; - oldValue[i] = newItem; - } - } - } else { - if (oldValue !== internalObject) { - // we are transitioning from something which was not an object into object. - oldValue = internalObject = {}; - oldLength = 0; - changeDetected++; - } - // copy the items to oldValue and look for changes. - newLength = 0; - for (key in newValue) { - if (newValue.hasOwnProperty(key)) { - newLength++; - newItem = newValue[key]; - oldItem = oldValue[key]; - - if (key in oldValue) { - bothNaN = (oldItem !== oldItem) && (newItem !== newItem); - if (!bothNaN && (oldItem !== newItem)) { - changeDetected++; - oldValue[key] = newItem; - } - } else { - oldLength++; - oldValue[key] = newItem; - changeDetected++; - } - } - } - if (oldLength > newLength) { - // we used to have more keys, need to find them and destroy them. - changeDetected++; - for (key in oldValue) { - if (!newValue.hasOwnProperty(key)) { - oldLength--; - delete oldValue[key]; - } - } - } - } - return changeDetected; - } - - function $watchCollectionAction() { - if (initRun) { - initRun = false; - listener(newValue, newValue, self); - } else { - listener(newValue, veryOldValue, self); - } - - // make a copy for the next time a collection is changed - if (trackVeryOldValue) { - if (!isObject(newValue)) { - //primitive - veryOldValue = newValue; - } else if (isArrayLike(newValue)) { - veryOldValue = new Array(newValue.length); - for (var i = 0; i < newValue.length; i++) { - veryOldValue[i] = newValue[i]; - } - } else { // if object - veryOldValue = {}; - for (var key in newValue) { - if (hasOwnProperty.call(newValue, key)) { - veryOldValue[key] = newValue[key]; - } - } - } - } - } - - return this.$watch(changeDetector, $watchCollectionAction); - }, - - /** - * @ngdoc method - * @name $rootScope.Scope#$digest - * @kind function - * - * @description - * Processes all of the {@link ng.$rootScope.Scope#$watch watchers} of the current scope and - * its children. Because a {@link ng.$rootScope.Scope#$watch watcher}'s listener can change - * the model, the `$digest()` keeps calling the {@link ng.$rootScope.Scope#$watch watchers} - * until no more listeners are firing. This means that it is possible to get into an infinite - * loop. This function will throw `'Maximum iteration limit exceeded.'` if the number of - * iterations exceeds 10. - * - * Usually, you don't call `$digest()` directly in - * {@link ng.directive:ngController controllers} or in - * {@link ng.$compileProvider#directive directives}. - * Instead, you should call {@link ng.$rootScope.Scope#$apply $apply()} (typically from within - * a {@link ng.$compileProvider#directive directive}), which will force a `$digest()`. - * - * If you want to be notified whenever `$digest()` is called, - * you can register a `watchExpression` function with - * {@link ng.$rootScope.Scope#$watch $watch()} with no `listener`. - * - * In unit tests, you may need to call `$digest()` to simulate the scope life cycle. - * - * # Example - * ```js - var scope = ...; - scope.name = 'misko'; - scope.counter = 0; - - expect(scope.counter).toEqual(0); - scope.$watch('name', function(newValue, oldValue) { - scope.counter = scope.counter + 1; - }); - expect(scope.counter).toEqual(0); - - scope.$digest(); - // the listener is always called during the first $digest loop after it was registered - expect(scope.counter).toEqual(1); - - scope.$digest(); - // but now it will not be called unless the value changes - expect(scope.counter).toEqual(1); - - scope.name = 'adam'; - scope.$digest(); - expect(scope.counter).toEqual(2); - * ``` - * - */ - $digest: function() { - var watch, value, last, - watchers, - length, - dirty, ttl = TTL, - next, current, target = this, - watchLog = [], - logIdx, logMsg, asyncTask; - - beginPhase('$digest'); - // Check for changes to browser url that happened in sync before the call to $digest - $browser.$$checkUrlChange(); - - if (this === $rootScope && applyAsyncId !== null) { - // If this is the root scope, and $applyAsync has scheduled a deferred $apply(), then - // cancel the scheduled $apply and flush the queue of expressions to be evaluated. - $browser.defer.cancel(applyAsyncId); - flushApplyAsync(); - } - - lastDirtyWatch = null; - - do { // "while dirty" loop - dirty = false; - current = target; - - while (asyncQueue.length) { - try { - asyncTask = asyncQueue.shift(); - asyncTask.scope.$eval(asyncTask.expression, asyncTask.locals); - } catch (e) { - $exceptionHandler(e); - } - lastDirtyWatch = null; - } - - traverseScopesLoop: - do { // "traverse the scopes" loop - if ((watchers = current.$$watchers)) { - // process our watches - length = watchers.length; - while (length--) { - try { - watch = watchers[length]; - // Most common watches are on primitives, in which case we can short - // circuit it with === operator, only when === fails do we use .equals - if (watch) { - if ((value = watch.get(current)) !== (last = watch.last) && - !(watch.eq - ? equals(value, last) - : (typeof value === 'number' && typeof last === 'number' - && isNaN(value) && isNaN(last)))) { - dirty = true; - lastDirtyWatch = watch; - watch.last = watch.eq ? copy(value, null) : value; - watch.fn(value, ((last === initWatchVal) ? value : last), current); - if (ttl < 5) { - logIdx = 4 - ttl; - if (!watchLog[logIdx]) watchLog[logIdx] = []; - watchLog[logIdx].push({ - msg: isFunction(watch.exp) ? 'fn: ' + (watch.exp.name || watch.exp.toString()) : watch.exp, - newVal: value, - oldVal: last - }); - } - } else if (watch === lastDirtyWatch) { - // If the most recently dirty watcher is now clean, short circuit since the remaining watchers - // have already been tested. - dirty = false; - break traverseScopesLoop; - } - } - } catch (e) { - $exceptionHandler(e); - } - } - } - - // Insanity Warning: scope depth-first traversal - // yes, this code is a bit crazy, but it works and we have tests to prove it! - // this piece should be kept in sync with the traversal in $broadcast - if (!(next = ((current.$$watchersCount && current.$$childHead) || - (current !== target && current.$$nextSibling)))) { - while (current !== target && !(next = current.$$nextSibling)) { - current = current.$parent; - } - } - } while ((current = next)); - - // `break traverseScopesLoop;` takes us to here - - if ((dirty || asyncQueue.length) && !(ttl--)) { - clearPhase(); - throw $rootScopeMinErr('infdig', - '{0} $digest() iterations reached. Aborting!\n' + - 'Watchers fired in the last 5 iterations: {1}', - TTL, watchLog); - } - - } while (dirty || asyncQueue.length); - - clearPhase(); - - while (postDigestQueue.length) { - try { - postDigestQueue.shift()(); - } catch (e) { - $exceptionHandler(e); - } - } - }, - - - /** - * @ngdoc event - * @name $rootScope.Scope#$destroy - * @eventType broadcast on scope being destroyed - * - * @description - * Broadcasted when a scope and its children are being destroyed. - * - * Note that, in AngularJS, there is also a `$destroy` jQuery event, which can be used to - * clean up DOM bindings before an element is removed from the DOM. - */ - - /** - * @ngdoc method - * @name $rootScope.Scope#$destroy - * @kind function - * - * @description - * Removes the current scope (and all of its children) from the parent scope. Removal implies - * that calls to {@link ng.$rootScope.Scope#$digest $digest()} will no longer - * propagate to the current scope and its children. Removal also implies that the current - * scope is eligible for garbage collection. - * - * The `$destroy()` is usually used by directives such as - * {@link ng.directive:ngRepeat ngRepeat} for managing the - * unrolling of the loop. - * - * Just before a scope is destroyed, a `$destroy` event is broadcasted on this scope. - * Application code can register a `$destroy` event handler that will give it a chance to - * perform any necessary cleanup. - * - * Note that, in AngularJS, there is also a `$destroy` jQuery event, which can be used to - * clean up DOM bindings before an element is removed from the DOM. - */ - $destroy: function() { - // We can't destroy a scope that has been already destroyed. - if (this.$$destroyed) return; - var parent = this.$parent; - - this.$broadcast('$destroy'); - this.$$destroyed = true; - - if (this === $rootScope) { - //Remove handlers attached to window when $rootScope is removed - $browser.$$applicationDestroyed(); - } - - incrementWatchersCount(this, -this.$$watchersCount); - for (var eventName in this.$$listenerCount) { - decrementListenerCount(this, this.$$listenerCount[eventName], eventName); - } - - // sever all the references to parent scopes (after this cleanup, the current scope should - // not be retained by any of our references and should be eligible for garbage collection) - if (parent && parent.$$childHead == this) parent.$$childHead = this.$$nextSibling; - if (parent && parent.$$childTail == this) parent.$$childTail = this.$$prevSibling; - if (this.$$prevSibling) this.$$prevSibling.$$nextSibling = this.$$nextSibling; - if (this.$$nextSibling) this.$$nextSibling.$$prevSibling = this.$$prevSibling; - - // Disable listeners, watchers and apply/digest methods - this.$destroy = this.$digest = this.$apply = this.$evalAsync = this.$applyAsync = noop; - this.$on = this.$watch = this.$watchGroup = function() { return noop; }; - this.$$listeners = {}; - - // All of the code below is bogus code that works around V8's memory leak via optimized code - // and inline caches. - // - // see: - // - https://code.google.com/p/v8/issues/detail?id=2073#c26 - // - https://github.com/angular/angular.js/issues/6794#issuecomment-38648909 - // - https://github.com/angular/angular.js/issues/1313#issuecomment-10378451 - - this.$parent = this.$$nextSibling = this.$$prevSibling = this.$$childHead = - this.$$childTail = this.$root = this.$$watchers = null; - }, - - /** - * @ngdoc method - * @name $rootScope.Scope#$eval - * @kind function - * - * @description - * Executes the `expression` on the current scope and returns the result. Any exceptions in - * the expression are propagated (uncaught). This is useful when evaluating Angular - * expressions. - * - * # Example - * ```js - var scope = ng.$rootScope.Scope(); - scope.a = 1; - scope.b = 2; - - expect(scope.$eval('a+b')).toEqual(3); - expect(scope.$eval(function(scope){ return scope.a + scope.b; })).toEqual(3); - * ``` - * - * @param {(string|function())=} expression An angular expression to be executed. - * - * - `string`: execute using the rules as defined in {@link guide/expression expression}. - * - `function(scope)`: execute the function with the current `scope` parameter. - * - * @param {(object)=} locals Local variables object, useful for overriding values in scope. - * @returns {*} The result of evaluating the expression. - */ - $eval: function(expr, locals) { - return $parse(expr)(this, locals); - }, - - /** - * @ngdoc method - * @name $rootScope.Scope#$evalAsync - * @kind function - * - * @description - * Executes the expression on the current scope at a later point in time. - * - * The `$evalAsync` makes no guarantees as to when the `expression` will be executed, only - * that: - * - * - it will execute after the function that scheduled the evaluation (preferably before DOM - * rendering). - * - at least one {@link ng.$rootScope.Scope#$digest $digest cycle} will be performed after - * `expression` execution. - * - * Any exceptions from the execution of the expression are forwarded to the - * {@link ng.$exceptionHandler $exceptionHandler} service. - * - * __Note:__ if this function is called outside of a `$digest` cycle, a new `$digest` cycle - * will be scheduled. However, it is encouraged to always call code that changes the model - * from within an `$apply` call. That includes code evaluated via `$evalAsync`. - * - * @param {(string|function())=} expression An angular expression to be executed. - * - * - `string`: execute using the rules as defined in {@link guide/expression expression}. - * - `function(scope)`: execute the function with the current `scope` parameter. - * - * @param {(object)=} locals Local variables object, useful for overriding values in scope. - */ - $evalAsync: function(expr, locals) { - // if we are outside of an $digest loop and this is the first time we are scheduling async - // task also schedule async auto-flush - if (!$rootScope.$$phase && !asyncQueue.length) { - $browser.defer(function() { - if (asyncQueue.length) { - $rootScope.$digest(); - } - }); - } - - asyncQueue.push({scope: this, expression: expr, locals: locals}); - }, - - $$postDigest: function(fn) { - postDigestQueue.push(fn); - }, - - /** - * @ngdoc method - * @name $rootScope.Scope#$apply - * @kind function - * - * @description - * `$apply()` is used to execute an expression in angular from outside of the angular - * framework. (For example from browser DOM events, setTimeout, XHR or third party libraries). - * Because we are calling into the angular framework we need to perform proper scope life - * cycle of {@link ng.$exceptionHandler exception handling}, - * {@link ng.$rootScope.Scope#$digest executing watches}. - * - * ## Life cycle - * - * # Pseudo-Code of `$apply()` - * ```js - function $apply(expr) { - try { - return $eval(expr); - } catch (e) { - $exceptionHandler(e); - } finally { - $root.$digest(); - } - } - * ``` - * - * - * Scope's `$apply()` method transitions through the following stages: - * - * 1. The {@link guide/expression expression} is executed using the - * {@link ng.$rootScope.Scope#$eval $eval()} method. - * 2. Any exceptions from the execution of the expression are forwarded to the - * {@link ng.$exceptionHandler $exceptionHandler} service. - * 3. The {@link ng.$rootScope.Scope#$watch watch} listeners are fired immediately after the - * expression was executed using the {@link ng.$rootScope.Scope#$digest $digest()} method. - * - * - * @param {(string|function())=} exp An angular expression to be executed. - * - * - `string`: execute using the rules as defined in {@link guide/expression expression}. - * - `function(scope)`: execute the function with current `scope` parameter. - * - * @returns {*} The result of evaluating the expression. - */ - $apply: function(expr) { - try { - beginPhase('$apply'); - try { - return this.$eval(expr); - } finally { - clearPhase(); - } - } catch (e) { - $exceptionHandler(e); - } finally { - try { - $rootScope.$digest(); - } catch (e) { - $exceptionHandler(e); - throw e; - } - } - }, - - /** - * @ngdoc method - * @name $rootScope.Scope#$applyAsync - * @kind function - * - * @description - * Schedule the invocation of $apply to occur at a later time. The actual time difference - * varies across browsers, but is typically around ~10 milliseconds. - * - * This can be used to queue up multiple expressions which need to be evaluated in the same - * digest. - * - * @param {(string|function())=} exp An angular expression to be executed. - * - * - `string`: execute using the rules as defined in {@link guide/expression expression}. - * - `function(scope)`: execute the function with current `scope` parameter. - */ - $applyAsync: function(expr) { - var scope = this; - expr && applyAsyncQueue.push($applyAsyncExpression); - scheduleApplyAsync(); - - function $applyAsyncExpression() { - scope.$eval(expr); - } - }, - - /** - * @ngdoc method - * @name $rootScope.Scope#$on - * @kind function - * - * @description - * Listens on events of a given type. See {@link ng.$rootScope.Scope#$emit $emit} for - * discussion of event life cycle. - * - * The event listener function format is: `function(event, args...)`. The `event` object - * passed into the listener has the following attributes: - * - * - `targetScope` - `{Scope}`: the scope on which the event was `$emit`-ed or - * `$broadcast`-ed. - * - `currentScope` - `{Scope}`: the scope that is currently handling the event. Once the - * event propagates through the scope hierarchy, this property is set to null. - * - `name` - `{string}`: name of the event. - * - `stopPropagation` - `{function=}`: calling `stopPropagation` function will cancel - * further event propagation (available only for events that were `$emit`-ed). - * - `preventDefault` - `{function}`: calling `preventDefault` sets `defaultPrevented` flag - * to true. - * - `defaultPrevented` - `{boolean}`: true if `preventDefault` was called. - * - * @param {string} name Event name to listen on. - * @param {function(event, ...args)} listener Function to call when the event is emitted. - * @returns {function()} Returns a deregistration function for this listener. - */ - $on: function(name, listener) { - var namedListeners = this.$$listeners[name]; - if (!namedListeners) { - this.$$listeners[name] = namedListeners = []; - } - namedListeners.push(listener); - - var current = this; - do { - if (!current.$$listenerCount[name]) { - current.$$listenerCount[name] = 0; - } - current.$$listenerCount[name]++; - } while ((current = current.$parent)); - - var self = this; - return function() { - var indexOfListener = namedListeners.indexOf(listener); - if (indexOfListener !== -1) { - namedListeners[indexOfListener] = null; - decrementListenerCount(self, 1, name); - } - }; - }, - - - /** - * @ngdoc method - * @name $rootScope.Scope#$emit - * @kind function - * - * @description - * Dispatches an event `name` upwards through the scope hierarchy notifying the - * registered {@link ng.$rootScope.Scope#$on} listeners. - * - * The event life cycle starts at the scope on which `$emit` was called. All - * {@link ng.$rootScope.Scope#$on listeners} listening for `name` event on this scope get - * notified. Afterwards, the event traverses upwards toward the root scope and calls all - * registered listeners along the way. The event will stop propagating if one of the listeners - * cancels it. - * - * Any exception emitted from the {@link ng.$rootScope.Scope#$on listeners} will be passed - * onto the {@link ng.$exceptionHandler $exceptionHandler} service. - * - * @param {string} name Event name to emit. - * @param {...*} args Optional one or more arguments which will be passed onto the event listeners. - * @return {Object} Event object (see {@link ng.$rootScope.Scope#$on}). - */ - $emit: function(name, args) { - var empty = [], - namedListeners, - scope = this, - stopPropagation = false, - event = { - name: name, - targetScope: scope, - stopPropagation: function() {stopPropagation = true;}, - preventDefault: function() { - event.defaultPrevented = true; - }, - defaultPrevented: false - }, - listenerArgs = concat([event], arguments, 1), - i, length; - - do { - namedListeners = scope.$$listeners[name] || empty; - event.currentScope = scope; - for (i = 0, length = namedListeners.length; i < length; i++) { - - // if listeners were deregistered, defragment the array - if (!namedListeners[i]) { - namedListeners.splice(i, 1); - i--; - length--; - continue; - } - try { - //allow all listeners attached to the current scope to run - namedListeners[i].apply(null, listenerArgs); - } catch (e) { - $exceptionHandler(e); - } - } - //if any listener on the current scope stops propagation, prevent bubbling - if (stopPropagation) { - event.currentScope = null; - return event; - } - //traverse upwards - scope = scope.$parent; - } while (scope); - - event.currentScope = null; - - return event; - }, - - - /** - * @ngdoc method - * @name $rootScope.Scope#$broadcast - * @kind function - * - * @description - * Dispatches an event `name` downwards to all child scopes (and their children) notifying the - * registered {@link ng.$rootScope.Scope#$on} listeners. - * - * The event life cycle starts at the scope on which `$broadcast` was called. All - * {@link ng.$rootScope.Scope#$on listeners} listening for `name` event on this scope get - * notified. Afterwards, the event propagates to all direct and indirect scopes of the current - * scope and calls all registered listeners along the way. The event cannot be canceled. - * - * Any exception emitted from the {@link ng.$rootScope.Scope#$on listeners} will be passed - * onto the {@link ng.$exceptionHandler $exceptionHandler} service. - * - * @param {string} name Event name to broadcast. - * @param {...*} args Optional one or more arguments which will be passed onto the event listeners. - * @return {Object} Event object, see {@link ng.$rootScope.Scope#$on} - */ - $broadcast: function(name, args) { - var target = this, - current = target, - next = target, - event = { - name: name, - targetScope: target, - preventDefault: function() { - event.defaultPrevented = true; - }, - defaultPrevented: false - }; - - if (!target.$$listenerCount[name]) return event; - - var listenerArgs = concat([event], arguments, 1), - listeners, i, length; - - //down while you can, then up and next sibling or up and next sibling until back at root - while ((current = next)) { - event.currentScope = current; - listeners = current.$$listeners[name] || []; - for (i = 0, length = listeners.length; i < length; i++) { - // if listeners were deregistered, defragment the array - if (!listeners[i]) { - listeners.splice(i, 1); - i--; - length--; - continue; - } - - try { - listeners[i].apply(null, listenerArgs); - } catch (e) { - $exceptionHandler(e); - } - } - - // Insanity Warning: scope depth-first traversal - // yes, this code is a bit crazy, but it works and we have tests to prove it! - // this piece should be kept in sync with the traversal in $digest - // (though it differs due to having the extra check for $$listenerCount) - if (!(next = ((current.$$listenerCount[name] && current.$$childHead) || - (current !== target && current.$$nextSibling)))) { - while (current !== target && !(next = current.$$nextSibling)) { - current = current.$parent; - } - } - } - - event.currentScope = null; - return event; - } - }; - - var $rootScope = new Scope(); - - //The internal queues. Expose them on the $rootScope for debugging/testing purposes. - var asyncQueue = $rootScope.$$asyncQueue = []; - var postDigestQueue = $rootScope.$$postDigestQueue = []; - var applyAsyncQueue = $rootScope.$$applyAsyncQueue = []; - - return $rootScope; - - - function beginPhase(phase) { - if ($rootScope.$$phase) { - throw $rootScopeMinErr('inprog', '{0} already in progress', $rootScope.$$phase); - } - - $rootScope.$$phase = phase; - } - - function clearPhase() { - $rootScope.$$phase = null; - } - - function incrementWatchersCount(current, count) { - do { - current.$$watchersCount += count; - } while ((current = current.$parent)); - } - - function decrementListenerCount(current, count, name) { - do { - current.$$listenerCount[name] -= count; - - if (current.$$listenerCount[name] === 0) { - delete current.$$listenerCount[name]; - } - } while ((current = current.$parent)); - } - - /** - * function used as an initial value for watchers. - * because it's unique we can easily tell it apart from other values - */ - function initWatchVal() {} - - function flushApplyAsync() { - while (applyAsyncQueue.length) { - try { - applyAsyncQueue.shift()(); - } catch (e) { - $exceptionHandler(e); - } - } - applyAsyncId = null; - } - - function scheduleApplyAsync() { - if (applyAsyncId === null) { - applyAsyncId = $browser.defer(function() { - $rootScope.$apply(flushApplyAsync); - }); - } - } - }]; -} - -/** - * @description - * Private service to sanitize uris for links and images. Used by $compile and $sanitize. - */ -function $$SanitizeUriProvider() { - var aHrefSanitizationWhitelist = /^\s*(https?|ftp|mailto|tel|file):/, - imgSrcSanitizationWhitelist = /^\s*((https?|ftp|file|blob):|data:image\/)/; - - /** - * @description - * Retrieves or overrides the default regular expression that is used for whitelisting of safe - * urls during a[href] sanitization. - * - * The sanitization is a security measure aimed at prevent XSS attacks via html links. - * - * Any url about to be assigned to a[href] via data-binding is first normalized and turned into - * an absolute url. Afterwards, the url is matched against the `aHrefSanitizationWhitelist` - * regular expression. If a match is found, the original url is written into the dom. Otherwise, - * the absolute url is prefixed with `'unsafe:'` string and only then is it written into the DOM. - * - * @param {RegExp=} regexp New regexp to whitelist urls with. - * @returns {RegExp|ng.$compileProvider} Current RegExp if called without value or self for - * chaining otherwise. - */ - this.aHrefSanitizationWhitelist = function(regexp) { - if (isDefined(regexp)) { - aHrefSanitizationWhitelist = regexp; - return this; - } - return aHrefSanitizationWhitelist; - }; - - - /** - * @description - * Retrieves or overrides the default regular expression that is used for whitelisting of safe - * urls during img[src] sanitization. - * - * The sanitization is a security measure aimed at prevent XSS attacks via html links. - * - * Any url about to be assigned to img[src] via data-binding is first normalized and turned into - * an absolute url. Afterwards, the url is matched against the `imgSrcSanitizationWhitelist` - * regular expression. If a match is found, the original url is written into the dom. Otherwise, - * the absolute url is prefixed with `'unsafe:'` string and only then is it written into the DOM. - * - * @param {RegExp=} regexp New regexp to whitelist urls with. - * @returns {RegExp|ng.$compileProvider} Current RegExp if called without value or self for - * chaining otherwise. - */ - this.imgSrcSanitizationWhitelist = function(regexp) { - if (isDefined(regexp)) { - imgSrcSanitizationWhitelist = regexp; - return this; - } - return imgSrcSanitizationWhitelist; - }; - - this.$get = function() { - return function sanitizeUri(uri, isImage) { - var regex = isImage ? imgSrcSanitizationWhitelist : aHrefSanitizationWhitelist; - var normalizedVal; - normalizedVal = urlResolve(uri).href; - if (normalizedVal !== '' && !normalizedVal.match(regex)) { - return 'unsafe:' + normalizedVal; - } - return uri; - }; - }; -} - -/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * - * Any commits to this file should be reviewed with security in mind. * - * Changes to this file can potentially create security vulnerabilities. * - * An approval from 2 Core members with history of modifying * - * this file is required. * - * * - * Does the change somehow allow for arbitrary javascript to be executed? * - * Or allows for someone to change the prototype of built-in objects? * - * Or gives undesired access to variables likes document or window? * - * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ - -var $sceMinErr = minErr('$sce'); - -var SCE_CONTEXTS = { - HTML: 'html', - CSS: 'css', - URL: 'url', - // RESOURCE_URL is a subtype of URL used in contexts where a privileged resource is sourced from a - // url. (e.g. ng-include, script src, templateUrl) - RESOURCE_URL: 'resourceUrl', - JS: 'js' -}; - -// Helper functions follow. - -function adjustMatcher(matcher) { - if (matcher === 'self') { - return matcher; - } else if (isString(matcher)) { - // Strings match exactly except for 2 wildcards - '*' and '**'. - // '*' matches any character except those from the set ':/.?&'. - // '**' matches any character (like .* in a RegExp). - // More than 2 *'s raises an error as it's ill defined. - if (matcher.indexOf('***') > -1) { - throw $sceMinErr('iwcard', - 'Illegal sequence *** in string matcher. String: {0}', matcher); - } - matcher = escapeForRegexp(matcher). - replace('\\*\\*', '.*'). - replace('\\*', '[^:/.?&;]*'); - return new RegExp('^' + matcher + '$'); - } else if (isRegExp(matcher)) { - // The only other type of matcher allowed is a Regexp. - // Match entire URL / disallow partial matches. - // Flags are reset (i.e. no global, ignoreCase or multiline) - return new RegExp('^' + matcher.source + '$'); - } else { - throw $sceMinErr('imatcher', - 'Matchers may only be "self", string patterns or RegExp objects'); - } -} - - -function adjustMatchers(matchers) { - var adjustedMatchers = []; - if (isDefined(matchers)) { - forEach(matchers, function(matcher) { - adjustedMatchers.push(adjustMatcher(matcher)); - }); - } - return adjustedMatchers; -} - - -/** - * @ngdoc service - * @name $sceDelegate - * @kind function - * - * @description - * - * `$sceDelegate` is a service that is used by the `$sce` service to provide {@link ng.$sce Strict - * Contextual Escaping (SCE)} services to AngularJS. - * - * Typically, you would configure or override the {@link ng.$sceDelegate $sceDelegate} instead of - * the `$sce` service to customize the way Strict Contextual Escaping works in AngularJS. This is - * because, while the `$sce` provides numerous shorthand methods, etc., you really only need to - * override 3 core functions (`trustAs`, `getTrusted` and `valueOf`) to replace the way things - * work because `$sce` delegates to `$sceDelegate` for these operations. - * - * Refer {@link ng.$sceDelegateProvider $sceDelegateProvider} to configure this service. - * - * The default instance of `$sceDelegate` should work out of the box with little pain. While you - * can override it completely to change the behavior of `$sce`, the common case would - * involve configuring the {@link ng.$sceDelegateProvider $sceDelegateProvider} instead by setting - * your own whitelists and blacklists for trusting URLs used for loading AngularJS resources such as - * templates. Refer {@link ng.$sceDelegateProvider#resourceUrlWhitelist - * $sceDelegateProvider.resourceUrlWhitelist} and {@link - * ng.$sceDelegateProvider#resourceUrlBlacklist $sceDelegateProvider.resourceUrlBlacklist} - */ - -/** - * @ngdoc provider - * @name $sceDelegateProvider - * @description - * - * The `$sceDelegateProvider` provider allows developers to configure the {@link ng.$sceDelegate - * $sceDelegate} service. This allows one to get/set the whitelists and blacklists used to ensure - * that the URLs used for sourcing Angular templates are safe. Refer {@link - * ng.$sceDelegateProvider#resourceUrlWhitelist $sceDelegateProvider.resourceUrlWhitelist} and - * {@link ng.$sceDelegateProvider#resourceUrlBlacklist $sceDelegateProvider.resourceUrlBlacklist} - * - * For the general details about this service in Angular, read the main page for {@link ng.$sce - * Strict Contextual Escaping (SCE)}. - * - * **Example**: Consider the following case. - * - * - your app is hosted at url `http://myapp.example.com/` - * - but some of your templates are hosted on other domains you control such as - * `http://srv01.assets.example.com/`,  `http://srv02.assets.example.com/`, etc. - * - and you have an open redirect at `http://myapp.example.com/clickThru?...`. - * - * Here is what a secure configuration for this scenario might look like: - * - * ``` - * angular.module('myApp', []).config(function($sceDelegateProvider) { - * $sceDelegateProvider.resourceUrlWhitelist([ - * // Allow same origin resource loads. - * 'self', - * // Allow loading from our assets domain. Notice the difference between * and **. - * 'http://srv*.assets.example.com/**' - * ]); - * - * // The blacklist overrides the whitelist so the open redirect here is blocked. - * $sceDelegateProvider.resourceUrlBlacklist([ - * 'http://myapp.example.com/clickThru**' - * ]); - * }); - * ``` - */ - -function $SceDelegateProvider() { - this.SCE_CONTEXTS = SCE_CONTEXTS; - - // Resource URLs can also be trusted by policy. - var resourceUrlWhitelist = ['self'], - resourceUrlBlacklist = []; - - /** - * @ngdoc method - * @name $sceDelegateProvider#resourceUrlWhitelist - * @kind function - * - * @param {Array=} whitelist When provided, replaces the resourceUrlWhitelist with the value - * provided. This must be an array or null. A snapshot of this array is used so further - * changes to the array are ignored. - * - * Follow {@link ng.$sce#resourceUrlPatternItem this link} for a description of the items - * allowed in this array. - * - * Note: **an empty whitelist array will block all URLs**! - * - * @return {Array} the currently set whitelist array. - * - * The **default value** when no whitelist has been explicitly set is `['self']` allowing only - * same origin resource requests. - * - * @description - * Sets/Gets the whitelist of trusted resource URLs. - */ - this.resourceUrlWhitelist = function(value) { - if (arguments.length) { - resourceUrlWhitelist = adjustMatchers(value); - } - return resourceUrlWhitelist; - }; - - /** - * @ngdoc method - * @name $sceDelegateProvider#resourceUrlBlacklist - * @kind function - * - * @param {Array=} blacklist When provided, replaces the resourceUrlBlacklist with the value - * provided. This must be an array or null. A snapshot of this array is used so further - * changes to the array are ignored. - * - * Follow {@link ng.$sce#resourceUrlPatternItem this link} for a description of the items - * allowed in this array. - * - * The typical usage for the blacklist is to **block - * [open redirects](http://cwe.mitre.org/data/definitions/601.html)** served by your domain as - * these would otherwise be trusted but actually return content from the redirected domain. - * - * Finally, **the blacklist overrides the whitelist** and has the final say. - * - * @return {Array} the currently set blacklist array. - * - * The **default value** when no whitelist has been explicitly set is the empty array (i.e. there - * is no blacklist.) - * - * @description - * Sets/Gets the blacklist of trusted resource URLs. - */ - - this.resourceUrlBlacklist = function(value) { - if (arguments.length) { - resourceUrlBlacklist = adjustMatchers(value); - } - return resourceUrlBlacklist; - }; - - this.$get = ['$injector', function($injector) { - - var htmlSanitizer = function htmlSanitizer(html) { - throw $sceMinErr('unsafe', 'Attempting to use an unsafe value in a safe context.'); - }; - - if ($injector.has('$sanitize')) { - htmlSanitizer = $injector.get('$sanitize'); - } - - - function matchUrl(matcher, parsedUrl) { - if (matcher === 'self') { - return urlIsSameOrigin(parsedUrl); - } else { - // definitely a regex. See adjustMatchers() - return !!matcher.exec(parsedUrl.href); - } - } - - function isResourceUrlAllowedByPolicy(url) { - var parsedUrl = urlResolve(url.toString()); - var i, n, allowed = false; - // Ensure that at least one item from the whitelist allows this url. - for (i = 0, n = resourceUrlWhitelist.length; i < n; i++) { - if (matchUrl(resourceUrlWhitelist[i], parsedUrl)) { - allowed = true; - break; - } - } - if (allowed) { - // Ensure that no item from the blacklist blocked this url. - for (i = 0, n = resourceUrlBlacklist.length; i < n; i++) { - if (matchUrl(resourceUrlBlacklist[i], parsedUrl)) { - allowed = false; - break; - } - } - } - return allowed; - } - - function generateHolderType(Base) { - var holderType = function TrustedValueHolderType(trustedValue) { - this.$$unwrapTrustedValue = function() { - return trustedValue; - }; - }; - if (Base) { - holderType.prototype = new Base(); - } - holderType.prototype.valueOf = function sceValueOf() { - return this.$$unwrapTrustedValue(); - }; - holderType.prototype.toString = function sceToString() { - return this.$$unwrapTrustedValue().toString(); - }; - return holderType; - } - - var trustedValueHolderBase = generateHolderType(), - byType = {}; - - byType[SCE_CONTEXTS.HTML] = generateHolderType(trustedValueHolderBase); - byType[SCE_CONTEXTS.CSS] = generateHolderType(trustedValueHolderBase); - byType[SCE_CONTEXTS.URL] = generateHolderType(trustedValueHolderBase); - byType[SCE_CONTEXTS.JS] = generateHolderType(trustedValueHolderBase); - byType[SCE_CONTEXTS.RESOURCE_URL] = generateHolderType(byType[SCE_CONTEXTS.URL]); - - /** - * @ngdoc method - * @name $sceDelegate#trustAs - * - * @description - * Returns an object that is trusted by angular for use in specified strict - * contextual escaping contexts (such as ng-bind-html, ng-include, any src - * attribute interpolation, any dom event binding attribute interpolation - * such as for onclick, etc.) that uses the provided value. - * See {@link ng.$sce $sce} for enabling strict contextual escaping. - * - * @param {string} type The kind of context in which this value is safe for use. e.g. url, - * resourceUrl, html, js and css. - * @param {*} value The value that that should be considered trusted/safe. - * @returns {*} A value that can be used to stand in for the provided `value` in places - * where Angular expects a $sce.trustAs() return value. - */ - function trustAs(type, trustedValue) { - var Constructor = (byType.hasOwnProperty(type) ? byType[type] : null); - if (!Constructor) { - throw $sceMinErr('icontext', - 'Attempted to trust a value in invalid context. Context: {0}; Value: {1}', - type, trustedValue); - } - if (trustedValue === null || trustedValue === undefined || trustedValue === '') { - return trustedValue; - } - // All the current contexts in SCE_CONTEXTS happen to be strings. In order to avoid trusting - // mutable objects, we ensure here that the value passed in is actually a string. - if (typeof trustedValue !== 'string') { - throw $sceMinErr('itype', - 'Attempted to trust a non-string value in a content requiring a string: Context: {0}', - type); - } - return new Constructor(trustedValue); - } - - /** - * @ngdoc method - * @name $sceDelegate#valueOf - * - * @description - * If the passed parameter had been returned by a prior call to {@link ng.$sceDelegate#trustAs - * `$sceDelegate.trustAs`}, returns the value that had been passed to {@link - * ng.$sceDelegate#trustAs `$sceDelegate.trustAs`}. - * - * If the passed parameter is not a value that had been returned by {@link - * ng.$sceDelegate#trustAs `$sceDelegate.trustAs`}, returns it as-is. - * - * @param {*} value The result of a prior {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs`} - * call or anything else. - * @returns {*} The `value` that was originally provided to {@link ng.$sceDelegate#trustAs - * `$sceDelegate.trustAs`} if `value` is the result of such a call. Otherwise, returns - * `value` unchanged. - */ - function valueOf(maybeTrusted) { - if (maybeTrusted instanceof trustedValueHolderBase) { - return maybeTrusted.$$unwrapTrustedValue(); - } else { - return maybeTrusted; - } - } - - /** - * @ngdoc method - * @name $sceDelegate#getTrusted - * - * @description - * Takes the result of a {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs`} call and - * returns the originally supplied value if the queried context type is a supertype of the - * created type. If this condition isn't satisfied, throws an exception. - * - * @param {string} type The kind of context in which this value is to be used. - * @param {*} maybeTrusted The result of a prior {@link ng.$sceDelegate#trustAs - * `$sceDelegate.trustAs`} call. - * @returns {*} The value the was originally provided to {@link ng.$sceDelegate#trustAs - * `$sceDelegate.trustAs`} if valid in this context. Otherwise, throws an exception. - */ - function getTrusted(type, maybeTrusted) { - if (maybeTrusted === null || maybeTrusted === undefined || maybeTrusted === '') { - return maybeTrusted; - } - var constructor = (byType.hasOwnProperty(type) ? byType[type] : null); - if (constructor && maybeTrusted instanceof constructor) { - return maybeTrusted.$$unwrapTrustedValue(); - } - // If we get here, then we may only take one of two actions. - // 1. sanitize the value for the requested type, or - // 2. throw an exception. - if (type === SCE_CONTEXTS.RESOURCE_URL) { - if (isResourceUrlAllowedByPolicy(maybeTrusted)) { - return maybeTrusted; - } else { - throw $sceMinErr('insecurl', - 'Blocked loading resource from url not allowed by $sceDelegate policy. URL: {0}', - maybeTrusted.toString()); - } - } else if (type === SCE_CONTEXTS.HTML) { - return htmlSanitizer(maybeTrusted); - } - throw $sceMinErr('unsafe', 'Attempting to use an unsafe value in a safe context.'); - } - - return { trustAs: trustAs, - getTrusted: getTrusted, - valueOf: valueOf }; - }]; -} - - -/** - * @ngdoc provider - * @name $sceProvider - * @description - * - * The $sceProvider provider allows developers to configure the {@link ng.$sce $sce} service. - * - enable/disable Strict Contextual Escaping (SCE) in a module - * - override the default implementation with a custom delegate - * - * Read more about {@link ng.$sce Strict Contextual Escaping (SCE)}. - */ - -/* jshint maxlen: false*/ - -/** - * @ngdoc service - * @name $sce - * @kind function - * - * @description - * - * `$sce` is a service that provides Strict Contextual Escaping services to AngularJS. - * - * # Strict Contextual Escaping - * - * Strict Contextual Escaping (SCE) is a mode in which AngularJS requires bindings in certain - * contexts to result in a value that is marked as safe to use for that context. One example of - * such a context is binding arbitrary html controlled by the user via `ng-bind-html`. We refer - * to these contexts as privileged or SCE contexts. - * - * As of version 1.2, Angular ships with SCE enabled by default. - * - * Note: When enabled (the default), IE<11 in quirks mode is not supported. In this mode, IE<11 allow - * one to execute arbitrary javascript by the use of the expression() syntax. Refer - * to learn more about them. - * You can ensure your document is in standards mode and not quirks mode by adding `` - * to the top of your HTML document. - * - * SCE assists in writing code in way that (a) is secure by default and (b) makes auditing for - * security vulnerabilities such as XSS, clickjacking, etc. a lot easier. - * - * Here's an example of a binding in a privileged context: - * - * ``` - * - *
    - * ``` - * - * Notice that `ng-bind-html` is bound to `userHtml` controlled by the user. With SCE - * disabled, this application allows the user to render arbitrary HTML into the DIV. - * In a more realistic example, one may be rendering user comments, blog articles, etc. via - * bindings. (HTML is just one example of a context where rendering user controlled input creates - * security vulnerabilities.) - * - * For the case of HTML, you might use a library, either on the client side, or on the server side, - * to sanitize unsafe HTML before binding to the value and rendering it in the document. - * - * How would you ensure that every place that used these types of bindings was bound to a value that - * was sanitized by your library (or returned as safe for rendering by your server?) How can you - * ensure that you didn't accidentally delete the line that sanitized the value, or renamed some - * properties/fields and forgot to update the binding to the sanitized value? - * - * To be secure by default, you want to ensure that any such bindings are disallowed unless you can - * determine that something explicitly says it's safe to use a value for binding in that - * context. You can then audit your code (a simple grep would do) to ensure that this is only done - * for those values that you can easily tell are safe - because they were received from your server, - * sanitized by your library, etc. You can organize your codebase to help with this - perhaps - * allowing only the files in a specific directory to do this. Ensuring that the internal API - * exposed by that code doesn't markup arbitrary values as safe then becomes a more manageable task. - * - * In the case of AngularJS' SCE service, one uses {@link ng.$sce#trustAs $sce.trustAs} - * (and shorthand methods such as {@link ng.$sce#trustAsHtml $sce.trustAsHtml}, etc.) to - * obtain values that will be accepted by SCE / privileged contexts. - * - * - * ## How does it work? - * - * In privileged contexts, directives and code will bind to the result of {@link ng.$sce#getTrusted - * $sce.getTrusted(context, value)} rather than to the value directly. Directives use {@link - * ng.$sce#parseAs $sce.parseAs} rather than `$parse` to watch attribute bindings, which performs the - * {@link ng.$sce#getTrusted $sce.getTrusted} behind the scenes on non-constant literals. - * - * As an example, {@link ng.directive:ngBindHtml ngBindHtml} uses {@link - * ng.$sce#parseAsHtml $sce.parseAsHtml(binding expression)}. Here's the actual code (slightly - * simplified): - * - * ``` - * var ngBindHtmlDirective = ['$sce', function($sce) { - * return function(scope, element, attr) { - * scope.$watch($sce.parseAsHtml(attr.ngBindHtml), function(value) { - * element.html(value || ''); - * }); - * }; - * }]; - * ``` - * - * ## Impact on loading templates - * - * This applies both to the {@link ng.directive:ngInclude `ng-include`} directive as well as - * `templateUrl`'s specified by {@link guide/directive directives}. - * - * By default, Angular only loads templates from the same domain and protocol as the application - * document. This is done by calling {@link ng.$sce#getTrustedResourceUrl - * $sce.getTrustedResourceUrl} on the template URL. To load templates from other domains and/or - * protocols, you may either either {@link ng.$sceDelegateProvider#resourceUrlWhitelist whitelist - * them} or {@link ng.$sce#trustAsResourceUrl wrap it} into a trusted value. - * - * *Please note*: - * The browser's - * [Same Origin Policy](https://code.google.com/p/browsersec/wiki/Part2#Same-origin_policy_for_XMLHttpRequest) - * and [Cross-Origin Resource Sharing (CORS)](http://www.w3.org/TR/cors/) - * policy apply in addition to this and may further restrict whether the template is successfully - * loaded. This means that without the right CORS policy, loading templates from a different domain - * won't work on all browsers. Also, loading templates from `file://` URL does not work on some - * browsers. - * - * ## This feels like too much overhead - * - * It's important to remember that SCE only applies to interpolation expressions. - * - * If your expressions are constant literals, they're automatically trusted and you don't need to - * call `$sce.trustAs` on them (remember to include the `ngSanitize` module) (e.g. - * `
    `) just works. - * - * Additionally, `a[href]` and `img[src]` automatically sanitize their URLs and do not pass them - * through {@link ng.$sce#getTrusted $sce.getTrusted}. SCE doesn't play a role here. - * - * The included {@link ng.$sceDelegate $sceDelegate} comes with sane defaults to allow you to load - * templates in `ng-include` from your application's domain without having to even know about SCE. - * It blocks loading templates from other domains or loading templates over http from an https - * served document. You can change these by setting your own custom {@link - * ng.$sceDelegateProvider#resourceUrlWhitelist whitelists} and {@link - * ng.$sceDelegateProvider#resourceUrlBlacklist blacklists} for matching such URLs. - * - * This significantly reduces the overhead. It is far easier to pay the small overhead and have an - * application that's secure and can be audited to verify that with much more ease than bolting - * security onto an application later. - * - * - * ## What trusted context types are supported? - * - * | Context | Notes | - * |---------------------|----------------| - * | `$sce.HTML` | For HTML that's safe to source into the application. The {@link ng.directive:ngBindHtml ngBindHtml} directive uses this context for bindings. If an unsafe value is encountered and the {@link ngSanitize $sanitize} module is present this will sanitize the value instead of throwing an error. | - * | `$sce.CSS` | For CSS that's safe to source into the application. Currently unused. Feel free to use it in your own directives. | - * | `$sce.URL` | For URLs that are safe to follow as links. Currently unused (`
    Note that `$sce.RESOURCE_URL` makes a stronger statement about the URL than `$sce.URL` does and therefore contexts requiring values trusted for `$sce.RESOURCE_URL` can be used anywhere that values trusted for `$sce.URL` are required. | - * | `$sce.JS` | For JavaScript that is safe to execute in your application's context. Currently unused. Feel free to use it in your own directives. | - * - * ## Format of items in {@link ng.$sceDelegateProvider#resourceUrlWhitelist resourceUrlWhitelist}/{@link ng.$sceDelegateProvider#resourceUrlBlacklist Blacklist}
    - * - * Each element in these arrays must be one of the following: - * - * - **'self'** - * - The special **string**, `'self'`, can be used to match against all URLs of the **same - * domain** as the application document using the **same protocol**. - * - **String** (except the special value `'self'`) - * - The string is matched against the full *normalized / absolute URL* of the resource - * being tested (substring matches are not good enough.) - * - There are exactly **two wildcard sequences** - `*` and `**`. All other characters - * match themselves. - * - `*`: matches zero or more occurrences of any character other than one of the following 6 - * characters: '`:`', '`/`', '`.`', '`?`', '`&`' and '`;`'. It's a useful wildcard for use - * in a whitelist. - * - `**`: matches zero or more occurrences of *any* character. As such, it's not - * appropriate for use in a scheme, domain, etc. as it would match too much. (e.g. - * http://**.example.com/ would match http://evil.com/?ignore=.example.com/ and that might - * not have been the intention.) Its usage at the very end of the path is ok. (e.g. - * http://foo.example.com/templates/**). - * - **RegExp** (*see caveat below*) - * - *Caveat*: While regular expressions are powerful and offer great flexibility, their syntax - * (and all the inevitable escaping) makes them *harder to maintain*. It's easy to - * accidentally introduce a bug when one updates a complex expression (imho, all regexes should - * have good test coverage). For instance, the use of `.` in the regex is correct only in a - * small number of cases. A `.` character in the regex used when matching the scheme or a - * subdomain could be matched against a `:` or literal `.` that was likely not intended. It - * is highly recommended to use the string patterns and only fall back to regular expressions - * as a last resort. - * - The regular expression must be an instance of RegExp (i.e. not a string.) It is - * matched against the **entire** *normalized / absolute URL* of the resource being tested - * (even when the RegExp did not have the `^` and `$` codes.) In addition, any flags - * present on the RegExp (such as multiline, global, ignoreCase) are ignored. - * - If you are generating your JavaScript from some other templating engine (not - * recommended, e.g. in issue [#4006](https://github.com/angular/angular.js/issues/4006)), - * remember to escape your regular expression (and be aware that you might need more than - * one level of escaping depending on your templating engine and the way you interpolated - * the value.) Do make use of your platform's escaping mechanism as it might be good - * enough before coding your own. E.g. Ruby has - * [Regexp.escape(str)](http://www.ruby-doc.org/core-2.0.0/Regexp.html#method-c-escape) - * and Python has [re.escape](http://docs.python.org/library/re.html#re.escape). - * Javascript lacks a similar built in function for escaping. Take a look at Google - * Closure library's [goog.string.regExpEscape(s)]( - * http://docs.closure-library.googlecode.com/git/closure_goog_string_string.js.source.html#line962). - * - * Refer {@link ng.$sceDelegateProvider $sceDelegateProvider} for an example. - * - * ## Show me an example using SCE. - * - * - * - *
    - *

    - * User comments
    - * By default, HTML that isn't explicitly trusted (e.g. Alice's comment) is sanitized when - * $sanitize is available. If $sanitize isn't available, this results in an error instead of an - * exploit. - *
    - *
    - * {{userComment.name}}: - * - *
    - *
    - *
    - *
    - *
    - * - * - * angular.module('mySceApp', ['ngSanitize']) - * .controller('AppController', ['$http', '$templateCache', '$sce', - * function($http, $templateCache, $sce) { - * var self = this; - * $http.get("test_data.json", {cache: $templateCache}).success(function(userComments) { - * self.userComments = userComments; - * }); - * self.explicitlyTrustedHtml = $sce.trustAsHtml( - * 'Hover over this text.'); - * }]); - * - * - * - * [ - * { "name": "Alice", - * "htmlComment": - * "Is anyone reading this?" - * }, - * { "name": "Bob", - * "htmlComment": "Yes! Am I the only other one?" - * } - * ] - * - * - * - * describe('SCE doc demo', function() { - * it('should sanitize untrusted values', function() { - * expect(element.all(by.css('.htmlComment')).first().getInnerHtml()) - * .toBe('Is anyone reading this?'); - * }); - * - * it('should NOT sanitize explicitly trusted values', function() { - * expect(element(by.id('explicitlyTrustedHtml')).getInnerHtml()).toBe( - * 'Hover over this text.'); - * }); - * }); - * - *
    - * - * - * - * ## Can I disable SCE completely? - * - * Yes, you can. However, this is strongly discouraged. SCE gives you a lot of security benefits - * for little coding overhead. It will be much harder to take an SCE disabled application and - * either secure it on your own or enable SCE at a later stage. It might make sense to disable SCE - * for cases where you have a lot of existing code that was written before SCE was introduced and - * you're migrating them a module at a time. - * - * That said, here's how you can completely disable SCE: - * - * ``` - * angular.module('myAppWithSceDisabledmyApp', []).config(function($sceProvider) { - * // Completely disable SCE. For demonstration purposes only! - * // Do not use in new projects. - * $sceProvider.enabled(false); - * }); - * ``` - * - */ -/* jshint maxlen: 100 */ - -function $SceProvider() { - var enabled = true; - - /** - * @ngdoc method - * @name $sceProvider#enabled - * @kind function - * - * @param {boolean=} value If provided, then enables/disables SCE. - * @return {boolean} true if SCE is enabled, false otherwise. - * - * @description - * Enables/disables SCE and returns the current value. - */ - this.enabled = function(value) { - if (arguments.length) { - enabled = !!value; - } - return enabled; - }; - - - /* Design notes on the default implementation for SCE. - * - * The API contract for the SCE delegate - * ------------------------------------- - * The SCE delegate object must provide the following 3 methods: - * - * - trustAs(contextEnum, value) - * This method is used to tell the SCE service that the provided value is OK to use in the - * contexts specified by contextEnum. It must return an object that will be accepted by - * getTrusted() for a compatible contextEnum and return this value. - * - * - valueOf(value) - * For values that were not produced by trustAs(), return them as is. For values that were - * produced by trustAs(), return the corresponding input value to trustAs. Basically, if - * trustAs is wrapping the given values into some type, this operation unwraps it when given - * such a value. - * - * - getTrusted(contextEnum, value) - * This function should return the a value that is safe to use in the context specified by - * contextEnum or throw and exception otherwise. - * - * NOTE: This contract deliberately does NOT state that values returned by trustAs() must be - * opaque or wrapped in some holder object. That happens to be an implementation detail. For - * instance, an implementation could maintain a registry of all trusted objects by context. In - * such a case, trustAs() would return the same object that was passed in. getTrusted() would - * return the same object passed in if it was found in the registry under a compatible context or - * throw an exception otherwise. An implementation might only wrap values some of the time based - * on some criteria. getTrusted() might return a value and not throw an exception for special - * constants or objects even if not wrapped. All such implementations fulfill this contract. - * - * - * A note on the inheritance model for SCE contexts - * ------------------------------------------------ - * I've used inheritance and made RESOURCE_URL wrapped types a subtype of URL wrapped types. This - * is purely an implementation details. - * - * The contract is simply this: - * - * getTrusted($sce.RESOURCE_URL, value) succeeding implies that getTrusted($sce.URL, value) - * will also succeed. - * - * Inheritance happens to capture this in a natural way. In some future, we - * may not use inheritance anymore. That is OK because no code outside of - * sce.js and sceSpecs.js would need to be aware of this detail. - */ - - this.$get = ['$parse', '$sceDelegate', function( - $parse, $sceDelegate) { - // Prereq: Ensure that we're not running in IE<11 quirks mode. In that mode, IE < 11 allow - // the "expression(javascript expression)" syntax which is insecure. - if (enabled && msie < 8) { - throw $sceMinErr('iequirks', - 'Strict Contextual Escaping does not support Internet Explorer version < 11 in quirks ' + - 'mode. You can fix this by adding the text to the top of your HTML ' + - 'document. See http://docs.angularjs.org/api/ng.$sce for more information.'); - } - - var sce = shallowCopy(SCE_CONTEXTS); - - /** - * @ngdoc method - * @name $sce#isEnabled - * @kind function - * - * @return {Boolean} true if SCE is enabled, false otherwise. If you want to set the value, you - * have to do it at module config time on {@link ng.$sceProvider $sceProvider}. - * - * @description - * Returns a boolean indicating if SCE is enabled. - */ - sce.isEnabled = function() { - return enabled; - }; - sce.trustAs = $sceDelegate.trustAs; - sce.getTrusted = $sceDelegate.getTrusted; - sce.valueOf = $sceDelegate.valueOf; - - if (!enabled) { - sce.trustAs = sce.getTrusted = function(type, value) { return value; }; - sce.valueOf = identity; - } - - /** - * @ngdoc method - * @name $sce#parseAs - * - * @description - * Converts Angular {@link guide/expression expression} into a function. This is like {@link - * ng.$parse $parse} and is identical when the expression is a literal constant. Otherwise, it - * wraps the expression in a call to {@link ng.$sce#getTrusted $sce.getTrusted(*type*, - * *result*)} - * - * @param {string} type The kind of SCE context in which this result will be used. - * @param {string} expression String expression to compile. - * @returns {function(context, locals)} a function which represents the compiled expression: - * - * * `context` – `{object}` – an object against which any expressions embedded in the strings - * are evaluated against (typically a scope object). - * * `locals` – `{object=}` – local variables context object, useful for overriding values in - * `context`. - */ - sce.parseAs = function sceParseAs(type, expr) { - var parsed = $parse(expr); - if (parsed.literal && parsed.constant) { - return parsed; - } else { - return $parse(expr, function(value) { - return sce.getTrusted(type, value); - }); - } - }; - - /** - * @ngdoc method - * @name $sce#trustAs - * - * @description - * Delegates to {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs`}. As such, - * returns an object that is trusted by angular for use in specified strict contextual - * escaping contexts (such as ng-bind-html, ng-include, any src attribute - * interpolation, any dom event binding attribute interpolation such as for onclick, etc.) - * that uses the provided value. See * {@link ng.$sce $sce} for enabling strict contextual - * escaping. - * - * @param {string} type The kind of context in which this value is safe for use. e.g. url, - * resourceUrl, html, js and css. - * @param {*} value The value that that should be considered trusted/safe. - * @returns {*} A value that can be used to stand in for the provided `value` in places - * where Angular expects a $sce.trustAs() return value. - */ - - /** - * @ngdoc method - * @name $sce#trustAsHtml - * - * @description - * Shorthand method. `$sce.trustAsHtml(value)` → - * {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs($sce.HTML, value)`} - * - * @param {*} value The value to trustAs. - * @returns {*} An object that can be passed to {@link ng.$sce#getTrustedHtml - * $sce.getTrustedHtml(value)} to obtain the original value. (privileged directives - * only accept expressions that are either literal constants or are the - * return value of {@link ng.$sce#trustAs $sce.trustAs}.) - */ - - /** - * @ngdoc method - * @name $sce#trustAsUrl - * - * @description - * Shorthand method. `$sce.trustAsUrl(value)` → - * {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs($sce.URL, value)`} - * - * @param {*} value The value to trustAs. - * @returns {*} An object that can be passed to {@link ng.$sce#getTrustedUrl - * $sce.getTrustedUrl(value)} to obtain the original value. (privileged directives - * only accept expressions that are either literal constants or are the - * return value of {@link ng.$sce#trustAs $sce.trustAs}.) - */ - - /** - * @ngdoc method - * @name $sce#trustAsResourceUrl - * - * @description - * Shorthand method. `$sce.trustAsResourceUrl(value)` → - * {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs($sce.RESOURCE_URL, value)`} - * - * @param {*} value The value to trustAs. - * @returns {*} An object that can be passed to {@link ng.$sce#getTrustedResourceUrl - * $sce.getTrustedResourceUrl(value)} to obtain the original value. (privileged directives - * only accept expressions that are either literal constants or are the return - * value of {@link ng.$sce#trustAs $sce.trustAs}.) - */ - - /** - * @ngdoc method - * @name $sce#trustAsJs - * - * @description - * Shorthand method. `$sce.trustAsJs(value)` → - * {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs($sce.JS, value)`} - * - * @param {*} value The value to trustAs. - * @returns {*} An object that can be passed to {@link ng.$sce#getTrustedJs - * $sce.getTrustedJs(value)} to obtain the original value. (privileged directives - * only accept expressions that are either literal constants or are the - * return value of {@link ng.$sce#trustAs $sce.trustAs}.) - */ - - /** - * @ngdoc method - * @name $sce#getTrusted - * - * @description - * Delegates to {@link ng.$sceDelegate#getTrusted `$sceDelegate.getTrusted`}. As such, - * takes the result of a {@link ng.$sce#trustAs `$sce.trustAs`}() call and returns the - * originally supplied value if the queried context type is a supertype of the created type. - * If this condition isn't satisfied, throws an exception. - * - * @param {string} type The kind of context in which this value is to be used. - * @param {*} maybeTrusted The result of a prior {@link ng.$sce#trustAs `$sce.trustAs`} - * call. - * @returns {*} The value the was originally provided to - * {@link ng.$sce#trustAs `$sce.trustAs`} if valid in this context. - * Otherwise, throws an exception. - */ - - /** - * @ngdoc method - * @name $sce#getTrustedHtml - * - * @description - * Shorthand method. `$sce.getTrustedHtml(value)` → - * {@link ng.$sceDelegate#getTrusted `$sceDelegate.getTrusted($sce.HTML, value)`} - * - * @param {*} value The value to pass to `$sce.getTrusted`. - * @returns {*} The return value of `$sce.getTrusted($sce.HTML, value)` - */ - - /** - * @ngdoc method - * @name $sce#getTrustedCss - * - * @description - * Shorthand method. `$sce.getTrustedCss(value)` → - * {@link ng.$sceDelegate#getTrusted `$sceDelegate.getTrusted($sce.CSS, value)`} - * - * @param {*} value The value to pass to `$sce.getTrusted`. - * @returns {*} The return value of `$sce.getTrusted($sce.CSS, value)` - */ - - /** - * @ngdoc method - * @name $sce#getTrustedUrl - * - * @description - * Shorthand method. `$sce.getTrustedUrl(value)` → - * {@link ng.$sceDelegate#getTrusted `$sceDelegate.getTrusted($sce.URL, value)`} - * - * @param {*} value The value to pass to `$sce.getTrusted`. - * @returns {*} The return value of `$sce.getTrusted($sce.URL, value)` - */ - - /** - * @ngdoc method - * @name $sce#getTrustedResourceUrl - * - * @description - * Shorthand method. `$sce.getTrustedResourceUrl(value)` → - * {@link ng.$sceDelegate#getTrusted `$sceDelegate.getTrusted($sce.RESOURCE_URL, value)`} - * - * @param {*} value The value to pass to `$sceDelegate.getTrusted`. - * @returns {*} The return value of `$sce.getTrusted($sce.RESOURCE_URL, value)` - */ - - /** - * @ngdoc method - * @name $sce#getTrustedJs - * - * @description - * Shorthand method. `$sce.getTrustedJs(value)` → - * {@link ng.$sceDelegate#getTrusted `$sceDelegate.getTrusted($sce.JS, value)`} - * - * @param {*} value The value to pass to `$sce.getTrusted`. - * @returns {*} The return value of `$sce.getTrusted($sce.JS, value)` - */ - - /** - * @ngdoc method - * @name $sce#parseAsHtml - * - * @description - * Shorthand method. `$sce.parseAsHtml(expression string)` → - * {@link ng.$sce#parseAs `$sce.parseAs($sce.HTML, value)`} - * - * @param {string} expression String expression to compile. - * @returns {function(context, locals)} a function which represents the compiled expression: - * - * * `context` – `{object}` – an object against which any expressions embedded in the strings - * are evaluated against (typically a scope object). - * * `locals` – `{object=}` – local variables context object, useful for overriding values in - * `context`. - */ - - /** - * @ngdoc method - * @name $sce#parseAsCss - * - * @description - * Shorthand method. `$sce.parseAsCss(value)` → - * {@link ng.$sce#parseAs `$sce.parseAs($sce.CSS, value)`} - * - * @param {string} expression String expression to compile. - * @returns {function(context, locals)} a function which represents the compiled expression: - * - * * `context` – `{object}` – an object against which any expressions embedded in the strings - * are evaluated against (typically a scope object). - * * `locals` – `{object=}` – local variables context object, useful for overriding values in - * `context`. - */ - - /** - * @ngdoc method - * @name $sce#parseAsUrl - * - * @description - * Shorthand method. `$sce.parseAsUrl(value)` → - * {@link ng.$sce#parseAs `$sce.parseAs($sce.URL, value)`} - * - * @param {string} expression String expression to compile. - * @returns {function(context, locals)} a function which represents the compiled expression: - * - * * `context` – `{object}` – an object against which any expressions embedded in the strings - * are evaluated against (typically a scope object). - * * `locals` – `{object=}` – local variables context object, useful for overriding values in - * `context`. - */ - - /** - * @ngdoc method - * @name $sce#parseAsResourceUrl - * - * @description - * Shorthand method. `$sce.parseAsResourceUrl(value)` → - * {@link ng.$sce#parseAs `$sce.parseAs($sce.RESOURCE_URL, value)`} - * - * @param {string} expression String expression to compile. - * @returns {function(context, locals)} a function which represents the compiled expression: - * - * * `context` – `{object}` – an object against which any expressions embedded in the strings - * are evaluated against (typically a scope object). - * * `locals` – `{object=}` – local variables context object, useful for overriding values in - * `context`. - */ - - /** - * @ngdoc method - * @name $sce#parseAsJs - * - * @description - * Shorthand method. `$sce.parseAsJs(value)` → - * {@link ng.$sce#parseAs `$sce.parseAs($sce.JS, value)`} - * - * @param {string} expression String expression to compile. - * @returns {function(context, locals)} a function which represents the compiled expression: - * - * * `context` – `{object}` – an object against which any expressions embedded in the strings - * are evaluated against (typically a scope object). - * * `locals` – `{object=}` – local variables context object, useful for overriding values in - * `context`. - */ - - // Shorthand delegations. - var parse = sce.parseAs, - getTrusted = sce.getTrusted, - trustAs = sce.trustAs; - - forEach(SCE_CONTEXTS, function(enumValue, name) { - var lName = lowercase(name); - sce[camelCase("parse_as_" + lName)] = function(expr) { - return parse(enumValue, expr); - }; - sce[camelCase("get_trusted_" + lName)] = function(value) { - return getTrusted(enumValue, value); - }; - sce[camelCase("trust_as_" + lName)] = function(value) { - return trustAs(enumValue, value); - }; - }); - - return sce; - }]; -} - -/** - * !!! This is an undocumented "private" service !!! - * - * @name $sniffer - * @requires $window - * @requires $document - * - * @property {boolean} history Does the browser support html5 history api ? - * @property {boolean} transitions Does the browser support CSS transition events ? - * @property {boolean} animations Does the browser support CSS animation events ? - * - * @description - * This is very simple implementation of testing browser's features. - */ -function $SnifferProvider() { - this.$get = ['$window', '$document', function($window, $document) { - var eventSupport = {}, - android = - toInt((/android (\d+)/.exec(lowercase(($window.navigator || {}).userAgent)) || [])[1]), - boxee = /Boxee/i.test(($window.navigator || {}).userAgent), - document = $document[0] || {}, - vendorPrefix, - vendorRegex = /^(Moz|webkit|ms)(?=[A-Z])/, - bodyStyle = document.body && document.body.style, - transitions = false, - animations = false, - match; - - if (bodyStyle) { - for (var prop in bodyStyle) { - if (match = vendorRegex.exec(prop)) { - vendorPrefix = match[0]; - vendorPrefix = vendorPrefix.substr(0, 1).toUpperCase() + vendorPrefix.substr(1); - break; - } - } - - if (!vendorPrefix) { - vendorPrefix = ('WebkitOpacity' in bodyStyle) && 'webkit'; - } - - transitions = !!(('transition' in bodyStyle) || (vendorPrefix + 'Transition' in bodyStyle)); - animations = !!(('animation' in bodyStyle) || (vendorPrefix + 'Animation' in bodyStyle)); - - if (android && (!transitions || !animations)) { - transitions = isString(bodyStyle.webkitTransition); - animations = isString(bodyStyle.webkitAnimation); - } - } - - - return { - // Android has history.pushState, but it does not update location correctly - // so let's not use the history API at all. - // http://code.google.com/p/android/issues/detail?id=17471 - // https://github.com/angular/angular.js/issues/904 - - // older webkit browser (533.9) on Boxee box has exactly the same problem as Android has - // so let's not use the history API also - // We are purposefully using `!(android < 4)` to cover the case when `android` is undefined - // jshint -W018 - history: !!($window.history && $window.history.pushState && !(android < 4) && !boxee), - // jshint +W018 - hasEvent: function(event) { - // IE9 implements 'input' event it's so fubared that we rather pretend that it doesn't have - // it. In particular the event is not fired when backspace or delete key are pressed or - // when cut operation is performed. - // IE10+ implements 'input' event but it erroneously fires under various situations, - // e.g. when placeholder changes, or a form is focused. - if (event === 'input' && msie <= 11) return false; - - if (isUndefined(eventSupport[event])) { - var divElm = document.createElement('div'); - eventSupport[event] = 'on' + event in divElm; - } - - return eventSupport[event]; - }, - csp: csp(), - vendorPrefix: vendorPrefix, - transitions: transitions, - animations: animations, - android: android - }; - }]; -} - -var $compileMinErr = minErr('$compile'); - -/** - * @ngdoc service - * @name $templateRequest - * - * @description - * The `$templateRequest` service runs security checks then downloads the provided template using - * `$http` and, upon success, stores the contents inside of `$templateCache`. If the HTTP request - * fails or the response data of the HTTP request is empty, a `$compile` error will be thrown (the - * exception can be thwarted by setting the 2nd parameter of the function to true). Note that the - * contents of `$templateCache` are trusted, so the call to `$sce.getTrustedUrl(tpl)` is omitted - * when `tpl` is of type string and `$templateCache` has the matching entry. - * - * @param {string|TrustedResourceUrl} tpl The HTTP request template URL - * @param {boolean=} ignoreRequestError Whether or not to ignore the exception when the request fails or the template is empty - * - * @return {Promise} a promise for the HTTP response data of the given URL. - * - * @property {number} totalPendingRequests total amount of pending template requests being downloaded. - */ -function $TemplateRequestProvider() { - this.$get = ['$templateCache', '$http', '$q', '$sce', function($templateCache, $http, $q, $sce) { - function handleRequestFn(tpl, ignoreRequestError) { - handleRequestFn.totalPendingRequests++; - - // We consider the template cache holds only trusted templates, so - // there's no need to go through whitelisting again for keys that already - // are included in there. This also makes Angular accept any script - // directive, no matter its name. However, we still need to unwrap trusted - // types. - if (!isString(tpl) || !$templateCache.get(tpl)) { - tpl = $sce.getTrustedResourceUrl(tpl); - } - - var transformResponse = $http.defaults && $http.defaults.transformResponse; - - if (isArray(transformResponse)) { - transformResponse = transformResponse.filter(function(transformer) { - return transformer !== defaultHttpResponseTransform; - }); - } else if (transformResponse === defaultHttpResponseTransform) { - transformResponse = null; - } - - var httpOptions = { - cache: $templateCache, - transformResponse: transformResponse - }; - - return $http.get(tpl, httpOptions) - ['finally'](function() { - handleRequestFn.totalPendingRequests--; - }) - .then(function(response) { - $templateCache.put(tpl, response.data); - return response.data; - }, handleError); - - function handleError(resp) { - if (!ignoreRequestError) { - throw $compileMinErr('tpload', 'Failed to load template: {0} (HTTP status: {1} {2})', - tpl, resp.status, resp.statusText); - } - return $q.reject(resp); - } - } - - handleRequestFn.totalPendingRequests = 0; - - return handleRequestFn; - }]; -} - -function $$TestabilityProvider() { - this.$get = ['$rootScope', '$browser', '$location', - function($rootScope, $browser, $location) { - - /** - * @name $testability - * - * @description - * The private $$testability service provides a collection of methods for use when debugging - * or by automated test and debugging tools. - */ - var testability = {}; - - /** - * @name $$testability#findBindings - * - * @description - * Returns an array of elements that are bound (via ng-bind or {{}}) - * to expressions matching the input. - * - * @param {Element} element The element root to search from. - * @param {string} expression The binding expression to match. - * @param {boolean} opt_exactMatch If true, only returns exact matches - * for the expression. Filters and whitespace are ignored. - */ - testability.findBindings = function(element, expression, opt_exactMatch) { - var bindings = element.getElementsByClassName('ng-binding'); - var matches = []; - forEach(bindings, function(binding) { - var dataBinding = angular.element(binding).data('$binding'); - if (dataBinding) { - forEach(dataBinding, function(bindingName) { - if (opt_exactMatch) { - var matcher = new RegExp('(^|\\s)' + escapeForRegexp(expression) + '(\\s|\\||$)'); - if (matcher.test(bindingName)) { - matches.push(binding); - } - } else { - if (bindingName.indexOf(expression) != -1) { - matches.push(binding); - } - } - }); - } - }); - return matches; - }; - - /** - * @name $$testability#findModels - * - * @description - * Returns an array of elements that are two-way found via ng-model to - * expressions matching the input. - * - * @param {Element} element The element root to search from. - * @param {string} expression The model expression to match. - * @param {boolean} opt_exactMatch If true, only returns exact matches - * for the expression. - */ - testability.findModels = function(element, expression, opt_exactMatch) { - var prefixes = ['ng-', 'data-ng-', 'ng\\:']; - for (var p = 0; p < prefixes.length; ++p) { - var attributeEquals = opt_exactMatch ? '=' : '*='; - var selector = '[' + prefixes[p] + 'model' + attributeEquals + '"' + expression + '"]'; - var elements = element.querySelectorAll(selector); - if (elements.length) { - return elements; - } - } - }; - - /** - * @name $$testability#getLocation - * - * @description - * Shortcut for getting the location in a browser agnostic way. Returns - * the path, search, and hash. (e.g. /path?a=b#hash) - */ - testability.getLocation = function() { - return $location.url(); - }; - - /** - * @name $$testability#setLocation - * - * @description - * Shortcut for navigating to a location without doing a full page reload. - * - * @param {string} url The location url (path, search and hash, - * e.g. /path?a=b#hash) to go to. - */ - testability.setLocation = function(url) { - if (url !== $location.url()) { - $location.url(url); - $rootScope.$digest(); - } - }; - - /** - * @name $$testability#whenStable - * - * @description - * Calls the callback when $timeout and $http requests are completed. - * - * @param {function} callback - */ - testability.whenStable = function(callback) { - $browser.notifyWhenNoOutstandingRequests(callback); - }; - - return testability; - }]; -} - -function $TimeoutProvider() { - this.$get = ['$rootScope', '$browser', '$q', '$$q', '$exceptionHandler', - function($rootScope, $browser, $q, $$q, $exceptionHandler) { - - var deferreds = {}; - - - /** - * @ngdoc service - * @name $timeout - * - * @description - * Angular's wrapper for `window.setTimeout`. The `fn` function is wrapped into a try/catch - * block and delegates any exceptions to - * {@link ng.$exceptionHandler $exceptionHandler} service. - * - * The return value of calling `$timeout` is a promise, which will be resolved when - * the delay has passed and the timeout function, if provided, is executed. - * - * To cancel a timeout request, call `$timeout.cancel(promise)`. - * - * In tests you can use {@link ngMock.$timeout `$timeout.flush()`} to - * synchronously flush the queue of deferred functions. - * - * If you only want a promise that will be resolved after some specified delay - * then you can call `$timeout` without the `fn` function. - * - * @param {function()=} fn A function, whose execution should be delayed. - * @param {number=} [delay=0] Delay in milliseconds. - * @param {boolean=} [invokeApply=true] If set to `false` skips model dirty checking, otherwise - * will invoke `fn` within the {@link ng.$rootScope.Scope#$apply $apply} block. - * @param {...*=} Pass additional parameters to the executed function. - * @returns {Promise} Promise that will be resolved when the timeout is reached. The value this - * promise will be resolved with is the return value of the `fn` function. - * - */ - function timeout(fn, delay, invokeApply) { - if (!isFunction(fn)) { - invokeApply = delay; - delay = fn; - fn = noop; - } - - var args = sliceArgs(arguments, 3), - skipApply = (isDefined(invokeApply) && !invokeApply), - deferred = (skipApply ? $$q : $q).defer(), - promise = deferred.promise, - timeoutId; - - timeoutId = $browser.defer(function() { - try { - deferred.resolve(fn.apply(null, args)); - } catch (e) { - deferred.reject(e); - $exceptionHandler(e); - } - finally { - delete deferreds[promise.$$timeoutId]; - } - - if (!skipApply) $rootScope.$apply(); - }, delay); - - promise.$$timeoutId = timeoutId; - deferreds[timeoutId] = deferred; - - return promise; - } - - - /** - * @ngdoc method - * @name $timeout#cancel - * - * @description - * Cancels a task associated with the `promise`. As a result of this, the promise will be - * resolved with a rejection. - * - * @param {Promise=} promise Promise returned by the `$timeout` function. - * @returns {boolean} Returns `true` if the task hasn't executed yet and was successfully - * canceled. - */ - timeout.cancel = function(promise) { - if (promise && promise.$$timeoutId in deferreds) { - deferreds[promise.$$timeoutId].reject('canceled'); - delete deferreds[promise.$$timeoutId]; - return $browser.defer.cancel(promise.$$timeoutId); - } - return false; - }; - - return timeout; - }]; -} - -// NOTE: The usage of window and document instead of $window and $document here is -// deliberate. This service depends on the specific behavior of anchor nodes created by the -// browser (resolving and parsing URLs) that is unlikely to be provided by mock objects and -// cause us to break tests. In addition, when the browser resolves a URL for XHR, it -// doesn't know about mocked locations and resolves URLs to the real document - which is -// exactly the behavior needed here. There is little value is mocking these out for this -// service. -var urlParsingNode = document.createElement("a"); -var originUrl = urlResolve(window.location.href); - - -/** - * - * Implementation Notes for non-IE browsers - * ---------------------------------------- - * Assigning a URL to the href property of an anchor DOM node, even one attached to the DOM, - * results both in the normalizing and parsing of the URL. Normalizing means that a relative - * URL will be resolved into an absolute URL in the context of the application document. - * Parsing means that the anchor node's host, hostname, protocol, port, pathname and related - * properties are all populated to reflect the normalized URL. This approach has wide - * compatibility - Safari 1+, Mozilla 1+, Opera 7+,e etc. See - * http://www.aptana.com/reference/html/api/HTMLAnchorElement.html - * - * Implementation Notes for IE - * --------------------------- - * IE <= 10 normalizes the URL when assigned to the anchor node similar to the other - * browsers. However, the parsed components will not be set if the URL assigned did not specify - * them. (e.g. if you assign a.href = "foo", then a.protocol, a.host, etc. will be empty.) We - * work around that by performing the parsing in a 2nd step by taking a previously normalized - * URL (e.g. by assigning to a.href) and assigning it a.href again. This correctly populates the - * properties such as protocol, hostname, port, etc. - * - * References: - * http://developer.mozilla.org/en-US/docs/Web/API/HTMLAnchorElement - * http://www.aptana.com/reference/html/api/HTMLAnchorElement.html - * http://url.spec.whatwg.org/#urlutils - * https://github.com/angular/angular.js/pull/2902 - * http://james.padolsey.com/javascript/parsing-urls-with-the-dom/ - * - * @kind function - * @param {string} url The URL to be parsed. - * @description Normalizes and parses a URL. - * @returns {object} Returns the normalized URL as a dictionary. - * - * | member name | Description | - * |---------------|----------------| - * | href | A normalized version of the provided URL if it was not an absolute URL | - * | protocol | The protocol including the trailing colon | - * | host | The host and port (if the port is non-default) of the normalizedUrl | - * | search | The search params, minus the question mark | - * | hash | The hash string, minus the hash symbol - * | hostname | The hostname - * | port | The port, without ":" - * | pathname | The pathname, beginning with "/" - * - */ -function urlResolve(url) { - var href = url; - - if (msie) { - // Normalize before parse. Refer Implementation Notes on why this is - // done in two steps on IE. - urlParsingNode.setAttribute("href", href); - href = urlParsingNode.href; - } - - urlParsingNode.setAttribute('href', href); - - // urlParsingNode provides the UrlUtils interface - http://url.spec.whatwg.org/#urlutils - return { - href: urlParsingNode.href, - protocol: urlParsingNode.protocol ? urlParsingNode.protocol.replace(/:$/, '') : '', - host: urlParsingNode.host, - search: urlParsingNode.search ? urlParsingNode.search.replace(/^\?/, '') : '', - hash: urlParsingNode.hash ? urlParsingNode.hash.replace(/^#/, '') : '', - hostname: urlParsingNode.hostname, - port: urlParsingNode.port, - pathname: (urlParsingNode.pathname.charAt(0) === '/') - ? urlParsingNode.pathname - : '/' + urlParsingNode.pathname - }; -} - -/** - * Parse a request URL and determine whether this is a same-origin request as the application document. - * - * @param {string|object} requestUrl The url of the request as a string that will be resolved - * or a parsed URL object. - * @returns {boolean} Whether the request is for the same origin as the application document. - */ -function urlIsSameOrigin(requestUrl) { - var parsed = (isString(requestUrl)) ? urlResolve(requestUrl) : requestUrl; - return (parsed.protocol === originUrl.protocol && - parsed.host === originUrl.host); -} - -/** - * @ngdoc service - * @name $window - * - * @description - * A reference to the browser's `window` object. While `window` - * is globally available in JavaScript, it causes testability problems, because - * it is a global variable. In angular we always refer to it through the - * `$window` service, so it may be overridden, removed or mocked for testing. - * - * Expressions, like the one defined for the `ngClick` directive in the example - * below, are evaluated with respect to the current scope. Therefore, there is - * no risk of inadvertently coding in a dependency on a global value in such an - * expression. - * - * @example - - - -
    - - -
    -
    - - it('should display the greeting in the input box', function() { - element(by.model('greeting')).sendKeys('Hello, E2E Tests'); - // If we click the button it will block the test runner - // element(':button').click(); - }); - -
    - */ -function $WindowProvider() { - this.$get = valueFn(window); -} - -/** - * @name $$cookieReader - * @requires $document - * - * @description - * This is a private service for reading cookies used by $http and ngCookies - * - * @return {Object} a key/value map of the current cookies - */ -function $$CookieReader($document) { - var rawDocument = $document[0] || {}; - var lastCookies = {}; - var lastCookieString = ''; - - function safeDecodeURIComponent(str) { - try { - return decodeURIComponent(str); - } catch (e) { - return str; - } - } - - return function() { - var cookieArray, cookie, i, index, name; - var currentCookieString = rawDocument.cookie || ''; - - if (currentCookieString !== lastCookieString) { - lastCookieString = currentCookieString; - cookieArray = lastCookieString.split('; '); - lastCookies = {}; - - for (i = 0; i < cookieArray.length; i++) { - cookie = cookieArray[i]; - index = cookie.indexOf('='); - if (index > 0) { //ignore nameless cookies - name = safeDecodeURIComponent(cookie.substring(0, index)); - // the first value that is seen for a cookie is the most - // specific one. values for the same cookie name that - // follow are for less specific paths. - if (lastCookies[name] === undefined) { - lastCookies[name] = safeDecodeURIComponent(cookie.substring(index + 1)); - } - } - } - } - return lastCookies; - }; -} - -$$CookieReader.$inject = ['$document']; - -function $$CookieReaderProvider() { - this.$get = $$CookieReader; -} - -/* global currencyFilter: true, - dateFilter: true, - filterFilter: true, - jsonFilter: true, - limitToFilter: true, - lowercaseFilter: true, - numberFilter: true, - orderByFilter: true, - uppercaseFilter: true, - */ - -/** - * @ngdoc provider - * @name $filterProvider - * @description - * - * Filters are just functions which transform input to an output. However filters need to be - * Dependency Injected. To achieve this a filter definition consists of a factory function which is - * annotated with dependencies and is responsible for creating a filter function. - * - *
    - * **Note:** Filter names must be valid angular {@link expression} identifiers, such as `uppercase` or `orderBy`. - * Names with special characters, such as hyphens and dots, are not allowed. If you wish to namespace - * your filters, then you can use capitalization (`myappSubsectionFilterx`) or underscores - * (`myapp_subsection_filterx`). - *
    - * - * ```js - * // Filter registration - * function MyModule($provide, $filterProvider) { - * // create a service to demonstrate injection (not always needed) - * $provide.value('greet', function(name){ - * return 'Hello ' + name + '!'; - * }); - * - * // register a filter factory which uses the - * // greet service to demonstrate DI. - * $filterProvider.register('greet', function(greet){ - * // return the filter function which uses the greet service - * // to generate salutation - * return function(text) { - * // filters need to be forgiving so check input validity - * return text && greet(text) || text; - * }; - * }); - * } - * ``` - * - * The filter function is registered with the `$injector` under the filter name suffix with - * `Filter`. - * - * ```js - * it('should be the same instance', inject( - * function($filterProvider) { - * $filterProvider.register('reverse', function(){ - * return ...; - * }); - * }, - * function($filter, reverseFilter) { - * expect($filter('reverse')).toBe(reverseFilter); - * }); - * ``` - * - * - * For more information about how angular filters work, and how to create your own filters, see - * {@link guide/filter Filters} in the Angular Developer Guide. - */ - -/** - * @ngdoc service - * @name $filter - * @kind function - * @description - * Filters are used for formatting data displayed to the user. - * - * The general syntax in templates is as follows: - * - * {{ expression [| filter_name[:parameter_value] ... ] }} - * - * @param {String} name Name of the filter function to retrieve - * @return {Function} the filter function - * @example - - -
    -

    {{ originalText }}

    -

    {{ filteredText }}

    -
    -
    - - - angular.module('filterExample', []) - .controller('MainCtrl', function($scope, $filter) { - $scope.originalText = 'hello'; - $scope.filteredText = $filter('uppercase')($scope.originalText); - }); - -
    - */ -$FilterProvider.$inject = ['$provide']; -function $FilterProvider($provide) { - var suffix = 'Filter'; - - /** - * @ngdoc method - * @name $filterProvider#register - * @param {string|Object} name Name of the filter function, or an object map of filters where - * the keys are the filter names and the values are the filter factories. - * - *
    - * **Note:** Filter names must be valid angular {@link expression} identifiers, such as `uppercase` or `orderBy`. - * Names with special characters, such as hyphens and dots, are not allowed. If you wish to namespace - * your filters, then you can use capitalization (`myappSubsectionFilterx`) or underscores - * (`myapp_subsection_filterx`). - *
    - * @param {Function} factory If the first argument was a string, a factory function for the filter to be registered. - * @returns {Object} Registered filter instance, or if a map of filters was provided then a map - * of the registered filter instances. - */ - function register(name, factory) { - if (isObject(name)) { - var filters = {}; - forEach(name, function(filter, key) { - filters[key] = register(key, filter); - }); - return filters; - } else { - return $provide.factory(name + suffix, factory); - } - } - this.register = register; - - this.$get = ['$injector', function($injector) { - return function(name) { - return $injector.get(name + suffix); - }; - }]; - - //////////////////////////////////////// - - /* global - currencyFilter: false, - dateFilter: false, - filterFilter: false, - jsonFilter: false, - limitToFilter: false, - lowercaseFilter: false, - numberFilter: false, - orderByFilter: false, - uppercaseFilter: false, - */ - - register('currency', currencyFilter); - register('date', dateFilter); - register('filter', filterFilter); - register('json', jsonFilter); - register('limitTo', limitToFilter); - register('lowercase', lowercaseFilter); - register('number', numberFilter); - register('orderBy', orderByFilter); - register('uppercase', uppercaseFilter); -} - -/** - * @ngdoc filter - * @name filter - * @kind function - * - * @description - * Selects a subset of items from `array` and returns it as a new array. - * - * @param {Array} array The source array. - * @param {string|Object|function()} expression The predicate to be used for selecting items from - * `array`. - * - * Can be one of: - * - * - `string`: The string is used for matching against the contents of the `array`. All strings or - * objects with string properties in `array` that match this string will be returned. This also - * applies to nested object properties. - * The predicate can be negated by prefixing the string with `!`. - * - * - `Object`: A pattern object can be used to filter specific properties on objects contained - * by `array`. For example `{name:"M", phone:"1"}` predicate will return an array of items - * which have property `name` containing "M" and property `phone` containing "1". A special - * property name `$` can be used (as in `{$:"text"}`) to accept a match against any - * property of the object or its nested object properties. That's equivalent to the simple - * substring match with a `string` as described above. The predicate can be negated by prefixing - * the string with `!`. - * For example `{name: "!M"}` predicate will return an array of items which have property `name` - * not containing "M". - * - * Note that a named property will match properties on the same level only, while the special - * `$` property will match properties on the same level or deeper. E.g. an array item like - * `{name: {first: 'John', last: 'Doe'}}` will **not** be matched by `{name: 'John'}`, but - * **will** be matched by `{$: 'John'}`. - * - * - `function(value, index, array)`: A predicate function can be used to write arbitrary filters. - * The function is called for each element of the array, with the element, its index, and - * the entire array itself as arguments. - * - * The final result is an array of those elements that the predicate returned true for. - * - * @param {function(actual, expected)|true|undefined} comparator Comparator which is used in - * determining if the expected value (from the filter expression) and actual value (from - * the object in the array) should be considered a match. - * - * Can be one of: - * - * - `function(actual, expected)`: - * The function will be given the object value and the predicate value to compare and - * should return true if both values should be considered equal. - * - * - `true`: A shorthand for `function(actual, expected) { return angular.equals(actual, expected)}`. - * This is essentially strict comparison of expected and actual. - * - * - `false|undefined`: A short hand for a function which will look for a substring match in case - * insensitive way. - * - * Primitive values are converted to strings. Objects are not compared against primitives, - * unless they have a custom `toString` method (e.g. `Date` objects). - * - * @example - - -
    - - - - - - - - -
    NamePhone
    {{friend.name}}{{friend.phone}}
    -
    -
    -
    -
    -
    - - - - - - -
    NamePhone
    {{friendObj.name}}{{friendObj.phone}}
    -
    - - var expectFriendNames = function(expectedNames, key) { - element.all(by.repeater(key + ' in friends').column(key + '.name')).then(function(arr) { - arr.forEach(function(wd, i) { - expect(wd.getText()).toMatch(expectedNames[i]); - }); - }); - }; - - it('should search across all fields when filtering with a string', function() { - var searchText = element(by.model('searchText')); - searchText.clear(); - searchText.sendKeys('m'); - expectFriendNames(['Mary', 'Mike', 'Adam'], 'friend'); - - searchText.clear(); - searchText.sendKeys('76'); - expectFriendNames(['John', 'Julie'], 'friend'); - }); - - it('should search in specific fields when filtering with a predicate object', function() { - var searchAny = element(by.model('search.$')); - searchAny.clear(); - searchAny.sendKeys('i'); - expectFriendNames(['Mary', 'Mike', 'Julie', 'Juliette'], 'friendObj'); - }); - it('should use a equal comparison when comparator is true', function() { - var searchName = element(by.model('search.name')); - var strict = element(by.model('strict')); - searchName.clear(); - searchName.sendKeys('Julie'); - strict.click(); - expectFriendNames(['Julie'], 'friendObj'); - }); - -
    - */ -function filterFilter() { - return function(array, expression, comparator) { - if (!isArrayLike(array)) { - if (array == null) { - return array; - } else { - throw minErr('filter')('notarray', 'Expected array but received: {0}', array); - } - } - - var expressionType = getTypeForFilter(expression); - var predicateFn; - var matchAgainstAnyProp; - - switch (expressionType) { - case 'function': - predicateFn = expression; - break; - case 'boolean': - case 'null': - case 'number': - case 'string': - matchAgainstAnyProp = true; - //jshint -W086 - case 'object': - //jshint +W086 - predicateFn = createPredicateFn(expression, comparator, matchAgainstAnyProp); - break; - default: - return array; - } - - return Array.prototype.filter.call(array, predicateFn); - }; -} - -// Helper functions for `filterFilter` -function createPredicateFn(expression, comparator, matchAgainstAnyProp) { - var shouldMatchPrimitives = isObject(expression) && ('$' in expression); - var predicateFn; - - if (comparator === true) { - comparator = equals; - } else if (!isFunction(comparator)) { - comparator = function(actual, expected) { - if (isUndefined(actual)) { - // No substring matching against `undefined` - return false; - } - if ((actual === null) || (expected === null)) { - // No substring matching against `null`; only match against `null` - return actual === expected; - } - if (isObject(expected) || (isObject(actual) && !hasCustomToString(actual))) { - // Should not compare primitives against objects, unless they have custom `toString` method - return false; - } - - actual = lowercase('' + actual); - expected = lowercase('' + expected); - return actual.indexOf(expected) !== -1; - }; - } - - predicateFn = function(item) { - if (shouldMatchPrimitives && !isObject(item)) { - return deepCompare(item, expression.$, comparator, false); - } - return deepCompare(item, expression, comparator, matchAgainstAnyProp); - }; - - return predicateFn; -} - -function deepCompare(actual, expected, comparator, matchAgainstAnyProp, dontMatchWholeObject) { - var actualType = getTypeForFilter(actual); - var expectedType = getTypeForFilter(expected); - - if ((expectedType === 'string') && (expected.charAt(0) === '!')) { - return !deepCompare(actual, expected.substring(1), comparator, matchAgainstAnyProp); - } else if (isArray(actual)) { - // In case `actual` is an array, consider it a match - // if ANY of it's items matches `expected` - return actual.some(function(item) { - return deepCompare(item, expected, comparator, matchAgainstAnyProp); - }); - } - - switch (actualType) { - case 'object': - var key; - if (matchAgainstAnyProp) { - for (key in actual) { - if ((key.charAt(0) !== '$') && deepCompare(actual[key], expected, comparator, true)) { - return true; - } - } - return dontMatchWholeObject ? false : deepCompare(actual, expected, comparator, false); - } else if (expectedType === 'object') { - for (key in expected) { - var expectedVal = expected[key]; - if (isFunction(expectedVal) || isUndefined(expectedVal)) { - continue; - } - - var matchAnyProperty = key === '$'; - var actualVal = matchAnyProperty ? actual : actual[key]; - if (!deepCompare(actualVal, expectedVal, comparator, matchAnyProperty, matchAnyProperty)) { - return false; - } - } - return true; - } else { - return comparator(actual, expected); - } - break; - case 'function': - return false; - default: - return comparator(actual, expected); - } -} - -// Used for easily differentiating between `null` and actual `object` -function getTypeForFilter(val) { - return (val === null) ? 'null' : typeof val; -} - -/** - * @ngdoc filter - * @name currency - * @kind function - * - * @description - * Formats a number as a currency (ie $1,234.56). When no currency symbol is provided, default - * symbol for current locale is used. - * - * @param {number} amount Input to filter. - * @param {string=} symbol Currency symbol or identifier to be displayed. - * @param {number=} fractionSize Number of decimal places to round the amount to, defaults to default max fraction size for current locale - * @returns {string} Formatted number. - * - * - * @example - - - -
    -
    - default currency symbol ($): {{amount | currency}}
    - custom currency identifier (USD$): {{amount | currency:"USD$"}} - no fractions (0): {{amount | currency:"USD$":0}} -
    -
    - - it('should init with 1234.56', function() { - expect(element(by.id('currency-default')).getText()).toBe('$1,234.56'); - expect(element(by.id('currency-custom')).getText()).toBe('USD$1,234.56'); - expect(element(by.id('currency-no-fractions')).getText()).toBe('USD$1,235'); - }); - it('should update', function() { - if (browser.params.browser == 'safari') { - // Safari does not understand the minus key. See - // https://github.com/angular/protractor/issues/481 - return; - } - element(by.model('amount')).clear(); - element(by.model('amount')).sendKeys('-1234'); - expect(element(by.id('currency-default')).getText()).toBe('-$1,234.00'); - expect(element(by.id('currency-custom')).getText()).toBe('-USD$1,234.00'); - expect(element(by.id('currency-no-fractions')).getText()).toBe('-USD$1,234'); - }); - -
    - */ -currencyFilter.$inject = ['$locale']; -function currencyFilter($locale) { - var formats = $locale.NUMBER_FORMATS; - return function(amount, currencySymbol, fractionSize) { - if (isUndefined(currencySymbol)) { - currencySymbol = formats.CURRENCY_SYM; - } - - if (isUndefined(fractionSize)) { - fractionSize = formats.PATTERNS[1].maxFrac; - } - - // if null or undefined pass it through - return (amount == null) - ? amount - : formatNumber(amount, formats.PATTERNS[1], formats.GROUP_SEP, formats.DECIMAL_SEP, fractionSize). - replace(/\u00A4/g, currencySymbol); - }; -} - -/** - * @ngdoc filter - * @name number - * @kind function - * - * @description - * Formats a number as text. - * - * If the input is null or undefined, it will just be returned. - * If the input is infinite (Infinity/-Infinity) the Infinity symbol '∞' is returned. - * If the input is not a number an empty string is returned. - * - * - * @param {number|string} number Number to format. - * @param {(number|string)=} fractionSize Number of decimal places to round the number to. - * If this is not provided then the fraction size is computed from the current locale's number - * formatting pattern. In the case of the default locale, it will be 3. - * @returns {string} Number rounded to decimalPlaces and places a “,” after each third digit. - * - * @example - - - -
    -
    - Default formatting: {{val | number}}
    - No fractions: {{val | number:0}}
    - Negative number: {{-val | number:4}} -
    -
    - - it('should format numbers', function() { - expect(element(by.id('number-default')).getText()).toBe('1,234.568'); - expect(element(by.binding('val | number:0')).getText()).toBe('1,235'); - expect(element(by.binding('-val | number:4')).getText()).toBe('-1,234.5679'); - }); - - it('should update', function() { - element(by.model('val')).clear(); - element(by.model('val')).sendKeys('3374.333'); - expect(element(by.id('number-default')).getText()).toBe('3,374.333'); - expect(element(by.binding('val | number:0')).getText()).toBe('3,374'); - expect(element(by.binding('-val | number:4')).getText()).toBe('-3,374.3330'); - }); - -
    - */ - - -numberFilter.$inject = ['$locale']; -function numberFilter($locale) { - var formats = $locale.NUMBER_FORMATS; - return function(number, fractionSize) { - - // if null or undefined pass it through - return (number == null) - ? number - : formatNumber(number, formats.PATTERNS[0], formats.GROUP_SEP, formats.DECIMAL_SEP, - fractionSize); - }; -} - -var DECIMAL_SEP = '.'; -function formatNumber(number, pattern, groupSep, decimalSep, fractionSize) { - if (isObject(number)) return ''; - - var isNegative = number < 0; - number = Math.abs(number); - - var isInfinity = number === Infinity; - if (!isInfinity && !isFinite(number)) return ''; - - var numStr = number + '', - formatedText = '', - hasExponent = false, - parts = []; - - if (isInfinity) formatedText = '\u221e'; - - if (!isInfinity && numStr.indexOf('e') !== -1) { - var match = numStr.match(/([\d\.]+)e(-?)(\d+)/); - if (match && match[2] == '-' && match[3] > fractionSize + 1) { - number = 0; - } else { - formatedText = numStr; - hasExponent = true; - } - } - - if (!isInfinity && !hasExponent) { - var fractionLen = (numStr.split(DECIMAL_SEP)[1] || '').length; - - // determine fractionSize if it is not specified - if (isUndefined(fractionSize)) { - fractionSize = Math.min(Math.max(pattern.minFrac, fractionLen), pattern.maxFrac); - } - - // safely round numbers in JS without hitting imprecisions of floating-point arithmetics - // inspired by: - // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/round - number = +(Math.round(+(number.toString() + 'e' + fractionSize)).toString() + 'e' + -fractionSize); - - var fraction = ('' + number).split(DECIMAL_SEP); - var whole = fraction[0]; - fraction = fraction[1] || ''; - - var i, pos = 0, - lgroup = pattern.lgSize, - group = pattern.gSize; - - if (whole.length >= (lgroup + group)) { - pos = whole.length - lgroup; - for (i = 0; i < pos; i++) { - if ((pos - i) % group === 0 && i !== 0) { - formatedText += groupSep; - } - formatedText += whole.charAt(i); - } - } - - for (i = pos; i < whole.length; i++) { - if ((whole.length - i) % lgroup === 0 && i !== 0) { - formatedText += groupSep; - } - formatedText += whole.charAt(i); - } - - // format fraction part. - while (fraction.length < fractionSize) { - fraction += '0'; - } - - if (fractionSize && fractionSize !== "0") formatedText += decimalSep + fraction.substr(0, fractionSize); - } else { - if (fractionSize > 0 && number < 1) { - formatedText = number.toFixed(fractionSize); - number = parseFloat(formatedText); - } - } - - if (number === 0) { - isNegative = false; - } - - parts.push(isNegative ? pattern.negPre : pattern.posPre, - formatedText, - isNegative ? pattern.negSuf : pattern.posSuf); - return parts.join(''); -} - -function padNumber(num, digits, trim) { - var neg = ''; - if (num < 0) { - neg = '-'; - num = -num; - } - num = '' + num; - while (num.length < digits) num = '0' + num; - if (trim) { - num = num.substr(num.length - digits); - } - return neg + num; -} - - -function dateGetter(name, size, offset, trim) { - offset = offset || 0; - return function(date) { - var value = date['get' + name](); - if (offset > 0 || value > -offset) { - value += offset; - } - if (value === 0 && offset == -12) value = 12; - return padNumber(value, size, trim); - }; -} - -function dateStrGetter(name, shortForm) { - return function(date, formats) { - var value = date['get' + name](); - var get = uppercase(shortForm ? ('SHORT' + name) : name); - - return formats[get][value]; - }; -} - -function timeZoneGetter(date, formats, offset) { - var zone = -1 * offset; - var paddedZone = (zone >= 0) ? "+" : ""; - - paddedZone += padNumber(Math[zone > 0 ? 'floor' : 'ceil'](zone / 60), 2) + - padNumber(Math.abs(zone % 60), 2); - - return paddedZone; -} - -function getFirstThursdayOfYear(year) { - // 0 = index of January - var dayOfWeekOnFirst = (new Date(year, 0, 1)).getDay(); - // 4 = index of Thursday (+1 to account for 1st = 5) - // 11 = index of *next* Thursday (+1 account for 1st = 12) - return new Date(year, 0, ((dayOfWeekOnFirst <= 4) ? 5 : 12) - dayOfWeekOnFirst); -} - -function getThursdayThisWeek(datetime) { - return new Date(datetime.getFullYear(), datetime.getMonth(), - // 4 = index of Thursday - datetime.getDate() + (4 - datetime.getDay())); -} - -function weekGetter(size) { - return function(date) { - var firstThurs = getFirstThursdayOfYear(date.getFullYear()), - thisThurs = getThursdayThisWeek(date); - - var diff = +thisThurs - +firstThurs, - result = 1 + Math.round(diff / 6.048e8); // 6.048e8 ms per week - - return padNumber(result, size); - }; -} - -function ampmGetter(date, formats) { - return date.getHours() < 12 ? formats.AMPMS[0] : formats.AMPMS[1]; -} - -function eraGetter(date, formats) { - return date.getFullYear() <= 0 ? formats.ERAS[0] : formats.ERAS[1]; -} - -function longEraGetter(date, formats) { - return date.getFullYear() <= 0 ? formats.ERANAMES[0] : formats.ERANAMES[1]; -} - -var DATE_FORMATS = { - yyyy: dateGetter('FullYear', 4), - yy: dateGetter('FullYear', 2, 0, true), - y: dateGetter('FullYear', 1), - MMMM: dateStrGetter('Month'), - MMM: dateStrGetter('Month', true), - MM: dateGetter('Month', 2, 1), - M: dateGetter('Month', 1, 1), - dd: dateGetter('Date', 2), - d: dateGetter('Date', 1), - HH: dateGetter('Hours', 2), - H: dateGetter('Hours', 1), - hh: dateGetter('Hours', 2, -12), - h: dateGetter('Hours', 1, -12), - mm: dateGetter('Minutes', 2), - m: dateGetter('Minutes', 1), - ss: dateGetter('Seconds', 2), - s: dateGetter('Seconds', 1), - // while ISO 8601 requires fractions to be prefixed with `.` or `,` - // we can be just safely rely on using `sss` since we currently don't support single or two digit fractions - sss: dateGetter('Milliseconds', 3), - EEEE: dateStrGetter('Day'), - EEE: dateStrGetter('Day', true), - a: ampmGetter, - Z: timeZoneGetter, - ww: weekGetter(2), - w: weekGetter(1), - G: eraGetter, - GG: eraGetter, - GGG: eraGetter, - GGGG: longEraGetter -}; - -var DATE_FORMATS_SPLIT = /((?:[^yMdHhmsaZEwG']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|d+|H+|h+|m+|s+|a|Z|G+|w+))(.*)/, - NUMBER_STRING = /^\-?\d+$/; - -/** - * @ngdoc filter - * @name date - * @kind function - * - * @description - * Formats `date` to a string based on the requested `format`. - * - * `format` string can be composed of the following elements: - * - * * `'yyyy'`: 4 digit representation of year (e.g. AD 1 => 0001, AD 2010 => 2010) - * * `'yy'`: 2 digit representation of year, padded (00-99). (e.g. AD 2001 => 01, AD 2010 => 10) - * * `'y'`: 1 digit representation of year, e.g. (AD 1 => 1, AD 199 => 199) - * * `'MMMM'`: Month in year (January-December) - * * `'MMM'`: Month in year (Jan-Dec) - * * `'MM'`: Month in year, padded (01-12) - * * `'M'`: Month in year (1-12) - * * `'dd'`: Day in month, padded (01-31) - * * `'d'`: Day in month (1-31) - * * `'EEEE'`: Day in Week,(Sunday-Saturday) - * * `'EEE'`: Day in Week, (Sun-Sat) - * * `'HH'`: Hour in day, padded (00-23) - * * `'H'`: Hour in day (0-23) - * * `'hh'`: Hour in AM/PM, padded (01-12) - * * `'h'`: Hour in AM/PM, (1-12) - * * `'mm'`: Minute in hour, padded (00-59) - * * `'m'`: Minute in hour (0-59) - * * `'ss'`: Second in minute, padded (00-59) - * * `'s'`: Second in minute (0-59) - * * `'sss'`: Millisecond in second, padded (000-999) - * * `'a'`: AM/PM marker - * * `'Z'`: 4 digit (+sign) representation of the timezone offset (-1200-+1200) - * * `'ww'`: Week of year, padded (00-53). Week 01 is the week with the first Thursday of the year - * * `'w'`: Week of year (0-53). Week 1 is the week with the first Thursday of the year - * * `'G'`, `'GG'`, `'GGG'`: The abbreviated form of the era string (e.g. 'AD') - * * `'GGGG'`: The long form of the era string (e.g. 'Anno Domini') - * - * `format` string can also be one of the following predefined - * {@link guide/i18n localizable formats}: - * - * * `'medium'`: equivalent to `'MMM d, y h:mm:ss a'` for en_US locale - * (e.g. Sep 3, 2010 12:05:08 PM) - * * `'short'`: equivalent to `'M/d/yy h:mm a'` for en_US locale (e.g. 9/3/10 12:05 PM) - * * `'fullDate'`: equivalent to `'EEEE, MMMM d, y'` for en_US locale - * (e.g. Friday, September 3, 2010) - * * `'longDate'`: equivalent to `'MMMM d, y'` for en_US locale (e.g. September 3, 2010) - * * `'mediumDate'`: equivalent to `'MMM d, y'` for en_US locale (e.g. Sep 3, 2010) - * * `'shortDate'`: equivalent to `'M/d/yy'` for en_US locale (e.g. 9/3/10) - * * `'mediumTime'`: equivalent to `'h:mm:ss a'` for en_US locale (e.g. 12:05:08 PM) - * * `'shortTime'`: equivalent to `'h:mm a'` for en_US locale (e.g. 12:05 PM) - * - * `format` string can contain literal values. These need to be escaped by surrounding with single quotes (e.g. - * `"h 'in the morning'"`). In order to output a single quote, escape it - i.e., two single quotes in a sequence - * (e.g. `"h 'o''clock'"`). - * - * @param {(Date|number|string)} date Date to format either as Date object, milliseconds (string or - * number) or various ISO 8601 datetime string formats (e.g. yyyy-MM-ddTHH:mm:ss.sssZ and its - * shorter versions like yyyy-MM-ddTHH:mmZ, yyyy-MM-dd or yyyyMMddTHHmmssZ). If no timezone is - * specified in the string input, the time is considered to be in the local timezone. - * @param {string=} format Formatting rules (see Description). If not specified, - * `mediumDate` is used. - * @param {string=} timezone Timezone to be used for formatting. It understands UTC/GMT and the - * continental US time zone abbreviations, but for general use, use a time zone offset, for - * example, `'+0430'` (4 hours, 30 minutes east of the Greenwich meridian) - * If not specified, the timezone of the browser will be used. - * @returns {string} Formatted string or the input if input is not recognized as date/millis. - * - * @example - - - {{1288323623006 | date:'medium'}}: - {{1288323623006 | date:'medium'}}
    - {{1288323623006 | date:'yyyy-MM-dd HH:mm:ss Z'}}: - {{1288323623006 | date:'yyyy-MM-dd HH:mm:ss Z'}}
    - {{1288323623006 | date:'MM/dd/yyyy @ h:mma'}}: - {{'1288323623006' | date:'MM/dd/yyyy @ h:mma'}}
    - {{1288323623006 | date:"MM/dd/yyyy 'at' h:mma"}}: - {{'1288323623006' | date:"MM/dd/yyyy 'at' h:mma"}}
    -
    - - it('should format date', function() { - expect(element(by.binding("1288323623006 | date:'medium'")).getText()). - toMatch(/Oct 2\d, 2010 \d{1,2}:\d{2}:\d{2} (AM|PM)/); - expect(element(by.binding("1288323623006 | date:'yyyy-MM-dd HH:mm:ss Z'")).getText()). - toMatch(/2010\-10\-2\d \d{2}:\d{2}:\d{2} (\-|\+)?\d{4}/); - expect(element(by.binding("'1288323623006' | date:'MM/dd/yyyy @ h:mma'")).getText()). - toMatch(/10\/2\d\/2010 @ \d{1,2}:\d{2}(AM|PM)/); - expect(element(by.binding("'1288323623006' | date:\"MM/dd/yyyy 'at' h:mma\"")).getText()). - toMatch(/10\/2\d\/2010 at \d{1,2}:\d{2}(AM|PM)/); - }); - -
    - */ -dateFilter.$inject = ['$locale']; -function dateFilter($locale) { - - - var R_ISO8601_STR = /^(\d{4})-?(\d\d)-?(\d\d)(?:T(\d\d)(?::?(\d\d)(?::?(\d\d)(?:\.(\d+))?)?)?(Z|([+-])(\d\d):?(\d\d))?)?$/; - // 1 2 3 4 5 6 7 8 9 10 11 - function jsonStringToDate(string) { - var match; - if (match = string.match(R_ISO8601_STR)) { - var date = new Date(0), - tzHour = 0, - tzMin = 0, - dateSetter = match[8] ? date.setUTCFullYear : date.setFullYear, - timeSetter = match[8] ? date.setUTCHours : date.setHours; - - if (match[9]) { - tzHour = toInt(match[9] + match[10]); - tzMin = toInt(match[9] + match[11]); - } - dateSetter.call(date, toInt(match[1]), toInt(match[2]) - 1, toInt(match[3])); - var h = toInt(match[4] || 0) - tzHour; - var m = toInt(match[5] || 0) - tzMin; - var s = toInt(match[6] || 0); - var ms = Math.round(parseFloat('0.' + (match[7] || 0)) * 1000); - timeSetter.call(date, h, m, s, ms); - return date; - } - return string; - } - - - return function(date, format, timezone) { - var text = '', - parts = [], - fn, match; - - format = format || 'mediumDate'; - format = $locale.DATETIME_FORMATS[format] || format; - if (isString(date)) { - date = NUMBER_STRING.test(date) ? toInt(date) : jsonStringToDate(date); - } - - if (isNumber(date)) { - date = new Date(date); - } - - if (!isDate(date) || !isFinite(date.getTime())) { - return date; - } - - while (format) { - match = DATE_FORMATS_SPLIT.exec(format); - if (match) { - parts = concat(parts, match, 1); - format = parts.pop(); - } else { - parts.push(format); - format = null; - } - } - - var dateTimezoneOffset = date.getTimezoneOffset(); - if (timezone) { - dateTimezoneOffset = timezoneToOffset(timezone, date.getTimezoneOffset()); - date = convertTimezoneToLocal(date, timezone, true); - } - forEach(parts, function(value) { - fn = DATE_FORMATS[value]; - text += fn ? fn(date, $locale.DATETIME_FORMATS, dateTimezoneOffset) - : value.replace(/(^'|'$)/g, '').replace(/''/g, "'"); - }); - - return text; - }; -} - - -/** - * @ngdoc filter - * @name json - * @kind function - * - * @description - * Allows you to convert a JavaScript object into JSON string. - * - * This filter is mostly useful for debugging. When using the double curly {{value}} notation - * the binding is automatically converted to JSON. - * - * @param {*} object Any JavaScript object (including arrays and primitive types) to filter. - * @param {number=} spacing The number of spaces to use per indentation, defaults to 2. - * @returns {string} JSON string. - * - * - * @example - - -
    {{ {'name':'value'} | json }}
    -
    {{ {'name':'value'} | json:4 }}
    -
    - - it('should jsonify filtered objects', function() { - expect(element(by.id('default-spacing')).getText()).toMatch(/\{\n "name": ?"value"\n}/); - expect(element(by.id('custom-spacing')).getText()).toMatch(/\{\n "name": ?"value"\n}/); - }); - -
    - * - */ -function jsonFilter() { - return function(object, spacing) { - if (isUndefined(spacing)) { - spacing = 2; - } - return toJson(object, spacing); - }; -} - - -/** - * @ngdoc filter - * @name lowercase - * @kind function - * @description - * Converts string to lowercase. - * @see angular.lowercase - */ -var lowercaseFilter = valueFn(lowercase); - - -/** - * @ngdoc filter - * @name uppercase - * @kind function - * @description - * Converts string to uppercase. - * @see angular.uppercase - */ -var uppercaseFilter = valueFn(uppercase); - -/** - * @ngdoc filter - * @name limitTo - * @kind function - * - * @description - * Creates a new array or string containing only a specified number of elements. The elements - * are taken from either the beginning or the end of the source array, string or number, as specified by - * the value and sign (positive or negative) of `limit`. If a number is used as input, it is - * converted to a string. - * - * @param {Array|string|number} input Source array, string or number to be limited. - * @param {string|number} limit The length of the returned array or string. If the `limit` number - * is positive, `limit` number of items from the beginning of the source array/string are copied. - * If the number is negative, `limit` number of items from the end of the source array/string - * are copied. The `limit` will be trimmed if it exceeds `array.length`. If `limit` is undefined, - * the input will be returned unchanged. - * @param {(string|number)=} begin Index at which to begin limitation. As a negative index, `begin` - * indicates an offset from the end of `input`. Defaults to `0`. - * @returns {Array|string} A new sub-array or substring of length `limit` or less if input array - * had less than `limit` elements. - * - * @example - - - -
    - -

    Output numbers: {{ numbers | limitTo:numLimit }}

    - -

    Output letters: {{ letters | limitTo:letterLimit }}

    - -

    Output long number: {{ longNumber | limitTo:longNumberLimit }}

    -
    -
    - - var numLimitInput = element(by.model('numLimit')); - var letterLimitInput = element(by.model('letterLimit')); - var longNumberLimitInput = element(by.model('longNumberLimit')); - var limitedNumbers = element(by.binding('numbers | limitTo:numLimit')); - var limitedLetters = element(by.binding('letters | limitTo:letterLimit')); - var limitedLongNumber = element(by.binding('longNumber | limitTo:longNumberLimit')); - - it('should limit the number array to first three items', function() { - expect(numLimitInput.getAttribute('value')).toBe('3'); - expect(letterLimitInput.getAttribute('value')).toBe('3'); - expect(longNumberLimitInput.getAttribute('value')).toBe('3'); - expect(limitedNumbers.getText()).toEqual('Output numbers: [1,2,3]'); - expect(limitedLetters.getText()).toEqual('Output letters: abc'); - expect(limitedLongNumber.getText()).toEqual('Output long number: 234'); - }); - - // There is a bug in safari and protractor that doesn't like the minus key - // it('should update the output when -3 is entered', function() { - // numLimitInput.clear(); - // numLimitInput.sendKeys('-3'); - // letterLimitInput.clear(); - // letterLimitInput.sendKeys('-3'); - // longNumberLimitInput.clear(); - // longNumberLimitInput.sendKeys('-3'); - // expect(limitedNumbers.getText()).toEqual('Output numbers: [7,8,9]'); - // expect(limitedLetters.getText()).toEqual('Output letters: ghi'); - // expect(limitedLongNumber.getText()).toEqual('Output long number: 342'); - // }); - - it('should not exceed the maximum size of input array', function() { - numLimitInput.clear(); - numLimitInput.sendKeys('100'); - letterLimitInput.clear(); - letterLimitInput.sendKeys('100'); - longNumberLimitInput.clear(); - longNumberLimitInput.sendKeys('100'); - expect(limitedNumbers.getText()).toEqual('Output numbers: [1,2,3,4,5,6,7,8,9]'); - expect(limitedLetters.getText()).toEqual('Output letters: abcdefghi'); - expect(limitedLongNumber.getText()).toEqual('Output long number: 2345432342'); - }); - -
    -*/ -function limitToFilter() { - return function(input, limit, begin) { - if (Math.abs(Number(limit)) === Infinity) { - limit = Number(limit); - } else { - limit = toInt(limit); - } - if (isNaN(limit)) return input; - - if (isNumber(input)) input = input.toString(); - if (!isArray(input) && !isString(input)) return input; - - begin = (!begin || isNaN(begin)) ? 0 : toInt(begin); - begin = (begin < 0 && begin >= -input.length) ? input.length + begin : begin; - - if (limit >= 0) { - return input.slice(begin, begin + limit); - } else { - if (begin === 0) { - return input.slice(limit, input.length); - } else { - return input.slice(Math.max(0, begin + limit), begin); - } - } - }; -} - -/** - * @ngdoc filter - * @name orderBy - * @kind function - * - * @description - * Orders a specified `array` by the `expression` predicate. It is ordered alphabetically - * for strings and numerically for numbers. Note: if you notice numbers are not being sorted - * as expected, make sure they are actually being saved as numbers and not strings. - * - * @param {Array} array The array to sort. - * @param {function(*)|string|Array.<(function(*)|string)>=} expression A predicate to be - * used by the comparator to determine the order of elements. - * - * Can be one of: - * - * - `function`: Getter function. The result of this function will be sorted using the - * `<`, `===`, `>` operator. - * - `string`: An Angular expression. The result of this expression is used to compare elements - * (for example `name` to sort by a property called `name` or `name.substr(0, 3)` to sort by - * 3 first characters of a property called `name`). The result of a constant expression - * is interpreted as a property name to be used in comparisons (for example `"special name"` - * to sort object by the value of their `special name` property). An expression can be - * optionally prefixed with `+` or `-` to control ascending or descending sort order - * (for example, `+name` or `-name`). If no property is provided, (e.g. `'+'`) then the array - * element itself is used to compare where sorting. - * - `Array`: An array of function or string predicates. The first predicate in the array - * is used for sorting, but when two items are equivalent, the next predicate is used. - * - * If the predicate is missing or empty then it defaults to `'+'`. - * - * @param {boolean=} reverse Reverse the order of the array. - * @returns {Array} Sorted copy of the source array. - * - * - * @example - * The example below demonstrates a simple ngRepeat, where the data is sorted - * by age in descending order (predicate is set to `'-age'`). - * `reverse` is not set, which means it defaults to `false`. - - - -
    - - - - - - - - - - - -
    NamePhone NumberAge
    {{friend.name}}{{friend.phone}}{{friend.age}}
    -
    -
    -
    - * - * The predicate and reverse parameters can be controlled dynamically through scope properties, - * as shown in the next example. - * @example - - - - -
    -
    Sorting predicate = {{predicate}}; reverse = {{reverse}}
    -
    - [ unsorted ] - - - - - - - - - - - -
    - Name - - - Phone Number - - - Age - -
    {{friend.name}}{{friend.phone}}{{friend.age}}
    -
    -
    -
    - * - * It's also possible to call the orderBy filter manually, by injecting `$filter`, retrieving the - * filter routine with `$filter('orderBy')`, and calling the returned filter routine with the - * desired parameters. - * - * Example: - * - * @example - - -
    - - - - - - - - - - - -
    Name - (^)Phone NumberAge
    {{friend.name}}{{friend.phone}}{{friend.age}}
    -
    -
    - - - angular.module('orderByExample', []) - .controller('ExampleController', ['$scope', '$filter', function($scope, $filter) { - var orderBy = $filter('orderBy'); - $scope.friends = [ - { name: 'John', phone: '555-1212', age: 10 }, - { name: 'Mary', phone: '555-9876', age: 19 }, - { name: 'Mike', phone: '555-4321', age: 21 }, - { name: 'Adam', phone: '555-5678', age: 35 }, - { name: 'Julie', phone: '555-8765', age: 29 } - ]; - $scope.order = function(predicate, reverse) { - $scope.friends = orderBy($scope.friends, predicate, reverse); - }; - $scope.order('-age',false); - }]); - -
    - */ -orderByFilter.$inject = ['$parse']; -function orderByFilter($parse) { - return function(array, sortPredicate, reverseOrder) { - - if (!(isArrayLike(array))) return array; - - if (!isArray(sortPredicate)) { sortPredicate = [sortPredicate]; } - if (sortPredicate.length === 0) { sortPredicate = ['+']; } - - var predicates = processPredicates(sortPredicate, reverseOrder); - // Add a predicate at the end that evaluates to the element index. This makes the - // sort stable as it works as a tie-breaker when all the input predicates cannot - // distinguish between two elements. - predicates.push({ get: function() { return {}; }, descending: reverseOrder ? -1 : 1}); - - // The next three lines are a version of a Swartzian Transform idiom from Perl - // (sometimes called the Decorate-Sort-Undecorate idiom) - // See https://en.wikipedia.org/wiki/Schwartzian_transform - var compareValues = Array.prototype.map.call(array, getComparisonObject); - compareValues.sort(doComparison); - array = compareValues.map(function(item) { return item.value; }); - - return array; - - function getComparisonObject(value, index) { - return { - value: value, - predicateValues: predicates.map(function(predicate) { - return getPredicateValue(predicate.get(value), index); - }) - }; - } - - function doComparison(v1, v2) { - var result = 0; - for (var index=0, length = predicates.length; index < length; ++index) { - result = compare(v1.predicateValues[index], v2.predicateValues[index]) * predicates[index].descending; - if (result) break; - } - return result; - } - }; - - function processPredicates(sortPredicate, reverseOrder) { - reverseOrder = reverseOrder ? -1 : 1; - return sortPredicate.map(function(predicate) { - var descending = 1, get = identity; - - if (isFunction(predicate)) { - get = predicate; - } else if (isString(predicate)) { - if ((predicate.charAt(0) == '+' || predicate.charAt(0) == '-')) { - descending = predicate.charAt(0) == '-' ? -1 : 1; - predicate = predicate.substring(1); - } - if (predicate !== '') { - get = $parse(predicate); - if (get.constant) { - var key = get(); - get = function(value) { return value[key]; }; - } - } - } - return { get: get, descending: descending * reverseOrder }; - }); - } - - function isPrimitive(value) { - switch (typeof value) { - case 'number': /* falls through */ - case 'boolean': /* falls through */ - case 'string': - return true; - default: - return false; - } - } - - function objectValue(value, index) { - // If `valueOf` is a valid function use that - if (typeof value.valueOf === 'function') { - value = value.valueOf(); - if (isPrimitive(value)) return value; - } - // If `toString` is a valid function and not the one from `Object.prototype` use that - if (hasCustomToString(value)) { - value = value.toString(); - if (isPrimitive(value)) return value; - } - // We have a basic object so we use the position of the object in the collection - return index; - } - - function getPredicateValue(value, index) { - var type = typeof value; - if (value === null) { - type = 'string'; - value = 'null'; - } else if (type === 'string') { - value = value.toLowerCase(); - } else if (type === 'object') { - value = objectValue(value, index); - } - return { value: value, type: type }; - } - - function compare(v1, v2) { - var result = 0; - if (v1.type === v2.type) { - if (v1.value !== v2.value) { - result = v1.value < v2.value ? -1 : 1; - } - } else { - result = v1.type < v2.type ? -1 : 1; - } - return result; - } -} - -function ngDirective(directive) { - if (isFunction(directive)) { - directive = { - link: directive - }; - } - directive.restrict = directive.restrict || 'AC'; - return valueFn(directive); -} - -/** - * @ngdoc directive - * @name a - * @restrict E - * - * @description - * Modifies the default behavior of the html A tag so that the default action is prevented when - * the href attribute is empty. - * - * This change permits the easy creation of action links with the `ngClick` directive - * without changing the location or causing page reloads, e.g.: - * `Add Item` - */ -var htmlAnchorDirective = valueFn({ - restrict: 'E', - compile: function(element, attr) { - if (!attr.href && !attr.xlinkHref) { - return function(scope, element) { - // If the linked element is not an anchor tag anymore, do nothing - if (element[0].nodeName.toLowerCase() !== 'a') return; - - // SVGAElement does not use the href attribute, but rather the 'xlinkHref' attribute. - var href = toString.call(element.prop('href')) === '[object SVGAnimatedString]' ? - 'xlink:href' : 'href'; - element.on('click', function(event) { - // if we have no href url, then don't navigate anywhere. - if (!element.attr(href)) { - event.preventDefault(); - } - }); - }; - } - } -}); - -/** - * @ngdoc directive - * @name ngHref - * @restrict A - * @priority 99 - * - * @description - * Using Angular markup like `{{hash}}` in an href attribute will - * make the link go to the wrong URL if the user clicks it before - * Angular has a chance to replace the `{{hash}}` markup with its - * value. Until Angular replaces the markup the link will be broken - * and will most likely return a 404 error. The `ngHref` directive - * solves this problem. - * - * The wrong way to write it: - * ```html - * link1 - * ``` - * - * The correct way to write it: - * ```html - * link1 - * ``` - * - * @element A - * @param {template} ngHref any string which can contain `{{}}` markup. - * - * @example - * This example shows various combinations of `href`, `ng-href` and `ng-click` attributes - * in links and their different behaviors: - - -
    - link 1 (link, don't reload)
    - link 2 (link, don't reload)
    - link 3 (link, reload!)
    - anchor (link, don't reload)
    - anchor (no link)
    - link (link, change location) -
    - - it('should execute ng-click but not reload when href without value', function() { - element(by.id('link-1')).click(); - expect(element(by.model('value')).getAttribute('value')).toEqual('1'); - expect(element(by.id('link-1')).getAttribute('href')).toBe(''); - }); - - it('should execute ng-click but not reload when href empty string', function() { - element(by.id('link-2')).click(); - expect(element(by.model('value')).getAttribute('value')).toEqual('2'); - expect(element(by.id('link-2')).getAttribute('href')).toBe(''); - }); - - it('should execute ng-click and change url when ng-href specified', function() { - expect(element(by.id('link-3')).getAttribute('href')).toMatch(/\/123$/); - - element(by.id('link-3')).click(); - - // At this point, we navigate away from an Angular page, so we need - // to use browser.driver to get the base webdriver. - - browser.wait(function() { - return browser.driver.getCurrentUrl().then(function(url) { - return url.match(/\/123$/); - }); - }, 5000, 'page should navigate to /123'); - }); - - it('should execute ng-click but not reload when href empty string and name specified', function() { - element(by.id('link-4')).click(); - expect(element(by.model('value')).getAttribute('value')).toEqual('4'); - expect(element(by.id('link-4')).getAttribute('href')).toBe(''); - }); - - it('should execute ng-click but not reload when no href but name specified', function() { - element(by.id('link-5')).click(); - expect(element(by.model('value')).getAttribute('value')).toEqual('5'); - expect(element(by.id('link-5')).getAttribute('href')).toBe(null); - }); - - it('should only change url when only ng-href', function() { - element(by.model('value')).clear(); - element(by.model('value')).sendKeys('6'); - expect(element(by.id('link-6')).getAttribute('href')).toMatch(/\/6$/); - - element(by.id('link-6')).click(); - - // At this point, we navigate away from an Angular page, so we need - // to use browser.driver to get the base webdriver. - browser.wait(function() { - return browser.driver.getCurrentUrl().then(function(url) { - return url.match(/\/6$/); - }); - }, 5000, 'page should navigate to /6'); - }); - -
    - */ - -/** - * @ngdoc directive - * @name ngSrc - * @restrict A - * @priority 99 - * - * @description - * Using Angular markup like `{{hash}}` in a `src` attribute doesn't - * work right: The browser will fetch from the URL with the literal - * text `{{hash}}` until Angular replaces the expression inside - * `{{hash}}`. The `ngSrc` directive solves this problem. - * - * The buggy way to write it: - * ```html - * Description - * ``` - * - * The correct way to write it: - * ```html - * Description - * ``` - * - * @element IMG - * @param {template} ngSrc any string which can contain `{{}}` markup. - */ - -/** - * @ngdoc directive - * @name ngSrcset - * @restrict A - * @priority 99 - * - * @description - * Using Angular markup like `{{hash}}` in a `srcset` attribute doesn't - * work right: The browser will fetch from the URL with the literal - * text `{{hash}}` until Angular replaces the expression inside - * `{{hash}}`. The `ngSrcset` directive solves this problem. - * - * The buggy way to write it: - * ```html - * Description - * ``` - * - * The correct way to write it: - * ```html - * Description - * ``` - * - * @element IMG - * @param {template} ngSrcset any string which can contain `{{}}` markup. - */ - -/** - * @ngdoc directive - * @name ngDisabled - * @restrict A - * @priority 100 - * - * @description - * - * This directive sets the `disabled` attribute on the element if the - * {@link guide/expression expression} inside `ngDisabled` evaluates to truthy. - * - * A special directive is necessary because we cannot use interpolation inside the `disabled` - * attribute. The following example would make the button enabled on Chrome/Firefox - * but not on older IEs: - * - * ```html - * - *
    - * - *
    - * ``` - * - * This is because the HTML specification does not require browsers to preserve the values of - * boolean attributes such as `disabled` (Their presence means true and their absence means false.) - * If we put an Angular interpolation expression into such an attribute then the - * binding information would be lost when the browser removes the attribute. - * - * @example - - -
    - -
    - - it('should toggle button', function() { - expect(element(by.css('button')).getAttribute('disabled')).toBeFalsy(); - element(by.model('checked')).click(); - expect(element(by.css('button')).getAttribute('disabled')).toBeTruthy(); - }); - -
    - * - * @element INPUT - * @param {expression} ngDisabled If the {@link guide/expression expression} is truthy, - * then the `disabled` attribute will be set on the element - */ - - -/** - * @ngdoc directive - * @name ngChecked - * @restrict A - * @priority 100 - * - * @description - * Sets the `checked` attribute on the element, if the expression inside `ngChecked` is truthy. - * - * Note that this directive should not be used together with {@link ngModel `ngModel`}, - * as this can lead to unexpected behavior. - * - * ### Why do we need `ngChecked`? - * - * The HTML specification does not require browsers to preserve the values of boolean attributes - * such as checked. (Their presence means true and their absence means false.) - * If we put an Angular interpolation expression into such an attribute then the - * binding information would be lost when the browser removes the attribute. - * The `ngChecked` directive solves this problem for the `checked` attribute. - * This complementary directive is not removed by the browser and so provides - * a permanent reliable place to store the binding information. - * @example - - -
    - -
    - - it('should check both checkBoxes', function() { - expect(element(by.id('checkSlave')).getAttribute('checked')).toBeFalsy(); - element(by.model('master')).click(); - expect(element(by.id('checkSlave')).getAttribute('checked')).toBeTruthy(); - }); - -
    - * - * @element INPUT - * @param {expression} ngChecked If the {@link guide/expression expression} is truthy, - * then the `checked` attribute will be set on the element - */ - - -/** - * @ngdoc directive - * @name ngReadonly - * @restrict A - * @priority 100 - * - * @description - * The HTML specification does not require browsers to preserve the values of boolean attributes - * such as readonly. (Their presence means true and their absence means false.) - * If we put an Angular interpolation expression into such an attribute then the - * binding information would be lost when the browser removes the attribute. - * The `ngReadonly` directive solves this problem for the `readonly` attribute. - * This complementary directive is not removed by the browser and so provides - * a permanent reliable place to store the binding information. - * @example - - -
    - -
    - - it('should toggle readonly attr', function() { - expect(element(by.css('[type="text"]')).getAttribute('readonly')).toBeFalsy(); - element(by.model('checked')).click(); - expect(element(by.css('[type="text"]')).getAttribute('readonly')).toBeTruthy(); - }); - -
    - * - * @element INPUT - * @param {expression} ngReadonly If the {@link guide/expression expression} is truthy, - * then special attribute "readonly" will be set on the element - */ - - -/** - * @ngdoc directive - * @name ngSelected - * @restrict A - * @priority 100 - * - * @description - * The HTML specification does not require browsers to preserve the values of boolean attributes - * such as selected. (Their presence means true and their absence means false.) - * If we put an Angular interpolation expression into such an attribute then the - * binding information would be lost when the browser removes the attribute. - * The `ngSelected` directive solves this problem for the `selected` attribute. - * This complementary directive is not removed by the browser and so provides - * a permanent reliable place to store the binding information. - * - * @example - - -
    - -
    - - it('should select Greetings!', function() { - expect(element(by.id('greet')).getAttribute('selected')).toBeFalsy(); - element(by.model('selected')).click(); - expect(element(by.id('greet')).getAttribute('selected')).toBeTruthy(); - }); - -
    - * - * @element OPTION - * @param {expression} ngSelected If the {@link guide/expression expression} is truthy, - * then special attribute "selected" will be set on the element - */ - -/** - * @ngdoc directive - * @name ngOpen - * @restrict A - * @priority 100 - * - * @description - * The HTML specification does not require browsers to preserve the values of boolean attributes - * such as open. (Their presence means true and their absence means false.) - * If we put an Angular interpolation expression into such an attribute then the - * binding information would be lost when the browser removes the attribute. - * The `ngOpen` directive solves this problem for the `open` attribute. - * This complementary directive is not removed by the browser and so provides - * a permanent reliable place to store the binding information. - * @example - - -
    -
    - Show/Hide me -
    -
    - - it('should toggle open', function() { - expect(element(by.id('details')).getAttribute('open')).toBeFalsy(); - element(by.model('open')).click(); - expect(element(by.id('details')).getAttribute('open')).toBeTruthy(); - }); - -
    - * - * @element DETAILS - * @param {expression} ngOpen If the {@link guide/expression expression} is truthy, - * then special attribute "open" will be set on the element - */ - -var ngAttributeAliasDirectives = {}; - -// boolean attrs are evaluated -forEach(BOOLEAN_ATTR, function(propName, attrName) { - // binding to multiple is not supported - if (propName == "multiple") return; - - function defaultLinkFn(scope, element, attr) { - scope.$watch(attr[normalized], function ngBooleanAttrWatchAction(value) { - attr.$set(attrName, !!value); - }); - } - - var normalized = directiveNormalize('ng-' + attrName); - var linkFn = defaultLinkFn; - - if (propName === 'checked') { - linkFn = function(scope, element, attr) { - // ensuring ngChecked doesn't interfere with ngModel when both are set on the same input - if (attr.ngModel !== attr[normalized]) { - defaultLinkFn(scope, element, attr); - } - }; - } - - ngAttributeAliasDirectives[normalized] = function() { - return { - restrict: 'A', - priority: 100, - link: linkFn - }; - }; -}); - -// aliased input attrs are evaluated -forEach(ALIASED_ATTR, function(htmlAttr, ngAttr) { - ngAttributeAliasDirectives[ngAttr] = function() { - return { - priority: 100, - link: function(scope, element, attr) { - //special case ngPattern when a literal regular expression value - //is used as the expression (this way we don't have to watch anything). - if (ngAttr === "ngPattern" && attr.ngPattern.charAt(0) == "/") { - var match = attr.ngPattern.match(REGEX_STRING_REGEXP); - if (match) { - attr.$set("ngPattern", new RegExp(match[1], match[2])); - return; - } - } - - scope.$watch(attr[ngAttr], function ngAttrAliasWatchAction(value) { - attr.$set(ngAttr, value); - }); - } - }; - }; -}); - -// ng-src, ng-srcset, ng-href are interpolated -forEach(['src', 'srcset', 'href'], function(attrName) { - var normalized = directiveNormalize('ng-' + attrName); - ngAttributeAliasDirectives[normalized] = function() { - return { - priority: 99, // it needs to run after the attributes are interpolated - link: function(scope, element, attr) { - var propName = attrName, - name = attrName; - - if (attrName === 'href' && - toString.call(element.prop('href')) === '[object SVGAnimatedString]') { - name = 'xlinkHref'; - attr.$attr[name] = 'xlink:href'; - propName = null; - } - - attr.$observe(normalized, function(value) { - if (!value) { - if (attrName === 'href') { - attr.$set(name, null); - } - return; - } - - attr.$set(name, value); - - // on IE, if "ng:src" directive declaration is used and "src" attribute doesn't exist - // then calling element.setAttribute('src', 'foo') doesn't do anything, so we need - // to set the property as well to achieve the desired effect. - // we use attr[attrName] value since $set can sanitize the url. - if (msie && propName) element.prop(propName, attr[name]); - }); - } - }; - }; -}); - -/* global -nullFormCtrl, -SUBMITTED_CLASS, addSetValidityMethod: true - */ -var nullFormCtrl = { - $addControl: noop, - $$renameControl: nullFormRenameControl, - $removeControl: noop, - $setValidity: noop, - $setDirty: noop, - $setPristine: noop, - $setSubmitted: noop -}, -SUBMITTED_CLASS = 'ng-submitted'; - -function nullFormRenameControl(control, name) { - control.$name = name; -} - -/** - * @ngdoc type - * @name form.FormController - * - * @property {boolean} $pristine True if user has not interacted with the form yet. - * @property {boolean} $dirty True if user has already interacted with the form. - * @property {boolean} $valid True if all of the containing forms and controls are valid. - * @property {boolean} $invalid True if at least one containing control or form is invalid. - * @property {boolean} $submitted True if user has submitted the form even if its invalid. - * - * @property {Object} $error Is an object hash, containing references to controls or - * forms with failing validators, where: - * - * - keys are validation tokens (error names), - * - values are arrays of controls or forms that have a failing validator for given error name. - * - * Built-in validation tokens: - * - * - `email` - * - `max` - * - `maxlength` - * - `min` - * - `minlength` - * - `number` - * - `pattern` - * - `required` - * - `url` - * - `date` - * - `datetimelocal` - * - `time` - * - `week` - * - `month` - * - * @description - * `FormController` keeps track of all its controls and nested forms as well as the state of them, - * such as being valid/invalid or dirty/pristine. - * - * Each {@link ng.directive:form form} directive creates an instance - * of `FormController`. - * - */ -//asks for $scope to fool the BC controller module -FormController.$inject = ['$element', '$attrs', '$scope', '$animate', '$interpolate']; -function FormController(element, attrs, $scope, $animate, $interpolate) { - var form = this, - controls = []; - - var parentForm = form.$$parentForm = element.parent().controller('form') || nullFormCtrl; - - // init state - form.$error = {}; - form.$$success = {}; - form.$pending = undefined; - form.$name = $interpolate(attrs.name || attrs.ngForm || '')($scope); - form.$dirty = false; - form.$pristine = true; - form.$valid = true; - form.$invalid = false; - form.$submitted = false; - - parentForm.$addControl(form); - - /** - * @ngdoc method - * @name form.FormController#$rollbackViewValue - * - * @description - * Rollback all form controls pending updates to the `$modelValue`. - * - * Updates may be pending by a debounced event or because the input is waiting for a some future - * event defined in `ng-model-options`. This method is typically needed by the reset button of - * a form that uses `ng-model-options` to pend updates. - */ - form.$rollbackViewValue = function() { - forEach(controls, function(control) { - control.$rollbackViewValue(); - }); - }; - - /** - * @ngdoc method - * @name form.FormController#$commitViewValue - * - * @description - * Commit all form controls pending updates to the `$modelValue`. - * - * Updates may be pending by a debounced event or because the input is waiting for a some future - * event defined in `ng-model-options`. This method is rarely needed as `NgModelController` - * usually handles calling this in response to input events. - */ - form.$commitViewValue = function() { - forEach(controls, function(control) { - control.$commitViewValue(); - }); - }; - - /** - * @ngdoc method - * @name form.FormController#$addControl - * - * @description - * Register a control with the form. - * - * Input elements using ngModelController do this automatically when they are linked. - */ - form.$addControl = function(control) { - // Breaking change - before, inputs whose name was "hasOwnProperty" were quietly ignored - // and not added to the scope. Now we throw an error. - assertNotHasOwnProperty(control.$name, 'input'); - controls.push(control); - - if (control.$name) { - form[control.$name] = control; - } - }; - - // Private API: rename a form control - form.$$renameControl = function(control, newName) { - var oldName = control.$name; - - if (form[oldName] === control) { - delete form[oldName]; - } - form[newName] = control; - control.$name = newName; - }; - - /** - * @ngdoc method - * @name form.FormController#$removeControl - * - * @description - * Deregister a control from the form. - * - * Input elements using ngModelController do this automatically when they are destroyed. - */ - form.$removeControl = function(control) { - if (control.$name && form[control.$name] === control) { - delete form[control.$name]; - } - forEach(form.$pending, function(value, name) { - form.$setValidity(name, null, control); - }); - forEach(form.$error, function(value, name) { - form.$setValidity(name, null, control); - }); - forEach(form.$$success, function(value, name) { - form.$setValidity(name, null, control); - }); - - arrayRemove(controls, control); - }; - - - /** - * @ngdoc method - * @name form.FormController#$setValidity - * - * @description - * Sets the validity of a form control. - * - * This method will also propagate to parent forms. - */ - addSetValidityMethod({ - ctrl: this, - $element: element, - set: function(object, property, controller) { - var list = object[property]; - if (!list) { - object[property] = [controller]; - } else { - var index = list.indexOf(controller); - if (index === -1) { - list.push(controller); - } - } - }, - unset: function(object, property, controller) { - var list = object[property]; - if (!list) { - return; - } - arrayRemove(list, controller); - if (list.length === 0) { - delete object[property]; - } - }, - parentForm: parentForm, - $animate: $animate - }); - - /** - * @ngdoc method - * @name form.FormController#$setDirty - * - * @description - * Sets the form to a dirty state. - * - * This method can be called to add the 'ng-dirty' class and set the form to a dirty - * state (ng-dirty class). This method will also propagate to parent forms. - */ - form.$setDirty = function() { - $animate.removeClass(element, PRISTINE_CLASS); - $animate.addClass(element, DIRTY_CLASS); - form.$dirty = true; - form.$pristine = false; - parentForm.$setDirty(); - }; - - /** - * @ngdoc method - * @name form.FormController#$setPristine - * - * @description - * Sets the form to its pristine state. - * - * This method can be called to remove the 'ng-dirty' class and set the form to its pristine - * state (ng-pristine class). This method will also propagate to all the controls contained - * in this form. - * - * Setting a form back to a pristine state is often useful when we want to 'reuse' a form after - * saving or resetting it. - */ - form.$setPristine = function() { - $animate.setClass(element, PRISTINE_CLASS, DIRTY_CLASS + ' ' + SUBMITTED_CLASS); - form.$dirty = false; - form.$pristine = true; - form.$submitted = false; - forEach(controls, function(control) { - control.$setPristine(); - }); - }; - - /** - * @ngdoc method - * @name form.FormController#$setUntouched - * - * @description - * Sets the form to its untouched state. - * - * This method can be called to remove the 'ng-touched' class and set the form controls to their - * untouched state (ng-untouched class). - * - * Setting a form controls back to their untouched state is often useful when setting the form - * back to its pristine state. - */ - form.$setUntouched = function() { - forEach(controls, function(control) { - control.$setUntouched(); - }); - }; - - /** - * @ngdoc method - * @name form.FormController#$setSubmitted - * - * @description - * Sets the form to its submitted state. - */ - form.$setSubmitted = function() { - $animate.addClass(element, SUBMITTED_CLASS); - form.$submitted = true; - parentForm.$setSubmitted(); - }; -} - -/** - * @ngdoc directive - * @name ngForm - * @restrict EAC - * - * @description - * Nestable alias of {@link ng.directive:form `form`} directive. HTML - * does not allow nesting of form elements. It is useful to nest forms, for example if the validity of a - * sub-group of controls needs to be determined. - * - * Note: the purpose of `ngForm` is to group controls, - * but not to be a replacement for the `
    ` tag with all of its capabilities - * (e.g. posting to the server, ...). - * - * @param {string=} ngForm|name Name of the form. If specified, the form controller will be published into - * related scope, under this name. - * - */ - - /** - * @ngdoc directive - * @name form - * @restrict E - * - * @description - * Directive that instantiates - * {@link form.FormController FormController}. - * - * If the `name` attribute is specified, the form controller is published onto the current scope under - * this name. - * - * # Alias: {@link ng.directive:ngForm `ngForm`} - * - * In Angular, forms can be nested. This means that the outer form is valid when all of the child - * forms are valid as well. However, browsers do not allow nesting of `` elements, so - * Angular provides the {@link ng.directive:ngForm `ngForm`} directive which behaves identically to - * `` but can be nested. This allows you to have nested forms, which is very useful when - * using Angular validation directives in forms that are dynamically generated using the - * {@link ng.directive:ngRepeat `ngRepeat`} directive. Since you cannot dynamically generate the `name` - * attribute of input elements using interpolation, you have to wrap each set of repeated inputs in an - * `ngForm` directive and nest these in an outer `form` element. - * - * - * # CSS classes - * - `ng-valid` is set if the form is valid. - * - `ng-invalid` is set if the form is invalid. - * - `ng-pristine` is set if the form is pristine. - * - `ng-dirty` is set if the form is dirty. - * - `ng-submitted` is set if the form was submitted. - * - * Keep in mind that ngAnimate can detect each of these classes when added and removed. - * - * - * # Submitting a form and preventing the default action - * - * Since the role of forms in client-side Angular applications is different than in classical - * roundtrip apps, it is desirable for the browser not to translate the form submission into a full - * page reload that sends the data to the server. Instead some javascript logic should be triggered - * to handle the form submission in an application-specific way. - * - * For this reason, Angular prevents the default action (form submission to the server) unless the - * `` element has an `action` attribute specified. - * - * You can use one of the following two ways to specify what javascript method should be called when - * a form is submitted: - * - * - {@link ng.directive:ngSubmit ngSubmit} directive on the form element - * - {@link ng.directive:ngClick ngClick} directive on the first - * button or input field of type submit (input[type=submit]) - * - * To prevent double execution of the handler, use only one of the {@link ng.directive:ngSubmit ngSubmit} - * or {@link ng.directive:ngClick ngClick} directives. - * This is because of the following form submission rules in the HTML specification: - * - * - If a form has only one input field then hitting enter in this field triggers form submit - * (`ngSubmit`) - * - if a form has 2+ input fields and no buttons or input[type=submit] then hitting enter - * doesn't trigger submit - * - if a form has one or more input fields and one or more buttons or input[type=submit] then - * hitting enter in any of the input fields will trigger the click handler on the *first* button or - * input[type=submit] (`ngClick`) *and* a submit handler on the enclosing form (`ngSubmit`) - * - * Any pending `ngModelOptions` changes will take place immediately when an enclosing form is - * submitted. Note that `ngClick` events will occur before the model is updated. Use `ngSubmit` - * to have access to the updated model. - * - * ## Animation Hooks - * - * Animations in ngForm are triggered when any of the associated CSS classes are added and removed. - * These classes are: `.ng-pristine`, `.ng-dirty`, `.ng-invalid` and `.ng-valid` as well as any - * other validations that are performed within the form. Animations in ngForm are similar to how - * they work in ngClass and animations can be hooked into using CSS transitions, keyframes as well - * as JS animations. - * - * The following example shows a simple way to utilize CSS transitions to style a form element - * that has been rendered as invalid after it has been validated: - * - *
    - * //be sure to include ngAnimate as a module to hook into more
    - * //advanced animations
    - * .my-form {
    - *   transition:0.5s linear all;
    - *   background: white;
    - * }
    - * .my-form.ng-invalid {
    - *   background: red;
    - *   color:white;
    - * }
    - * 
    - * - * @example - - - - - - userType: - Required!
    - userType = {{userType}}
    - myForm.input.$valid = {{myForm.input.$valid}}
    - myForm.input.$error = {{myForm.input.$error}}
    - myForm.$valid = {{myForm.$valid}}
    - myForm.$error.required = {{!!myForm.$error.required}}
    - -
    - - it('should initialize to model', function() { - var userType = element(by.binding('userType')); - var valid = element(by.binding('myForm.input.$valid')); - - expect(userType.getText()).toContain('guest'); - expect(valid.getText()).toContain('true'); - }); - - it('should be invalid if empty', function() { - var userType = element(by.binding('userType')); - var valid = element(by.binding('myForm.input.$valid')); - var userInput = element(by.model('userType')); - - userInput.clear(); - userInput.sendKeys(''); - - expect(userType.getText()).toEqual('userType ='); - expect(valid.getText()).toContain('false'); - }); - -
    - * - * @param {string=} name Name of the form. If specified, the form controller will be published into - * related scope, under this name. - */ -var formDirectiveFactory = function(isNgForm) { - return ['$timeout', '$parse', function($timeout, $parse) { - var formDirective = { - name: 'form', - restrict: isNgForm ? 'EAC' : 'E', - controller: FormController, - compile: function ngFormCompile(formElement, attr) { - // Setup initial state of the control - formElement.addClass(PRISTINE_CLASS).addClass(VALID_CLASS); - - var nameAttr = attr.name ? 'name' : (isNgForm && attr.ngForm ? 'ngForm' : false); - - return { - pre: function ngFormPreLink(scope, formElement, attr, controller) { - // if `action` attr is not present on the form, prevent the default action (submission) - if (!('action' in attr)) { - // we can't use jq events because if a form is destroyed during submission the default - // action is not prevented. see #1238 - // - // IE 9 is not affected because it doesn't fire a submit event and try to do a full - // page reload if the form was destroyed by submission of the form via a click handler - // on a button in the form. Looks like an IE9 specific bug. - var handleFormSubmission = function(event) { - scope.$apply(function() { - controller.$commitViewValue(); - controller.$setSubmitted(); - }); - - event.preventDefault(); - }; - - addEventListenerFn(formElement[0], 'submit', handleFormSubmission); - - // unregister the preventDefault listener so that we don't not leak memory but in a - // way that will achieve the prevention of the default action. - formElement.on('$destroy', function() { - $timeout(function() { - removeEventListenerFn(formElement[0], 'submit', handleFormSubmission); - }, 0, false); - }); - } - - var parentFormCtrl = controller.$$parentForm; - var setter = nameAttr ? getSetter(controller.$name) : noop; - - if (nameAttr) { - setter(scope, controller); - attr.$observe(nameAttr, function(newValue) { - if (controller.$name === newValue) return; - setter(scope, undefined); - parentFormCtrl.$$renameControl(controller, newValue); - setter = getSetter(controller.$name); - setter(scope, controller); - }); - } - formElement.on('$destroy', function() { - parentFormCtrl.$removeControl(controller); - setter(scope, undefined); - extend(controller, nullFormCtrl); //stop propagating child destruction handlers upwards - }); - } - }; - } - }; - - return formDirective; - - function getSetter(expression) { - if (expression === '') { - //create an assignable expression, so forms with an empty name can be renamed later - return $parse('this[""]').assign; - } - return $parse(expression).assign || noop; - } - }]; -}; - -var formDirective = formDirectiveFactory(); -var ngFormDirective = formDirectiveFactory(true); - -/* global VALID_CLASS: false, - INVALID_CLASS: false, - PRISTINE_CLASS: false, - DIRTY_CLASS: false, - UNTOUCHED_CLASS: false, - TOUCHED_CLASS: false, - ngModelMinErr: false, -*/ - -// Regex code is obtained from SO: https://stackoverflow.com/questions/3143070/javascript-regex-iso-datetime#answer-3143231 -var ISO_DATE_REGEXP = /\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+([+-][0-2]\d:[0-5]\d|Z)/; -var URL_REGEXP = /^(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?$/; -var EMAIL_REGEXP = /^[a-z0-9!#$%&'*+\/=?^_`{|}~.-]+@[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/i; -var NUMBER_REGEXP = /^\s*(\-|\+)?(\d+|(\d*(\.\d*)))([eE][+-]?\d+)?\s*$/; -var DATE_REGEXP = /^(\d{4})-(\d{2})-(\d{2})$/; -var DATETIMELOCAL_REGEXP = /^(\d{4})-(\d\d)-(\d\d)T(\d\d):(\d\d)(?::(\d\d)(\.\d{1,3})?)?$/; -var WEEK_REGEXP = /^(\d{4})-W(\d\d)$/; -var MONTH_REGEXP = /^(\d{4})-(\d\d)$/; -var TIME_REGEXP = /^(\d\d):(\d\d)(?::(\d\d)(\.\d{1,3})?)?$/; - -var inputType = { - - /** - * @ngdoc input - * @name input[text] - * - * @description - * Standard HTML text input with angular data binding, inherited by most of the `input` elements. - * - * - * @param {string} ngModel Assignable angular expression to data-bind to. - * @param {string=} name Property name of the form under which the control is published. - * @param {string=} required Adds `required` validation error key if the value is not entered. - * @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to - * the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of - * `required` when you want to data-bind to the `required` attribute. - * @param {number=} ngMinlength Sets `minlength` validation error key if the value is shorter than - * minlength. - * @param {number=} ngMaxlength Sets `maxlength` validation error key if the value is longer than - * maxlength. Setting the attribute to a negative or non-numeric value, allows view values of - * any length. - * @param {string=} pattern Similar to `ngPattern` except that the attribute value is the actual string - * that contains the regular expression body that will be converted to a regular expression - * as in the ngPattern directive. - * @param {string=} ngPattern Sets `pattern` validation error key if the ngModel value does not match - * a RegExp found by evaluating the Angular expression given in the attribute value. - * If the expression evaluates to a RegExp object, then this is used directly. - * If the expression evaluates to a string, then it will be converted to a RegExp - * after wrapping it in `^` and `$` characters. For instance, `"abc"` will be converted to - * `new RegExp('^abc$')`.
    - * **Note:** Avoid using the `g` flag on the RegExp, as it will cause each successive search to - * start at the index of the last search's match, thus not taking the whole input value into - * account. - * @param {string=} ngChange Angular expression to be executed when input changes due to user - * interaction with the input element. - * @param {boolean=} [ngTrim=true] If set to false Angular will not automatically trim the input. - * This parameter is ignored for input[type=password] controls, which will never trim the - * input. - * - * @example - - - -
    - -
    - - Required! - - Single word only! -
    - text = {{example.text}}
    - myForm.input.$valid = {{myForm.input.$valid}}
    - myForm.input.$error = {{myForm.input.$error}}
    - myForm.$valid = {{myForm.$valid}}
    - myForm.$error.required = {{!!myForm.$error.required}}
    -
    -
    - - var text = element(by.binding('example.text')); - var valid = element(by.binding('myForm.input.$valid')); - var input = element(by.model('example.text')); - - it('should initialize to model', function() { - expect(text.getText()).toContain('guest'); - expect(valid.getText()).toContain('true'); - }); - - it('should be invalid if empty', function() { - input.clear(); - input.sendKeys(''); - - expect(text.getText()).toEqual('text ='); - expect(valid.getText()).toContain('false'); - }); - - it('should be invalid if multi word', function() { - input.clear(); - input.sendKeys('hello world'); - - expect(valid.getText()).toContain('false'); - }); - -
    - */ - 'text': textInputType, - - /** - * @ngdoc input - * @name input[date] - * - * @description - * Input with date validation and transformation. In browsers that do not yet support - * the HTML5 date input, a text element will be used. In that case, text must be entered in a valid ISO-8601 - * date format (yyyy-MM-dd), for example: `2009-01-06`. Since many - * modern browsers do not yet support this input type, it is important to provide cues to users on the - * expected input format via a placeholder or label. - * - * The model must always be a Date object, otherwise Angular will throw an error. - * Invalid `Date` objects (dates whose `getTime()` is `NaN`) will be rendered as an empty string. - * - * The timezone to be used to read/write the `Date` instance in the model can be defined using - * {@link ng.directive:ngModelOptions ngModelOptions}. By default, this is the timezone of the browser. - * - * @param {string} ngModel Assignable angular expression to data-bind to. - * @param {string=} name Property name of the form under which the control is published. - * @param {string=} min Sets the `min` validation error key if the value entered is less than `min`. This must be a - * valid ISO date string (yyyy-MM-dd). - * @param {string=} max Sets the `max` validation error key if the value entered is greater than `max`. This must be - * a valid ISO date string (yyyy-MM-dd). - * @param {string=} required Sets `required` validation error key if the value is not entered. - * @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to - * the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of - * `required` when you want to data-bind to the `required` attribute. - * @param {string=} ngChange Angular expression to be executed when input changes due to user - * interaction with the input element. - * - * @example - - - -
    - - -
    - - Required! - - Not a valid date! -
    - value = {{example.value | date: "yyyy-MM-dd"}}
    - myForm.input.$valid = {{myForm.input.$valid}}
    - myForm.input.$error = {{myForm.input.$error}}
    - myForm.$valid = {{myForm.$valid}}
    - myForm.$error.required = {{!!myForm.$error.required}}
    -
    -
    - - var value = element(by.binding('example.value | date: "yyyy-MM-dd"')); - var valid = element(by.binding('myForm.input.$valid')); - var input = element(by.model('example.value')); - - // currently protractor/webdriver does not support - // sending keys to all known HTML5 input controls - // for various browsers (see https://github.com/angular/protractor/issues/562). - function setInput(val) { - // set the value of the element and force validation. - var scr = "var ipt = document.getElementById('exampleInput'); " + - "ipt.value = '" + val + "';" + - "angular.element(ipt).scope().$apply(function(s) { s.myForm[ipt.name].$setViewValue('" + val + "'); });"; - browser.executeScript(scr); - } - - it('should initialize to model', function() { - expect(value.getText()).toContain('2013-10-22'); - expect(valid.getText()).toContain('myForm.input.$valid = true'); - }); - - it('should be invalid if empty', function() { - setInput(''); - expect(value.getText()).toEqual('value ='); - expect(valid.getText()).toContain('myForm.input.$valid = false'); - }); - - it('should be invalid if over max', function() { - setInput('2015-01-01'); - expect(value.getText()).toContain(''); - expect(valid.getText()).toContain('myForm.input.$valid = false'); - }); - -
    - */ - 'date': createDateInputType('date', DATE_REGEXP, - createDateParser(DATE_REGEXP, ['yyyy', 'MM', 'dd']), - 'yyyy-MM-dd'), - - /** - * @ngdoc input - * @name input[datetime-local] - * - * @description - * Input with datetime validation and transformation. In browsers that do not yet support - * the HTML5 date input, a text element will be used. In that case, the text must be entered in a valid ISO-8601 - * local datetime format (yyyy-MM-ddTHH:mm:ss), for example: `2010-12-28T14:57:00`. - * - * The model must always be a Date object, otherwise Angular will throw an error. - * Invalid `Date` objects (dates whose `getTime()` is `NaN`) will be rendered as an empty string. - * - * The timezone to be used to read/write the `Date` instance in the model can be defined using - * {@link ng.directive:ngModelOptions ngModelOptions}. By default, this is the timezone of the browser. - * - * @param {string} ngModel Assignable angular expression to data-bind to. - * @param {string=} name Property name of the form under which the control is published. - * @param {string=} min Sets the `min` validation error key if the value entered is less than `min`. This must be a - * valid ISO datetime format (yyyy-MM-ddTHH:mm:ss). - * @param {string=} max Sets the `max` validation error key if the value entered is greater than `max`. This must be - * a valid ISO datetime format (yyyy-MM-ddTHH:mm:ss). - * @param {string=} required Sets `required` validation error key if the value is not entered. - * @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to - * the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of - * `required` when you want to data-bind to the `required` attribute. - * @param {string=} ngChange Angular expression to be executed when input changes due to user - * interaction with the input element. - * - * @example - - - -
    - - -
    - - Required! - - Not a valid date! -
    - value = {{example.value | date: "yyyy-MM-ddTHH:mm:ss"}}
    - myForm.input.$valid = {{myForm.input.$valid}}
    - myForm.input.$error = {{myForm.input.$error}}
    - myForm.$valid = {{myForm.$valid}}
    - myForm.$error.required = {{!!myForm.$error.required}}
    -
    -
    - - var value = element(by.binding('example.value | date: "yyyy-MM-ddTHH:mm:ss"')); - var valid = element(by.binding('myForm.input.$valid')); - var input = element(by.model('example.value')); - - // currently protractor/webdriver does not support - // sending keys to all known HTML5 input controls - // for various browsers (https://github.com/angular/protractor/issues/562). - function setInput(val) { - // set the value of the element and force validation. - var scr = "var ipt = document.getElementById('exampleInput'); " + - "ipt.value = '" + val + "';" + - "angular.element(ipt).scope().$apply(function(s) { s.myForm[ipt.name].$setViewValue('" + val + "'); });"; - browser.executeScript(scr); - } - - it('should initialize to model', function() { - expect(value.getText()).toContain('2010-12-28T14:57:00'); - expect(valid.getText()).toContain('myForm.input.$valid = true'); - }); - - it('should be invalid if empty', function() { - setInput(''); - expect(value.getText()).toEqual('value ='); - expect(valid.getText()).toContain('myForm.input.$valid = false'); - }); - - it('should be invalid if over max', function() { - setInput('2015-01-01T23:59:00'); - expect(value.getText()).toContain(''); - expect(valid.getText()).toContain('myForm.input.$valid = false'); - }); - -
    - */ - 'datetime-local': createDateInputType('datetimelocal', DATETIMELOCAL_REGEXP, - createDateParser(DATETIMELOCAL_REGEXP, ['yyyy', 'MM', 'dd', 'HH', 'mm', 'ss', 'sss']), - 'yyyy-MM-ddTHH:mm:ss.sss'), - - /** - * @ngdoc input - * @name input[time] - * - * @description - * Input with time validation and transformation. In browsers that do not yet support - * the HTML5 date input, a text element will be used. In that case, the text must be entered in a valid ISO-8601 - * local time format (HH:mm:ss), for example: `14:57:00`. Model must be a Date object. This binding will always output a - * Date object to the model of January 1, 1970, or local date `new Date(1970, 0, 1, HH, mm, ss)`. - * - * The model must always be a Date object, otherwise Angular will throw an error. - * Invalid `Date` objects (dates whose `getTime()` is `NaN`) will be rendered as an empty string. - * - * The timezone to be used to read/write the `Date` instance in the model can be defined using - * {@link ng.directive:ngModelOptions ngModelOptions}. By default, this is the timezone of the browser. - * - * @param {string} ngModel Assignable angular expression to data-bind to. - * @param {string=} name Property name of the form under which the control is published. - * @param {string=} min Sets the `min` validation error key if the value entered is less than `min`. This must be a - * valid ISO time format (HH:mm:ss). - * @param {string=} max Sets the `max` validation error key if the value entered is greater than `max`. This must be a - * valid ISO time format (HH:mm:ss). - * @param {string=} required Sets `required` validation error key if the value is not entered. - * @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to - * the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of - * `required` when you want to data-bind to the `required` attribute. - * @param {string=} ngChange Angular expression to be executed when input changes due to user - * interaction with the input element. - * - * @example - - - -
    - - -
    - - Required! - - Not a valid date! -
    - value = {{example.value | date: "HH:mm:ss"}}
    - myForm.input.$valid = {{myForm.input.$valid}}
    - myForm.input.$error = {{myForm.input.$error}}
    - myForm.$valid = {{myForm.$valid}}
    - myForm.$error.required = {{!!myForm.$error.required}}
    -
    -
    - - var value = element(by.binding('example.value | date: "HH:mm:ss"')); - var valid = element(by.binding('myForm.input.$valid')); - var input = element(by.model('example.value')); - - // currently protractor/webdriver does not support - // sending keys to all known HTML5 input controls - // for various browsers (https://github.com/angular/protractor/issues/562). - function setInput(val) { - // set the value of the element and force validation. - var scr = "var ipt = document.getElementById('exampleInput'); " + - "ipt.value = '" + val + "';" + - "angular.element(ipt).scope().$apply(function(s) { s.myForm[ipt.name].$setViewValue('" + val + "'); });"; - browser.executeScript(scr); - } - - it('should initialize to model', function() { - expect(value.getText()).toContain('14:57:00'); - expect(valid.getText()).toContain('myForm.input.$valid = true'); - }); - - it('should be invalid if empty', function() { - setInput(''); - expect(value.getText()).toEqual('value ='); - expect(valid.getText()).toContain('myForm.input.$valid = false'); - }); - - it('should be invalid if over max', function() { - setInput('23:59:00'); - expect(value.getText()).toContain(''); - expect(valid.getText()).toContain('myForm.input.$valid = false'); - }); - -
    - */ - 'time': createDateInputType('time', TIME_REGEXP, - createDateParser(TIME_REGEXP, ['HH', 'mm', 'ss', 'sss']), - 'HH:mm:ss.sss'), - - /** - * @ngdoc input - * @name input[week] - * - * @description - * Input with week-of-the-year validation and transformation to Date. In browsers that do not yet support - * the HTML5 week input, a text element will be used. In that case, the text must be entered in a valid ISO-8601 - * week format (yyyy-W##), for example: `2013-W02`. - * - * The model must always be a Date object, otherwise Angular will throw an error. - * Invalid `Date` objects (dates whose `getTime()` is `NaN`) will be rendered as an empty string. - * - * The timezone to be used to read/write the `Date` instance in the model can be defined using - * {@link ng.directive:ngModelOptions ngModelOptions}. By default, this is the timezone of the browser. - * - * @param {string} ngModel Assignable angular expression to data-bind to. - * @param {string=} name Property name of the form under which the control is published. - * @param {string=} min Sets the `min` validation error key if the value entered is less than `min`. This must be a - * valid ISO week format (yyyy-W##). - * @param {string=} max Sets the `max` validation error key if the value entered is greater than `max`. This must be - * a valid ISO week format (yyyy-W##). - * @param {string=} required Sets `required` validation error key if the value is not entered. - * @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to - * the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of - * `required` when you want to data-bind to the `required` attribute. - * @param {string=} ngChange Angular expression to be executed when input changes due to user - * interaction with the input element. - * - * @example - - - -
    - -
    - - Required! - - Not a valid date! -
    - value = {{example.value | date: "yyyy-Www"}}
    - myForm.input.$valid = {{myForm.input.$valid}}
    - myForm.input.$error = {{myForm.input.$error}}
    - myForm.$valid = {{myForm.$valid}}
    - myForm.$error.required = {{!!myForm.$error.required}}
    -
    -
    - - var value = element(by.binding('example.value | date: "yyyy-Www"')); - var valid = element(by.binding('myForm.input.$valid')); - var input = element(by.model('example.value')); - - // currently protractor/webdriver does not support - // sending keys to all known HTML5 input controls - // for various browsers (https://github.com/angular/protractor/issues/562). - function setInput(val) { - // set the value of the element and force validation. - var scr = "var ipt = document.getElementById('exampleInput'); " + - "ipt.value = '" + val + "';" + - "angular.element(ipt).scope().$apply(function(s) { s.myForm[ipt.name].$setViewValue('" + val + "'); });"; - browser.executeScript(scr); - } - - it('should initialize to model', function() { - expect(value.getText()).toContain('2013-W01'); - expect(valid.getText()).toContain('myForm.input.$valid = true'); - }); - - it('should be invalid if empty', function() { - setInput(''); - expect(value.getText()).toEqual('value ='); - expect(valid.getText()).toContain('myForm.input.$valid = false'); - }); - - it('should be invalid if over max', function() { - setInput('2015-W01'); - expect(value.getText()).toContain(''); - expect(valid.getText()).toContain('myForm.input.$valid = false'); - }); - -
    - */ - 'week': createDateInputType('week', WEEK_REGEXP, weekParser, 'yyyy-Www'), - - /** - * @ngdoc input - * @name input[month] - * - * @description - * Input with month validation and transformation. In browsers that do not yet support - * the HTML5 month input, a text element will be used. In that case, the text must be entered in a valid ISO-8601 - * month format (yyyy-MM), for example: `2009-01`. - * - * The model must always be a Date object, otherwise Angular will throw an error. - * Invalid `Date` objects (dates whose `getTime()` is `NaN`) will be rendered as an empty string. - * If the model is not set to the first of the month, the next view to model update will set it - * to the first of the month. - * - * The timezone to be used to read/write the `Date` instance in the model can be defined using - * {@link ng.directive:ngModelOptions ngModelOptions}. By default, this is the timezone of the browser. - * - * @param {string} ngModel Assignable angular expression to data-bind to. - * @param {string=} name Property name of the form under which the control is published. - * @param {string=} min Sets the `min` validation error key if the value entered is less than `min`. This must be - * a valid ISO month format (yyyy-MM). - * @param {string=} max Sets the `max` validation error key if the value entered is greater than `max`. This must - * be a valid ISO month format (yyyy-MM). - * @param {string=} required Sets `required` validation error key if the value is not entered. - * @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to - * the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of - * `required` when you want to data-bind to the `required` attribute. - * @param {string=} ngChange Angular expression to be executed when input changes due to user - * interaction with the input element. - * - * @example - - - -
    - - -
    - - Required! - - Not a valid month! -
    - value = {{example.value | date: "yyyy-MM"}}
    - myForm.input.$valid = {{myForm.input.$valid}}
    - myForm.input.$error = {{myForm.input.$error}}
    - myForm.$valid = {{myForm.$valid}}
    - myForm.$error.required = {{!!myForm.$error.required}}
    -
    -
    - - var value = element(by.binding('example.value | date: "yyyy-MM"')); - var valid = element(by.binding('myForm.input.$valid')); - var input = element(by.model('example.value')); - - // currently protractor/webdriver does not support - // sending keys to all known HTML5 input controls - // for various browsers (https://github.com/angular/protractor/issues/562). - function setInput(val) { - // set the value of the element and force validation. - var scr = "var ipt = document.getElementById('exampleInput'); " + - "ipt.value = '" + val + "';" + - "angular.element(ipt).scope().$apply(function(s) { s.myForm[ipt.name].$setViewValue('" + val + "'); });"; - browser.executeScript(scr); - } - - it('should initialize to model', function() { - expect(value.getText()).toContain('2013-10'); - expect(valid.getText()).toContain('myForm.input.$valid = true'); - }); - - it('should be invalid if empty', function() { - setInput(''); - expect(value.getText()).toEqual('value ='); - expect(valid.getText()).toContain('myForm.input.$valid = false'); - }); - - it('should be invalid if over max', function() { - setInput('2015-01'); - expect(value.getText()).toContain(''); - expect(valid.getText()).toContain('myForm.input.$valid = false'); - }); - -
    - */ - 'month': createDateInputType('month', MONTH_REGEXP, - createDateParser(MONTH_REGEXP, ['yyyy', 'MM']), - 'yyyy-MM'), - - /** - * @ngdoc input - * @name input[number] - * - * @description - * Text input with number validation and transformation. Sets the `number` validation - * error if not a valid number. - * - *
    - * The model must always be of type `number` otherwise Angular will throw an error. - * Be aware that a string containing a number is not enough. See the {@link ngModel:numfmt} - * error docs for more information and an example of how to convert your model if necessary. - *
    - * - * ## Issues with HTML5 constraint validation - * - * In browsers that follow the - * [HTML5 specification](https://html.spec.whatwg.org/multipage/forms.html#number-state-%28type=number%29), - * `input[number]` does not work as expected with {@link ngModelOptions `ngModelOptions.allowInvalid`}. - * If a non-number is entered in the input, the browser will report the value as an empty string, - * which means the view / model values in `ngModel` and subsequently the scope value - * will also be an empty string. - * - * - * @param {string} ngModel Assignable angular expression to data-bind to. - * @param {string=} name Property name of the form under which the control is published. - * @param {string=} min Sets the `min` validation error key if the value entered is less than `min`. - * @param {string=} max Sets the `max` validation error key if the value entered is greater than `max`. - * @param {string=} required Sets `required` validation error key if the value is not entered. - * @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to - * the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of - * `required` when you want to data-bind to the `required` attribute. - * @param {number=} ngMinlength Sets `minlength` validation error key if the value is shorter than - * minlength. - * @param {number=} ngMaxlength Sets `maxlength` validation error key if the value is longer than - * maxlength. Setting the attribute to a negative or non-numeric value, allows view values of - * any length. - * @param {string=} pattern Similar to `ngPattern` except that the attribute value is the actual string - * that contains the regular expression body that will be converted to a regular expression - * as in the ngPattern directive. - * @param {string=} ngPattern Sets `pattern` validation error key if the ngModel value does not match - * a RegExp found by evaluating the Angular expression given in the attribute value. - * If the expression evaluates to a RegExp object, then this is used directly. - * If the expression evaluates to a string, then it will be converted to a RegExp - * after wrapping it in `^` and `$` characters. For instance, `"abc"` will be converted to - * `new RegExp('^abc$')`.
    - * **Note:** Avoid using the `g` flag on the RegExp, as it will cause each successive search to - * start at the index of the last search's match, thus not taking the whole input value into - * account. - * @param {string=} ngChange Angular expression to be executed when input changes due to user - * interaction with the input element. - * - * @example - - - -
    - -
    - - Required! - - Not valid number! -
    - value = {{example.value}}
    - myForm.input.$valid = {{myForm.input.$valid}}
    - myForm.input.$error = {{myForm.input.$error}}
    - myForm.$valid = {{myForm.$valid}}
    - myForm.$error.required = {{!!myForm.$error.required}}
    -
    -
    - - var value = element(by.binding('example.value')); - var valid = element(by.binding('myForm.input.$valid')); - var input = element(by.model('example.value')); - - it('should initialize to model', function() { - expect(value.getText()).toContain('12'); - expect(valid.getText()).toContain('true'); - }); - - it('should be invalid if empty', function() { - input.clear(); - input.sendKeys(''); - expect(value.getText()).toEqual('value ='); - expect(valid.getText()).toContain('false'); - }); - - it('should be invalid if over max', function() { - input.clear(); - input.sendKeys('123'); - expect(value.getText()).toEqual('value ='); - expect(valid.getText()).toContain('false'); - }); - -
    - */ - 'number': numberInputType, - - - /** - * @ngdoc input - * @name input[url] - * - * @description - * Text input with URL validation. Sets the `url` validation error key if the content is not a - * valid URL. - * - *
    - * **Note:** `input[url]` uses a regex to validate urls that is derived from the regex - * used in Chromium. If you need stricter validation, you can use `ng-pattern` or modify - * the built-in validators (see the {@link guide/forms Forms guide}) - *
    - * - * @param {string} ngModel Assignable angular expression to data-bind to. - * @param {string=} name Property name of the form under which the control is published. - * @param {string=} required Sets `required` validation error key if the value is not entered. - * @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to - * the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of - * `required` when you want to data-bind to the `required` attribute. - * @param {number=} ngMinlength Sets `minlength` validation error key if the value is shorter than - * minlength. - * @param {number=} ngMaxlength Sets `maxlength` validation error key if the value is longer than - * maxlength. Setting the attribute to a negative or non-numeric value, allows view values of - * any length. - * @param {string=} pattern Similar to `ngPattern` except that the attribute value is the actual string - * that contains the regular expression body that will be converted to a regular expression - * as in the ngPattern directive. - * @param {string=} ngPattern Sets `pattern` validation error key if the ngModel value does not match - * a RegExp found by evaluating the Angular expression given in the attribute value. - * If the expression evaluates to a RegExp object, then this is used directly. - * If the expression evaluates to a string, then it will be converted to a RegExp - * after wrapping it in `^` and `$` characters. For instance, `"abc"` will be converted to - * `new RegExp('^abc$')`.
    - * **Note:** Avoid using the `g` flag on the RegExp, as it will cause each successive search to - * start at the index of the last search's match, thus not taking the whole input value into - * account. - * @param {string=} ngChange Angular expression to be executed when input changes due to user - * interaction with the input element. - * - * @example - - - -
    -
    - - var text = element(by.binding('url.text')); - var valid = element(by.binding('myForm.input.$valid')); - var input = element(by.model('url.text')); - - it('should initialize to model', function() { - expect(text.getText()).toContain('http://google.com'); - expect(valid.getText()).toContain('true'); - }); - - it('should be invalid if empty', function() { - input.clear(); - input.sendKeys(''); - - expect(text.getText()).toEqual('text ='); - expect(valid.getText()).toContain('false'); - }); - - it('should be invalid if not url', function() { - input.clear(); - input.sendKeys('box'); - - expect(valid.getText()).toContain('false'); - }); - -
    - */ - 'url': urlInputType, - - - /** - * @ngdoc input - * @name input[email] - * - * @description - * Text input with email validation. Sets the `email` validation error key if not a valid email - * address. - * - *
    - * **Note:** `input[email]` uses a regex to validate email addresses that is derived from the regex - * used in Chromium. If you need stricter validation (e.g. requiring a top-level domain), you can - * use `ng-pattern` or modify the built-in validators (see the {@link guide/forms Forms guide}) - *
    - * - * @param {string} ngModel Assignable angular expression to data-bind to. - * @param {string=} name Property name of the form under which the control is published. - * @param {string=} required Sets `required` validation error key if the value is not entered. - * @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to - * the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of - * `required` when you want to data-bind to the `required` attribute. - * @param {number=} ngMinlength Sets `minlength` validation error key if the value is shorter than - * minlength. - * @param {number=} ngMaxlength Sets `maxlength` validation error key if the value is longer than - * maxlength. Setting the attribute to a negative or non-numeric value, allows view values of - * any length. - * @param {string=} pattern Similar to `ngPattern` except that the attribute value is the actual string - * that contains the regular expression body that will be converted to a regular expression - * as in the ngPattern directive. - * @param {string=} ngPattern Sets `pattern` validation error key if the ngModel value does not match - * a RegExp found by evaluating the Angular expression given in the attribute value. - * If the expression evaluates to a RegExp object, then this is used directly. - * If the expression evaluates to a string, then it will be converted to a RegExp - * after wrapping it in `^` and `$` characters. For instance, `"abc"` will be converted to - * `new RegExp('^abc$')`.
    - * **Note:** Avoid using the `g` flag on the RegExp, as it will cause each successive search to - * start at the index of the last search's match, thus not taking the whole input value into - * account. - * @param {string=} ngChange Angular expression to be executed when input changes due to user - * interaction with the input element. - * - * @example - - - -
    - -
    - - Required! - - Not valid email! -
    - text = {{email.text}}
    - myForm.input.$valid = {{myForm.input.$valid}}
    - myForm.input.$error = {{myForm.input.$error}}
    - myForm.$valid = {{myForm.$valid}}
    - myForm.$error.required = {{!!myForm.$error.required}}
    - myForm.$error.email = {{!!myForm.$error.email}}
    -
    -
    - - var text = element(by.binding('email.text')); - var valid = element(by.binding('myForm.input.$valid')); - var input = element(by.model('email.text')); - - it('should initialize to model', function() { - expect(text.getText()).toContain('me@example.com'); - expect(valid.getText()).toContain('true'); - }); - - it('should be invalid if empty', function() { - input.clear(); - input.sendKeys(''); - expect(text.getText()).toEqual('text ='); - expect(valid.getText()).toContain('false'); - }); - - it('should be invalid if not email', function() { - input.clear(); - input.sendKeys('xxx'); - - expect(valid.getText()).toContain('false'); - }); - -
    - */ - 'email': emailInputType, - - - /** - * @ngdoc input - * @name input[radio] - * - * @description - * HTML radio button. - * - * @param {string} ngModel Assignable angular expression to data-bind to. - * @param {string} value The value to which the `ngModel` expression should be set when selected. - * Note that `value` only supports `string` values, i.e. the scope model needs to be a string, - * too. Use `ngValue` if you need complex models (`number`, `object`, ...). - * @param {string=} name Property name of the form under which the control is published. - * @param {string=} ngChange Angular expression to be executed when input changes due to user - * interaction with the input element. - * @param {string} ngValue Angular expression to which `ngModel` will be be set when the radio - * is selected. Should be used instead of the `value` attribute if you need - * a non-string `ngModel` (`boolean`, `array`, ...). - * - * @example - - - -
    -
    -
    -
    - color = {{color.name | json}}
    -
    - Note that `ng-value="specialValue"` sets radio item's value to be the value of `$scope.specialValue`. -
    - - it('should change state', function() { - var color = element(by.binding('color.name')); - - expect(color.getText()).toContain('blue'); - - element.all(by.model('color.name')).get(0).click(); - - expect(color.getText()).toContain('red'); - }); - -
    - */ - 'radio': radioInputType, - - - /** - * @ngdoc input - * @name input[checkbox] - * - * @description - * HTML checkbox. - * - * @param {string} ngModel Assignable angular expression to data-bind to. - * @param {string=} name Property name of the form under which the control is published. - * @param {expression=} ngTrueValue The value to which the expression should be set when selected. - * @param {expression=} ngFalseValue The value to which the expression should be set when not selected. - * @param {string=} ngChange Angular expression to be executed when input changes due to user - * interaction with the input element. - * - * @example - - - -
    -
    -
    - value1 = {{checkboxModel.value1}}
    - value2 = {{checkboxModel.value2}}
    -
    -
    - - it('should change state', function() { - var value1 = element(by.binding('checkboxModel.value1')); - var value2 = element(by.binding('checkboxModel.value2')); - - expect(value1.getText()).toContain('true'); - expect(value2.getText()).toContain('YES'); - - element(by.model('checkboxModel.value1')).click(); - element(by.model('checkboxModel.value2')).click(); - - expect(value1.getText()).toContain('false'); - expect(value2.getText()).toContain('NO'); - }); - -
    - */ - 'checkbox': checkboxInputType, - - 'hidden': noop, - 'button': noop, - 'submit': noop, - 'reset': noop, - 'file': noop -}; - -function stringBasedInputType(ctrl) { - ctrl.$formatters.push(function(value) { - return ctrl.$isEmpty(value) ? value : value.toString(); - }); -} - -function textInputType(scope, element, attr, ctrl, $sniffer, $browser) { - baseInputType(scope, element, attr, ctrl, $sniffer, $browser); - stringBasedInputType(ctrl); -} - -function baseInputType(scope, element, attr, ctrl, $sniffer, $browser) { - var type = lowercase(element[0].type); - - // In composition mode, users are still inputing intermediate text buffer, - // hold the listener until composition is done. - // More about composition events: https://developer.mozilla.org/en-US/docs/Web/API/CompositionEvent - if (!$sniffer.android) { - var composing = false; - - element.on('compositionstart', function(data) { - composing = true; - }); - - element.on('compositionend', function() { - composing = false; - listener(); - }); - } - - var listener = function(ev) { - if (timeout) { - $browser.defer.cancel(timeout); - timeout = null; - } - if (composing) return; - var value = element.val(), - event = ev && ev.type; - - // By default we will trim the value - // If the attribute ng-trim exists we will avoid trimming - // If input type is 'password', the value is never trimmed - if (type !== 'password' && (!attr.ngTrim || attr.ngTrim !== 'false')) { - value = trim(value); - } - - // If a control is suffering from bad input (due to native validators), browsers discard its - // value, so it may be necessary to revalidate (by calling $setViewValue again) even if the - // control's value is the same empty value twice in a row. - if (ctrl.$viewValue !== value || (value === '' && ctrl.$$hasNativeValidators)) { - ctrl.$setViewValue(value, event); - } - }; - - // if the browser does support "input" event, we are fine - except on IE9 which doesn't fire the - // input event on backspace, delete or cut - if ($sniffer.hasEvent('input')) { - element.on('input', listener); - } else { - var timeout; - - var deferListener = function(ev, input, origValue) { - if (!timeout) { - timeout = $browser.defer(function() { - timeout = null; - if (!input || input.value !== origValue) { - listener(ev); - } - }); - } - }; - - element.on('keydown', function(event) { - var key = event.keyCode; - - // ignore - // command modifiers arrows - if (key === 91 || (15 < key && key < 19) || (37 <= key && key <= 40)) return; - - deferListener(event, this, this.value); - }); - - // if user modifies input value using context menu in IE, we need "paste" and "cut" events to catch it - if ($sniffer.hasEvent('paste')) { - element.on('paste cut', deferListener); - } - } - - // if user paste into input using mouse on older browser - // or form autocomplete on newer browser, we need "change" event to catch it - element.on('change', listener); - - ctrl.$render = function() { - // Workaround for Firefox validation #12102. - var value = ctrl.$isEmpty(ctrl.$viewValue) ? '' : ctrl.$viewValue; - if (element.val() !== value) { - element.val(value); - } - }; -} - -function weekParser(isoWeek, existingDate) { - if (isDate(isoWeek)) { - return isoWeek; - } - - if (isString(isoWeek)) { - WEEK_REGEXP.lastIndex = 0; - var parts = WEEK_REGEXP.exec(isoWeek); - if (parts) { - var year = +parts[1], - week = +parts[2], - hours = 0, - minutes = 0, - seconds = 0, - milliseconds = 0, - firstThurs = getFirstThursdayOfYear(year), - addDays = (week - 1) * 7; - - if (existingDate) { - hours = existingDate.getHours(); - minutes = existingDate.getMinutes(); - seconds = existingDate.getSeconds(); - milliseconds = existingDate.getMilliseconds(); - } - - return new Date(year, 0, firstThurs.getDate() + addDays, hours, minutes, seconds, milliseconds); - } - } - - return NaN; -} - -function createDateParser(regexp, mapping) { - return function(iso, date) { - var parts, map; - - if (isDate(iso)) { - return iso; - } - - if (isString(iso)) { - // When a date is JSON'ified to wraps itself inside of an extra - // set of double quotes. This makes the date parsing code unable - // to match the date string and parse it as a date. - if (iso.charAt(0) == '"' && iso.charAt(iso.length - 1) == '"') { - iso = iso.substring(1, iso.length - 1); - } - if (ISO_DATE_REGEXP.test(iso)) { - return new Date(iso); - } - regexp.lastIndex = 0; - parts = regexp.exec(iso); - - if (parts) { - parts.shift(); - if (date) { - map = { - yyyy: date.getFullYear(), - MM: date.getMonth() + 1, - dd: date.getDate(), - HH: date.getHours(), - mm: date.getMinutes(), - ss: date.getSeconds(), - sss: date.getMilliseconds() / 1000 - }; - } else { - map = { yyyy: 1970, MM: 1, dd: 1, HH: 0, mm: 0, ss: 0, sss: 0 }; - } - - forEach(parts, function(part, index) { - if (index < mapping.length) { - map[mapping[index]] = +part; - } - }); - return new Date(map.yyyy, map.MM - 1, map.dd, map.HH, map.mm, map.ss || 0, map.sss * 1000 || 0); - } - } - - return NaN; - }; -} - -function createDateInputType(type, regexp, parseDate, format) { - return function dynamicDateInputType(scope, element, attr, ctrl, $sniffer, $browser, $filter) { - badInputChecker(scope, element, attr, ctrl); - baseInputType(scope, element, attr, ctrl, $sniffer, $browser); - var timezone = ctrl && ctrl.$options && ctrl.$options.timezone; - var previousDate; - - ctrl.$$parserName = type; - ctrl.$parsers.push(function(value) { - if (ctrl.$isEmpty(value)) return null; - if (regexp.test(value)) { - // Note: We cannot read ctrl.$modelValue, as there might be a different - // parser/formatter in the processing chain so that the model - // contains some different data format! - var parsedDate = parseDate(value, previousDate); - if (timezone) { - parsedDate = convertTimezoneToLocal(parsedDate, timezone); - } - return parsedDate; - } - return undefined; - }); - - ctrl.$formatters.push(function(value) { - if (value && !isDate(value)) { - throw ngModelMinErr('datefmt', 'Expected `{0}` to be a date', value); - } - if (isValidDate(value)) { - previousDate = value; - if (previousDate && timezone) { - previousDate = convertTimezoneToLocal(previousDate, timezone, true); - } - return $filter('date')(value, format, timezone); - } else { - previousDate = null; - return ''; - } - }); - - if (isDefined(attr.min) || attr.ngMin) { - var minVal; - ctrl.$validators.min = function(value) { - return !isValidDate(value) || isUndefined(minVal) || parseDate(value) >= minVal; - }; - attr.$observe('min', function(val) { - minVal = parseObservedDateValue(val); - ctrl.$validate(); - }); - } - - if (isDefined(attr.max) || attr.ngMax) { - var maxVal; - ctrl.$validators.max = function(value) { - return !isValidDate(value) || isUndefined(maxVal) || parseDate(value) <= maxVal; - }; - attr.$observe('max', function(val) { - maxVal = parseObservedDateValue(val); - ctrl.$validate(); - }); - } - - function isValidDate(value) { - // Invalid Date: getTime() returns NaN - return value && !(value.getTime && value.getTime() !== value.getTime()); - } - - function parseObservedDateValue(val) { - return isDefined(val) ? (isDate(val) ? val : parseDate(val)) : undefined; - } - }; -} - -function badInputChecker(scope, element, attr, ctrl) { - var node = element[0]; - var nativeValidation = ctrl.$$hasNativeValidators = isObject(node.validity); - if (nativeValidation) { - ctrl.$parsers.push(function(value) { - var validity = element.prop(VALIDITY_STATE_PROPERTY) || {}; - // Detect bug in FF35 for input[email] (https://bugzilla.mozilla.org/show_bug.cgi?id=1064430): - // - also sets validity.badInput (should only be validity.typeMismatch). - // - see http://www.whatwg.org/specs/web-apps/current-work/multipage/forms.html#e-mail-state-(type=email) - // - can ignore this case as we can still read out the erroneous email... - return validity.badInput && !validity.typeMismatch ? undefined : value; - }); - } -} - -function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) { - badInputChecker(scope, element, attr, ctrl); - baseInputType(scope, element, attr, ctrl, $sniffer, $browser); - - ctrl.$$parserName = 'number'; - ctrl.$parsers.push(function(value) { - if (ctrl.$isEmpty(value)) return null; - if (NUMBER_REGEXP.test(value)) return parseFloat(value); - return undefined; - }); - - ctrl.$formatters.push(function(value) { - if (!ctrl.$isEmpty(value)) { - if (!isNumber(value)) { - throw ngModelMinErr('numfmt', 'Expected `{0}` to be a number', value); - } - value = value.toString(); - } - return value; - }); - - if (isDefined(attr.min) || attr.ngMin) { - var minVal; - ctrl.$validators.min = function(value) { - return ctrl.$isEmpty(value) || isUndefined(minVal) || value >= minVal; - }; - - attr.$observe('min', function(val) { - if (isDefined(val) && !isNumber(val)) { - val = parseFloat(val, 10); - } - minVal = isNumber(val) && !isNaN(val) ? val : undefined; - // TODO(matsko): implement validateLater to reduce number of validations - ctrl.$validate(); - }); - } - - if (isDefined(attr.max) || attr.ngMax) { - var maxVal; - ctrl.$validators.max = function(value) { - return ctrl.$isEmpty(value) || isUndefined(maxVal) || value <= maxVal; - }; - - attr.$observe('max', function(val) { - if (isDefined(val) && !isNumber(val)) { - val = parseFloat(val, 10); - } - maxVal = isNumber(val) && !isNaN(val) ? val : undefined; - // TODO(matsko): implement validateLater to reduce number of validations - ctrl.$validate(); - }); - } -} - -function urlInputType(scope, element, attr, ctrl, $sniffer, $browser) { - // Note: no badInputChecker here by purpose as `url` is only a validation - // in browsers, i.e. we can always read out input.value even if it is not valid! - baseInputType(scope, element, attr, ctrl, $sniffer, $browser); - stringBasedInputType(ctrl); - - ctrl.$$parserName = 'url'; - ctrl.$validators.url = function(modelValue, viewValue) { - var value = modelValue || viewValue; - return ctrl.$isEmpty(value) || URL_REGEXP.test(value); - }; -} - -function emailInputType(scope, element, attr, ctrl, $sniffer, $browser) { - // Note: no badInputChecker here by purpose as `url` is only a validation - // in browsers, i.e. we can always read out input.value even if it is not valid! - baseInputType(scope, element, attr, ctrl, $sniffer, $browser); - stringBasedInputType(ctrl); - - ctrl.$$parserName = 'email'; - ctrl.$validators.email = function(modelValue, viewValue) { - var value = modelValue || viewValue; - return ctrl.$isEmpty(value) || EMAIL_REGEXP.test(value); - }; -} - -function radioInputType(scope, element, attr, ctrl) { - // make the name unique, if not defined - if (isUndefined(attr.name)) { - element.attr('name', nextUid()); - } - - var listener = function(ev) { - if (element[0].checked) { - ctrl.$setViewValue(attr.value, ev && ev.type); - } - }; - - element.on('click', listener); - - ctrl.$render = function() { - var value = attr.value; - element[0].checked = (value == ctrl.$viewValue); - }; - - attr.$observe('value', ctrl.$render); -} - -function parseConstantExpr($parse, context, name, expression, fallback) { - var parseFn; - if (isDefined(expression)) { - parseFn = $parse(expression); - if (!parseFn.constant) { - throw ngModelMinErr('constexpr', 'Expected constant expression for `{0}`, but saw ' + - '`{1}`.', name, expression); - } - return parseFn(context); - } - return fallback; -} - -function checkboxInputType(scope, element, attr, ctrl, $sniffer, $browser, $filter, $parse) { - var trueValue = parseConstantExpr($parse, scope, 'ngTrueValue', attr.ngTrueValue, true); - var falseValue = parseConstantExpr($parse, scope, 'ngFalseValue', attr.ngFalseValue, false); - - var listener = function(ev) { - ctrl.$setViewValue(element[0].checked, ev && ev.type); - }; - - element.on('click', listener); - - ctrl.$render = function() { - element[0].checked = ctrl.$viewValue; - }; - - // Override the standard `$isEmpty` because the $viewValue of an empty checkbox is always set to `false` - // This is because of the parser below, which compares the `$modelValue` with `trueValue` to convert - // it to a boolean. - ctrl.$isEmpty = function(value) { - return value === false; - }; - - ctrl.$formatters.push(function(value) { - return equals(value, trueValue); - }); - - ctrl.$parsers.push(function(value) { - return value ? trueValue : falseValue; - }); -} - - -/** - * @ngdoc directive - * @name textarea - * @restrict E - * - * @description - * HTML textarea element control with angular data-binding. The data-binding and validation - * properties of this element are exactly the same as those of the - * {@link ng.directive:input input element}. - * - * @param {string} ngModel Assignable angular expression to data-bind to. - * @param {string=} name Property name of the form under which the control is published. - * @param {string=} required Sets `required` validation error key if the value is not entered. - * @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to - * the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of - * `required` when you want to data-bind to the `required` attribute. - * @param {number=} ngMinlength Sets `minlength` validation error key if the value is shorter than - * minlength. - * @param {number=} ngMaxlength Sets `maxlength` validation error key if the value is longer than - * maxlength. Setting the attribute to a negative or non-numeric value, allows view values of any - * length. - * @param {string=} ngPattern Sets `pattern` validation error key if the ngModel value does not match - * a RegExp found by evaluating the Angular expression given in the attribute value. - * If the expression evaluates to a RegExp object, then this is used directly. - * If the expression evaluates to a string, then it will be converted to a RegExp - * after wrapping it in `^` and `$` characters. For instance, `"abc"` will be converted to - * `new RegExp('^abc$')`.
    - * **Note:** Avoid using the `g` flag on the RegExp, as it will cause each successive search to - * start at the index of the last search's match, thus not taking the whole input value into - * account. - * @param {string=} ngChange Angular expression to be executed when input changes due to user - * interaction with the input element. - * @param {boolean=} [ngTrim=true] If set to false Angular will not automatically trim the input. - */ - - -/** - * @ngdoc directive - * @name input - * @restrict E - * - * @description - * HTML input element control. When used together with {@link ngModel `ngModel`}, it provides data-binding, - * input state control, and validation. - * Input control follows HTML5 input types and polyfills the HTML5 validation behavior for older browsers. - * - *
    - * **Note:** Not every feature offered is available for all input types. - * Specifically, data binding and event handling via `ng-model` is unsupported for `input[file]`. - *
    - * - * @param {string} ngModel Assignable angular expression to data-bind to. - * @param {string=} name Property name of the form under which the control is published. - * @param {string=} required Sets `required` validation error key if the value is not entered. - * @param {boolean=} ngRequired Sets `required` attribute if set to true - * @param {number=} ngMinlength Sets `minlength` validation error key if the value is shorter than - * minlength. - * @param {number=} ngMaxlength Sets `maxlength` validation error key if the value is longer than - * maxlength. Setting the attribute to a negative or non-numeric value, allows view values of any - * length. - * @param {string=} ngPattern Sets `pattern` validation error key if the ngModel value does not match - * a RegExp found by evaluating the Angular expression given in the attribute value. - * If the expression evaluates to a RegExp object, then this is used directly. - * If the expression evaluates to a string, then it will be converted to a RegExp - * after wrapping it in `^` and `$` characters. For instance, `"abc"` will be converted to - * `new RegExp('^abc$')`.
    - * **Note:** Avoid using the `g` flag on the RegExp, as it will cause each successive search to - * start at the index of the last search's match, thus not taking the whole input value into - * account. - * @param {string=} ngChange Angular expression to be executed when input changes due to user - * interaction with the input element. - * @param {boolean=} [ngTrim=true] If set to false Angular will not automatically trim the input. - * This parameter is ignored for input[type=password] controls, which will never trim the - * input. - * - * @example - - - -
    -
    - -
    - - Required! -
    - -
    - - Too short! - - Too long! -
    -
    -
    - user = {{user}}
    - myForm.userName.$valid = {{myForm.userName.$valid}}
    - myForm.userName.$error = {{myForm.userName.$error}}
    - myForm.lastName.$valid = {{myForm.lastName.$valid}}
    - myForm.lastName.$error = {{myForm.lastName.$error}}
    - myForm.$valid = {{myForm.$valid}}
    - myForm.$error.required = {{!!myForm.$error.required}}
    - myForm.$error.minlength = {{!!myForm.$error.minlength}}
    - myForm.$error.maxlength = {{!!myForm.$error.maxlength}}
    -
    -
    - - var user = element(by.exactBinding('user')); - var userNameValid = element(by.binding('myForm.userName.$valid')); - var lastNameValid = element(by.binding('myForm.lastName.$valid')); - var lastNameError = element(by.binding('myForm.lastName.$error')); - var formValid = element(by.binding('myForm.$valid')); - var userNameInput = element(by.model('user.name')); - var userLastInput = element(by.model('user.last')); - - it('should initialize to model', function() { - expect(user.getText()).toContain('{"name":"guest","last":"visitor"}'); - expect(userNameValid.getText()).toContain('true'); - expect(formValid.getText()).toContain('true'); - }); - - it('should be invalid if empty when required', function() { - userNameInput.clear(); - userNameInput.sendKeys(''); - - expect(user.getText()).toContain('{"last":"visitor"}'); - expect(userNameValid.getText()).toContain('false'); - expect(formValid.getText()).toContain('false'); - }); - - it('should be valid if empty when min length is set', function() { - userLastInput.clear(); - userLastInput.sendKeys(''); - - expect(user.getText()).toContain('{"name":"guest","last":""}'); - expect(lastNameValid.getText()).toContain('true'); - expect(formValid.getText()).toContain('true'); - }); - - it('should be invalid if less than required min length', function() { - userLastInput.clear(); - userLastInput.sendKeys('xx'); - - expect(user.getText()).toContain('{"name":"guest"}'); - expect(lastNameValid.getText()).toContain('false'); - expect(lastNameError.getText()).toContain('minlength'); - expect(formValid.getText()).toContain('false'); - }); - - it('should be invalid if longer than max length', function() { - userLastInput.clear(); - userLastInput.sendKeys('some ridiculously long name'); - - expect(user.getText()).toContain('{"name":"guest"}'); - expect(lastNameValid.getText()).toContain('false'); - expect(lastNameError.getText()).toContain('maxlength'); - expect(formValid.getText()).toContain('false'); - }); - -
    - */ -var inputDirective = ['$browser', '$sniffer', '$filter', '$parse', - function($browser, $sniffer, $filter, $parse) { - return { - restrict: 'E', - require: ['?ngModel'], - link: { - pre: function(scope, element, attr, ctrls) { - if (ctrls[0]) { - (inputType[lowercase(attr.type)] || inputType.text)(scope, element, attr, ctrls[0], $sniffer, - $browser, $filter, $parse); - } - } - } - }; -}]; - - - -var CONSTANT_VALUE_REGEXP = /^(true|false|\d+)$/; -/** - * @ngdoc directive - * @name ngValue - * - * @description - * Binds the given expression to the value of `