From 2d3a7f4112509c9581d97c36a088715bd0aebca4 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20H=C3=B8ydahl?=
Date: Fri, 23 Mar 2018 17:34:50 +0100
Subject: [PATCH 01/88] SOLR-12121: JWT Token authentication plugin
---
lucene/ivy-versions.properties | 4 +-
solr/CHANGES.txt | 2 +
solr/NOTICE.txt | 18 +
solr/core/ivy.xml | 3 +
.../security/BearerAuthSchemeProvider.java | 93 ++++
.../apache/solr/security/JWTAuthPlugin.java | 445 ++++++++++++++++++
.../solr/security/PrincipalWithUserRoles.java | 95 ++++
.../solr/security/TokenCredentials.java | 43 ++
.../solr/security/VerifiedUserRoles.java | 33 ++
.../security/jwt_plugin_jwk_security.json | 11 +
.../security/jwt_plugin_jwk_url_security.json | 6 +
.../solr/security/JWTAuthPluginTest.java | 331 +++++++++++++
solr/licenses/jose4j-0.6.3.jar.sha1 | 1 +
solr/licenses/jose4j-LICENSE-ASL.txt | 272 +++++++++++
solr/licenses/jose4j-NOTICE.txt | 13 +
...hentication-and-authorization-plugins.adoc | 1 +
.../src/jwt-authentication-plugin.adoc | 87 ++++
17 files changed, 1457 insertions(+), 1 deletion(-)
create mode 100644 solr/core/src/java/org/apache/solr/security/BearerAuthSchemeProvider.java
create mode 100644 solr/core/src/java/org/apache/solr/security/JWTAuthPlugin.java
create mode 100644 solr/core/src/java/org/apache/solr/security/PrincipalWithUserRoles.java
create mode 100644 solr/core/src/java/org/apache/solr/security/TokenCredentials.java
create mode 100644 solr/core/src/java/org/apache/solr/security/VerifiedUserRoles.java
create mode 100644 solr/core/src/test-files/solr/security/jwt_plugin_jwk_security.json
create mode 100644 solr/core/src/test-files/solr/security/jwt_plugin_jwk_url_security.json
create mode 100644 solr/core/src/test/org/apache/solr/security/JWTAuthPluginTest.java
create mode 100644 solr/licenses/jose4j-0.6.3.jar.sha1
create mode 100644 solr/licenses/jose4j-LICENSE-ASL.txt
create mode 100644 solr/licenses/jose4j-NOTICE.txt
create mode 100644 solr/solr-ref-guide/src/jwt-authentication-plugin.adoc
diff --git a/lucene/ivy-versions.properties b/lucene/ivy-versions.properties
index afbab2041ff5..b4d070a29c4d 100644
--- a/lucene/ivy-versions.properties
+++ b/lucene/ivy-versions.properties
@@ -221,6 +221,8 @@ org.apache.uima.version = 2.3.1
/org.aspectj/aspectjrt = 1.8.0
+/org.bitbucket.b_c/jose4j = 0.6.3
+
org.bouncycastle.version = 1.54
/org.bouncycastle/bcmail-jdk15on = ${org.bouncycastle.version}
/org.bouncycastle/bcpkix-jdk15on = ${org.bouncycastle.version}
@@ -310,4 +312,4 @@ org.slf4j.version = 1.7.24
ua.net.nlp.morfologik-ukrainian-search.version = 3.9.0
/ua.net.nlp/morfologik-ukrainian-search = ${ua.net.nlp.morfologik-ukrainian-search.version}
-/xerces/xercesImpl = 2.9.1
+/xerces/xercesImpl = 2.9.1
\ No newline at end of file
diff --git a/solr/CHANGES.txt b/solr/CHANGES.txt
index 866c46fcceed..135b1c071d3a 100644
--- a/solr/CHANGES.txt
+++ b/solr/CHANGES.txt
@@ -61,6 +61,8 @@ New Features
* SOLR-12015: Add support "add-distinct" in AtomicURP so that we can use the 'add-distict' as a request parameter e.g:
atomic.=add-distict (Amrit Sarkar via noble)
+* SOLR-12121: JWT Token authentication plugin (janhoy)
+
Bug Fixes
----------------------
diff --git a/solr/NOTICE.txt b/solr/NOTICE.txt
index fd954f4ef4f6..62328d2c9641 100644
--- a/solr/NOTICE.txt
+++ b/solr/NOTICE.txt
@@ -537,3 +537,21 @@ See http://www.restlet.org/
Protocol Buffers - Google's data interchange format
Copyright 2008 Google Inc.
http://code.google.com/apis/protocolbuffers/
+
+=========================================================================
+== Jose4j Notice ==
+=========================================================================
+
+jose4j
+Copyright 2012-2015 Brian Campbell
+
+EcdsaUsingShaAlgorithm contains code for converting the concatenated
+R & S values of the signature to and from DER, which was originally
+derived from the Apache Santuario XML Security library's SignatureECDSA
+implementation. http://santuario.apache.org/
+
+The Base64 implementation in this software was derived from the
+Apache Commons Codec project. http://commons.apache.org/proper/commons-codec/
+
+JSON processing in this software was derived from the JSON.simple toolkit.
+https://code.google.com/p/json-simple/
\ No newline at end of file
diff --git a/solr/core/ivy.xml b/solr/core/ivy.xml
index ff4fa48679dd..fb7ad019392e 100644
--- a/solr/core/ivy.xml
+++ b/solr/core/ivy.xml
@@ -151,6 +151,9 @@
+
+
+
diff --git a/solr/core/src/java/org/apache/solr/security/BearerAuthSchemeProvider.java b/solr/core/src/java/org/apache/solr/security/BearerAuthSchemeProvider.java
new file mode 100644
index 000000000000..d64566b89ebc
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/security/BearerAuthSchemeProvider.java
@@ -0,0 +1,93 @@
+/*
+ * 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.
+ */
+package org.apache.solr.security;
+
+import org.apache.http.Header;
+import org.apache.http.HttpRequest;
+import org.apache.http.auth.AUTH;
+import org.apache.http.auth.AuthScheme;
+import org.apache.http.auth.AuthSchemeProvider;
+import org.apache.http.auth.AuthenticationException;
+import org.apache.http.auth.ContextAwareAuthScheme;
+import org.apache.http.auth.Credentials;
+import org.apache.http.auth.MalformedChallengeException;
+import org.apache.http.message.BufferedHeader;
+import org.apache.http.protocol.HttpContext;
+import org.apache.http.util.CharArrayBuffer;
+
+public class BearerAuthSchemeProvider implements AuthSchemeProvider {
+
+ /**
+ * Creates an instance of {@link AuthScheme}.
+ *
+ * @param context the http context
+ * @return auth scheme.
+ */
+ @Override
+ public AuthScheme create(HttpContext context) {
+ return new BearerAuthScheme();
+ }
+
+ private static class BearerAuthScheme implements ContextAwareAuthScheme {
+ private boolean complete = false;
+
+ @Override
+ public void processChallenge(Header header) throws MalformedChallengeException {
+ this.complete = true;
+ }
+
+ @Override
+ public Header authenticate(Credentials credentials, HttpRequest request) throws AuthenticationException {
+ return authenticate(credentials, request, null);
+ }
+
+ @Override
+ public Header authenticate(Credentials credentials, HttpRequest request, HttpContext httpContext)
+ throws AuthenticationException {
+ CharArrayBuffer buffer = new CharArrayBuffer(128);
+ buffer.append(AUTH.WWW_AUTH_RESP);
+ buffer.append(": Bearer ");
+ buffer.append(credentials.getUserPrincipal().getName());
+ return new BufferedHeader(buffer);
+ }
+
+ @Override
+ public String getSchemeName() {
+ return "Bearer";
+ }
+
+ @Override
+ public String getParameter(String name) {
+ return null;
+ }
+
+ @Override
+ public String getRealm() {
+ return null;
+ }
+
+ @Override
+ public boolean isConnectionBased() {
+ return false;
+ }
+
+ @Override
+ public boolean isComplete() {
+ return this.complete;
+ }
+ }
+}
\ No newline at end of file
diff --git a/solr/core/src/java/org/apache/solr/security/JWTAuthPlugin.java b/solr/core/src/java/org/apache/solr/security/JWTAuthPlugin.java
new file mode 100644
index 000000000000..6dd810a51ecc
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/security/JWTAuthPlugin.java
@@ -0,0 +1,445 @@
+/*
+ * 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.
+ */
+package org.apache.solr.security;
+
+import javax.servlet.FilterChain;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletRequestWrapper;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.lang.invoke.MethodHandles;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.security.Principal;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.StringTokenizer;
+import java.util.regex.Pattern;
+
+import org.apache.http.Header;
+import org.apache.http.HttpHeaders;
+import org.apache.http.auth.AuthSchemeProvider;
+import org.apache.http.auth.AuthScope;
+import org.apache.http.auth.BasicUserPrincipal;
+import org.apache.http.client.CredentialsProvider;
+import org.apache.http.config.Lookup;
+import org.apache.http.config.RegistryBuilder;
+import org.apache.http.impl.client.BasicCredentialsProvider;
+import org.apache.http.message.BasicHeader;
+import org.apache.solr.client.solrj.impl.SolrHttpClientBuilder;
+import org.apache.solr.common.SolrException;
+import org.apache.solr.common.SpecProvider;
+import org.apache.solr.common.util.Utils;
+import org.apache.solr.common.util.ValidatingJsonMap;
+import org.apache.solr.security.JWTAuthPlugin.AuthenticationResponse.AuthCode;
+import org.jose4j.jwa.AlgorithmConstraints;
+import org.jose4j.jwk.HttpsJwks;
+import org.jose4j.jwk.JsonWebKey;
+import org.jose4j.jwk.JsonWebKeySet;
+import org.jose4j.jwt.JwtClaims;
+import org.jose4j.jwt.MalformedClaimException;
+import org.jose4j.jwt.consumer.InvalidJwtException;
+import org.jose4j.jwt.consumer.JwtConsumer;
+import org.jose4j.jwt.consumer.JwtConsumerBuilder;
+import org.jose4j.keys.resolvers.HttpsJwksVerificationKeyResolver;
+import org.jose4j.keys.resolvers.JwksVerificationKeyResolver;
+import org.jose4j.keys.resolvers.VerificationKeyResolver;
+import org.jose4j.lang.JoseException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Authenticaion plugin that finds logged in user by validating the signature of a JWT token
+ */
+public class JWTAuthPlugin extends AuthenticationPlugin implements HttpClientBuilderPlugin, SpecProvider {
+ private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+ private final static ThreadLocal authHeader = new ThreadLocal<>();
+ private static final String PARAM_BLOCK_UNKNOWN = "block_unknown";
+ private static final String PARAM_JWK_URL = "jwk_url";
+ private static final String PARAM_JWK = "jwk";
+ private static final String PARAM_ISSUER = "iss";
+ private static final String PARAM_AUDIENCE = "aud";
+ private static final String PARAM_REQUIRE_SUBJECT = "require_sub";
+ private static final String PARAM_PRINCIPAL_CLAIM = "principal_claim";
+ private static final String PARAM_ROLES_CLAIM = "roles_claim";
+ private static final String PARAM_REQUIRE_EXPIRATIONTIME = "require_exp";
+ private static final String PARAM_ALG_WHITELIST = "alg_whitelist";
+ private static final String PARAM_JWK_CACHE_DURATION = "jwk_cache_dur";
+ private static final String PARAM_CLAIMS_MATCH = "claims_match";
+ private static final String AUTH_REALM = "solr";
+
+ private String jwk_url;
+ private Map jwk;
+ private JwtConsumer jwtConsumer;
+ private String iss;
+ private String aud;
+ private boolean requireSubject;
+ private boolean requireExpirationTime;
+ private List algWhitelist;
+ private HttpsJwks httpsJkws;
+ private long jwkCacheDuration;
+ VerificationKeyResolver verificationKeyResolver;
+ private JsonWebKeySet jwks;
+ private String principalClaim;
+ private String rolesClaim;
+ private Map claimsMatch;
+ private HashMap claimsMatchCompiled;
+ private boolean blockUnknown;
+
+ @Override
+ public void init(Map pluginConfig) {
+ blockUnknown = Boolean.parseBoolean(String.valueOf(pluginConfig.getOrDefault(PARAM_BLOCK_UNKNOWN, false)));
+ jwk_url = (String) pluginConfig.get(PARAM_JWK_URL);
+ jwk = (Map) pluginConfig.get(PARAM_JWK);
+ iss = (String) pluginConfig.get(PARAM_ISSUER);
+ aud = (String) pluginConfig.get(PARAM_AUDIENCE);
+ requireSubject = Boolean.parseBoolean(String.valueOf(pluginConfig.getOrDefault(PARAM_REQUIRE_SUBJECT, "true")));
+ requireExpirationTime = Boolean.parseBoolean(String.valueOf(pluginConfig.getOrDefault(PARAM_REQUIRE_EXPIRATIONTIME, "true")));
+ algWhitelist = (List) pluginConfig.get(PARAM_ALG_WHITELIST);
+ jwkCacheDuration = Long.parseLong((String) pluginConfig.getOrDefault(PARAM_JWK_CACHE_DURATION, "3600"));
+ principalClaim = (String) pluginConfig.getOrDefault(PARAM_PRINCIPAL_CLAIM, "sub");
+ rolesClaim = (String) pluginConfig.get(PARAM_ROLES_CLAIM);
+ claimsMatch = (Map) pluginConfig.get(PARAM_CLAIMS_MATCH);
+ claimsMatchCompiled = new HashMap<>();
+ if (claimsMatch != null) {
+ for (Map.Entry entry : claimsMatch.entrySet()) {
+ claimsMatchCompiled.put(entry.getKey(), Pattern.compile(entry.getValue()));
+ }
+ }
+
+ jwtConsumer = null;
+ if (jwk_url != null) {
+ // The HttpsJwks retrieves and caches keys from a the given HTTPS JWKS endpoint.
+ try {
+ URL jwkUrl = new URL(jwk_url);
+ if (!"https".equalsIgnoreCase(jwkUrl.getProtocol())) {
+ throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "jwk_url must be an HTTPS url");
+ }
+ } catch (MalformedURLException e) {
+ throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "jwk_url must be a valid https URL");
+ }
+ httpsJkws = new HttpsJwks(jwk_url);
+ httpsJkws.setDefaultCacheDuration(jwkCacheDuration);
+ verificationKeyResolver = new HttpsJwksVerificationKeyResolver(httpsJkws);
+ initConsumer();
+ } else if (jwk != null) {
+ try {
+ jwks = parseJwkSet(jwk);
+ verificationKeyResolver = new JwksVerificationKeyResolver(jwks.getJsonWebKeys());
+ initConsumer();
+ } catch (JoseException e) {
+ throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "Invalid JWTAuthPlugin configuration, jwk field parse error", e);
+ }
+ } else {
+ log.warn("JWTAuthPlugin needs to specify either 'jwk' or 'jwk_url' parameters.");
+ }
+ }
+
+ protected JsonWebKeySet parseJwkSet(Map jwkObj) throws JoseException {
+ JsonWebKeySet webKeySet = new JsonWebKeySet();
+ if (jwkObj.containsKey("keys")) {
+ List
+ */
+ public static class JWTPrincipalWithUserRoles extends JWTPrincipal implements VerifiedUserRoles {
+ private final Set roles;
+
+ public JWTPrincipalWithUserRoles(final String username, String token, Map claims, Set roles) {
+ super(username, token, claims);
+ Args.notNull(roles, "User roles");
+ this.roles = roles;
+ }
+
+ /**
+ * Gets the list of roles
+ */
+ @Override
+ public Set getVerifiedRoles() {
+ return roles;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (!(o instanceof JWTPrincipalWithUserRoles))
+ return false;
+ JWTPrincipalWithUserRoles that = (JWTPrincipalWithUserRoles) o;
+ return super.equals(o) && roles.equals(that.roles);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(username, token, claims, roles);
+ }
+
+ @Override
+ public String toString() {
+ return "JWTPrincipalWithUserRoles{" +
+ "username='" + username + '\'' +
+ ", token='" + token + '\'' +
+ ", claims=" + claims +
+ ", roles=" + roles +
+ '}';
+ }
+ }
+
+ // The interceptor class that adds correct header or delegates to PKI
+ private class PkiDelegationInterceptor implements HttpRequestInterceptor {
+ @Override
+ public void process(HttpRequest request, HttpContext context) throws HttpException, IOException {
+ if (context instanceof HttpClientContext) {
+ HttpClientContext httpClientContext = (HttpClientContext) context;
+ if (httpClientContext.getUserToken() instanceof JWTPrincipal) {
+ JWTPrincipal jwtPrincipal = (JWTPrincipal) httpClientContext.getUserToken();
+ request.setHeader(HttpHeaders.AUTHORIZATION, "Bearer " + jwtPrincipal.token);
+ log.debug("Set JWT header on inter-node request");
+ return;
+ }
+ }
+
+ if (coreContainer.getPkiAuthenticationPlugin() != null) {
+ log.debug("Inter-node request delegated from JWTAuthPlugin to PKIAuthenticationPlugin");
+ coreContainer.getPkiAuthenticationPlugin().setHeader(request);
+ } else {
+ throw new SolrException(SolrException.ErrorCode.SERVER_ERROR,
+ "JWTAuthPlugin wants to delegate inter-node request to PKI, but PKI plugin was not initialized");
+ }
+ }
+ }
}
diff --git a/solr/core/src/java/org/apache/solr/security/PKIAuthenticationPlugin.java b/solr/core/src/java/org/apache/solr/security/PKIAuthenticationPlugin.java
index 43dac480168c..c0c0857839fa 100644
--- a/solr/core/src/java/org/apache/solr/security/PKIAuthenticationPlugin.java
+++ b/solr/core/src/java/org/apache/solr/security/PKIAuthenticationPlugin.java
@@ -244,7 +244,7 @@ public void process(HttpRequest httpRequest, HttpContext httpContext) throws Htt
}
@SuppressForbidden(reason = "Needs currentTimeMillis to set current time in header")
- void setHeader(HttpRequest httpRequest) {
+ protected void setHeader(HttpRequest httpRequest) {
SolrRequestInfo reqInfo = getRequestInfo();
String usr;
if (reqInfo != null) {
diff --git a/solr/core/src/java/org/apache/solr/security/PrincipalWithUserRoles.java b/solr/core/src/java/org/apache/solr/security/PrincipalWithUserRoles.java
deleted file mode 100644
index 6d685a8f1bf2..000000000000
--- a/solr/core/src/java/org/apache/solr/security/PrincipalWithUserRoles.java
+++ /dev/null
@@ -1,95 +0,0 @@
-/*
- * 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.
- */
-package org.apache.solr.security;
-
-import java.io.Serializable;
-import java.security.Principal;
-import java.util.Set;
-
-import org.apache.http.util.Args;
-
-/**
- * Type of Principal object that can contain also a list of roles the user has.
- * One use case can be to keep track of user-role mappings in an Identity Server
- * external to Solr and pass the information to Solr in a signed JWT token or in
- * another secure manner. The role information can then be used to authorize
- * requests without the need to maintain or lookup what roles each user belongs to.
- */
-public class PrincipalWithUserRoles implements Principal, VerifiedUserRoles, Serializable {
- private static final long serialVersionUID = 4144666467522831388L;
- private final String username;
-
- private final Set roles;
-
- /**
- * User principal with user name as well as one or more roles that he/she belong to
- * @param username string with user name for user
- * @param roles a set of roles that we know this user belongs to, or empty list for no roles
- */
- public PrincipalWithUserRoles(final String username, Set roles) {
- super();
- Args.notNull(username, "User name");
- Args.notNull(roles, "User roles");
- this.username = username;
- this.roles = roles;
- }
-
- /**
- * Returns the name of this principal.
- *
- * @return the name of this principal.
- */
- @Override
- public String getName() {
- return this.username;
- }
-
- /**
- * Gets the list of roles
- */
- @Override
- public Set getVerifiedRoles() {
- return roles;
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
-
- PrincipalWithUserRoles that = (PrincipalWithUserRoles) o;
-
- if (!username.equals(that.username)) return false;
- return roles.equals(that.roles);
- }
-
- @Override
- public int hashCode() {
- int result = username.hashCode();
- result = 31 * result + roles.hashCode();
- return result;
- }
-
- @Override
- public String toString() {
- return "PrincipalWithUserRoles{" +
- "username='" + username + '\'' +
- ", roles=" + roles +
- '}';
- }
-}
-
diff --git a/solr/core/src/java/org/apache/solr/security/TokenCredentials.java b/solr/core/src/java/org/apache/solr/security/TokenCredentials.java
deleted file mode 100644
index 6433fd622d2f..000000000000
--- a/solr/core/src/java/org/apache/solr/security/TokenCredentials.java
+++ /dev/null
@@ -1,43 +0,0 @@
-/*
- * 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.
- */
-package org.apache.solr.security;
-
-import java.security.Principal;
-
-import org.apache.http.auth.BasicUserPrincipal;
-import org.apache.http.auth.Credentials;
-
-/**
- * Credentials implementation that holds on to a (JWT) token for later use in inter-node requests
- */
-public class TokenCredentials implements Credentials {
- private Principal userPrincipal;
-
- public TokenCredentials(String token) {
- this.userPrincipal = new BasicUserPrincipal(token);
- }
-
- @Override
- public Principal getUserPrincipal() {
- return userPrincipal;
- }
-
- @Override
- public String getPassword() {
- return null;
- }
-}
\ No newline at end of file
diff --git a/solr/core/src/test/org/apache/solr/security/JWTAuthPluginIntegrationTest.java b/solr/core/src/test/org/apache/solr/security/JWTAuthPluginIntegrationTest.java
index 6a2b9f1286bb..3fd3641bdd17 100644
--- a/solr/core/src/test/org/apache/solr/security/JWTAuthPluginIntegrationTest.java
+++ b/solr/core/src/test/org/apache/solr/security/JWTAuthPluginIntegrationTest.java
@@ -16,17 +16,16 @@
*/
package org.apache.solr.security;
-import org.apache.http.Header;
-import org.apache.http.HttpHeaders;
-import org.apache.http.message.BasicHeader;
-import org.apache.solr.client.solrj.SolrQuery;
-import org.apache.solr.client.solrj.impl.CloudSolrClient;
-import org.apache.solr.client.solrj.impl.HttpSolrClient;
-import org.apache.solr.client.solrj.request.CollectionAdminRequest;
-import org.apache.solr.client.solrj.response.QueryResponse;
-import org.apache.solr.cloud.AbstractDistribZkTestBase;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.util.stream.Collectors;
+
import org.apache.solr.cloud.SolrCloudTestCase;
-import org.apache.solr.common.SolrInputDocument;
+import org.apache.solr.common.util.Pair;
import org.jose4j.jwk.PublicJsonWebKey;
import org.jose4j.jwk.RsaJsonWebKey;
import org.jose4j.jws.AlgorithmIdentifiers;
@@ -40,26 +39,29 @@ public class JWTAuthPluginIntegrationTest extends SolrCloudTestCase {
protected static final int NUM_SERVERS = 2;
protected static final int NUM_SHARDS = 2;
protected static final int REPLICATION_FACTOR = 1;
- private static Header tokenHeader;
- private static String jwkJSON = "{\n" +
- " \"kty\": \"RSA\",\n" +
- " \"d\": \"i6pyv2z3o-MlYytWsOr3IE1olu2RXZBzjPRBNgWAP1TlLNaphHEvH5aHhe_CtBAastgFFMuP29CFhaL3_tGczkvWJkSveZQN2AHWHgRShKgoSVMspkhOt3Ghha4CvpnZ9BnQzVHnaBnHDTTTfVgXz7P1ZNBhQY4URG61DKIF-JSSClyh1xKuMoJX0lILXDYGGcjVTZL_hci4IXPPTpOJHV51-pxuO7WU5M9252UYoiYyCJ56ai8N49aKIMsqhdGuO4aWUwsGIW4oQpjtce5eEojCprYl-9rDhTwLAFoBtjy6LvkqlR2Ae5dKZYpStljBjK8PJrBvWZjXAEMDdQ8PuQ\",\n" +
- " \"e\": \"AQAB\",\n" +
- " \"use\": \"sig\",\n" +
- " \"kid\": \"test\",\n" +
- " \"alg\": \"RS256\",\n" +
- " \"n\": \"jeyrvOaZrmKWjyNXt0myAc_pJ1hNt3aRupExJEx1ewPaL9J9HFgSCjMrYxCB1ETO1NDyZ3nSgjZis-jHHDqBxBjRdq_t1E2rkGFaYbxAyKt220Pwgme_SFTB9MXVrFQGkKyjmQeVmOmV6zM3KK8uMdKQJ4aoKmwBcF5Zg7EZdDcKOFgpgva1Jq-FlEsaJ2xrYDYo3KnGcOHIt9_0NQeLsqZbeWYLxYni7uROFncXYV5FhSJCeR4A_rrbwlaCydGxE0ToC_9HNYibUHlkJjqyUhAgORCbNS8JLCJH8NUi5sDdIawK9GTSyvsJXZ-QHqo4cMUuxWV5AJtaRGghuMUfqQ\"\n" +
- "}";
- private static PublicJsonWebKey jwk;
-
+ private static String jwtTestToken;
+ private static String baseUrl;
+
@BeforeClass
public static void setupClass() throws Exception {
configureCluster(NUM_SERVERS)// nodes
.withSecurityJson(TEST_PATH().resolve("security").resolve("jwt_plugin_jwk_security.json"))
.addConfig("conf1", TEST_PATH().resolve("configsets").resolve("cloud-minimal").resolve("conf"))
.configure();
+ String hostport = cluster.getSolrClient().getClusterStateProvider().getLiveNodes().iterator().next().split("_")[0];
+ baseUrl = "http://" + hostport + "/solr/";
+
+ String jwkJSON = "{\n" +
+ " \"kty\": \"RSA\",\n" +
+ " \"d\": \"i6pyv2z3o-MlYytWsOr3IE1olu2RXZBzjPRBNgWAP1TlLNaphHEvH5aHhe_CtBAastgFFMuP29CFhaL3_tGczkvWJkSveZQN2AHWHgRShKgoSVMspkhOt3Ghha4CvpnZ9BnQzVHnaBnHDTTTfVgXz7P1ZNBhQY4URG61DKIF-JSSClyh1xKuMoJX0lILXDYGGcjVTZL_hci4IXPPTpOJHV51-pxuO7WU5M9252UYoiYyCJ56ai8N49aKIMsqhdGuO4aWUwsGIW4oQpjtce5eEojCprYl-9rDhTwLAFoBtjy6LvkqlR2Ae5dKZYpStljBjK8PJrBvWZjXAEMDdQ8PuQ\",\n" +
+ " \"e\": \"AQAB\",\n" +
+ " \"use\": \"sig\",\n" +
+ " \"kid\": \"test\",\n" +
+ " \"alg\": \"RS256\",\n" +
+ " \"n\": \"jeyrvOaZrmKWjyNXt0myAc_pJ1hNt3aRupExJEx1ewPaL9J9HFgSCjMrYxCB1ETO1NDyZ3nSgjZis-jHHDqBxBjRdq_t1E2rkGFaYbxAyKt220Pwgme_SFTB9MXVrFQGkKyjmQeVmOmV6zM3KK8uMdKQJ4aoKmwBcF5Zg7EZdDcKOFgpgva1Jq-FlEsaJ2xrYDYo3KnGcOHIt9_0NQeLsqZbeWYLxYni7uROFncXYV5FhSJCeR4A_rrbwlaCydGxE0ToC_9HNYibUHlkJjqyUhAgORCbNS8JLCJH8NUi5sDdIawK9GTSyvsJXZ-QHqo4cMUuxWV5AJtaRGghuMUfqQ\"\n" +
+ "}";
- jwk = RsaJsonWebKey.Factory.newPublicJwk(jwkJSON);
+ PublicJsonWebKey jwk = RsaJsonWebKey.Factory.newPublicJwk(jwkJSON);
JwtClaims claims = JWTAuthPluginTest.generateClaims();
JsonWebSignature jws = new JsonWebSignature();
jws.setPayload(claims.toJson());
@@ -67,56 +69,127 @@ public static void setupClass() throws Exception {
jws.setKeyIdHeaderValue(jwk.getKeyId());
jws.setAlgorithmHeaderValue(AlgorithmIdentifiers.RSA_USING_SHA256);
- String testJwt = jws.getCompactSerialization();
- tokenHeader = new BasicHeader(HttpHeaders.AUTHORIZATION, "Bearer " + testJwt);
+ jwtTestToken = jws.getCompactSerialization();
}
@AfterClass
public static void tearDownClass() throws Exception {
System.clearProperty("java.security.auth.login.config");
+ shutdownCluster();
}
- @Test(expected = HttpSolrClient.RemoteSolrException.class)
- public void testWithoutToken() throws Exception {
- testCollectionCreateSearchDelete();
- // sometimes run a second test e.g. to test collection create-delete-create scenario
- if (random().nextBoolean()) testCollectionCreateSearchDelete();
+ @Test(expected = IOException.class)
+ public void infoRequestWithoutToken() throws Exception {
+ get(baseUrl + "admin/info/system", null);
}
@Test
- public void testWithToken() throws Exception {
- enableToken();
- testCollectionCreateSearchDelete();
- // sometimes run a second test e.g. to test collection create-delete-create scenario
- if (random().nextBoolean()) testCollectionCreateSearchDelete();
+ public void infoRequestWithToken() throws IOException {
+ Pair result = get(baseUrl + "admin/info/system", jwtTestToken);
+ assertEquals(Integer.valueOf(200), result.second());
}
- private void enableToken() {
+ @Test
+ public void createCollectionAndQueryDistributed() throws Exception {
+ // Admin request will use PKI inter-node auth from Overseer, and succeed
+ assertEquals(200, get(baseUrl + "admin/collections?action=CREATE&name=mycoll&numShards=2", jwtTestToken).second().intValue());
+ // Now do a distributed query, using JWTAUth for inter-node
+ Pair result = get(baseUrl + "mycoll/query?q=*:*", jwtTestToken);
+ assertEquals(Integer.valueOf(200), result.second());
}
- protected void testCollectionCreateSearchDelete() throws Exception {
- CloudSolrClient solrClient = cluster.getSolrClient();
- String collectionName = "jwtAuthTestColl";
+
+
+// NOCOMMIT: Test using SolrJ as client
+// private void testCollectionCreateSearchDelete(boolean enableJwt) throws Exception {
+// if (enableJwt) {
+// HttpClientUtil.setHttpClientBuilder(getHttpClientBuilder(HttpClientUtil.getHttpClientBuilder(), testJwt));
+// } else {
+// HttpClientUtil.resetHttpClientBuilder();
+// }
+
+// ClusterStateProvider csProv = cluster.getSolrClient().getClusterStateProvider();
+// CloudSolrClientBuilder builder = new CloudSolrClientBuilder(csProv);
+// CloudSolrClient solrClient = builder.build();
+
+// CloudSolrClient solrClient = cluster.getSolrClient();
+
+// URL solrurl = new URL(baseUrl + "admin/info/system");
+// HttpURLConnection conn = (HttpURLConnection) solrurl.openConnection();
+// conn.setRequestProperty("Authorization", "Bearer " + testJwt);
+// conn.connect();
+// BufferedReader br = new BufferedReader(new InputStreamReader((InputStream) conn.getContent()));
+// br.lines().forEach(l -> {
+// System.out.println(l);
+// });
+// conn.disconnect();
+
+
+// Pair result;
+//
+// result = get(baseUrl + "admin/collections?action=CREATE&name=mycoll&numShards=2");
+// System.out.println(result.first());
+// assertEquals(Integer.valueOf(200), result.second());
+//
+// result = get(baseUrl + "mycoll/query?q=*:*");
+// System.out.println(result.first());
+// assertEquals(Integer.valueOf(200), result.second());
+
+
+// String collectionName = "jwtAuthTestColl";
// create collection
- CollectionAdminRequest.Create create = CollectionAdminRequest.createCollection(collectionName, "conf1",
- NUM_SHARDS, REPLICATION_FACTOR);
- create.process(solrClient);
-
- solrClient.add(collectionName, new SolrInputDocument("id", "1"));
- solrClient.add(collectionName, new SolrInputDocument("id", "2"));
- solrClient.commit(collectionName);
-
- SolrQuery query = new SolrQuery();
- query.setQuery("*:*");
- QueryResponse rsp = solrClient.query(collectionName, query);
- assertEquals(2, rsp.getResults().getNumFound());
-
- CollectionAdminRequest.Delete deleteReq = CollectionAdminRequest.deleteCollection(collectionName);
- deleteReq.process(solrClient);
- AbstractDistribZkTestBase.waitForCollectionToDisappear(collectionName,
- solrClient.getZkStateReader(), true, true, 330);
+// CollectionAdminRequest.Create create = CollectionAdminRequest.createCollection(collectionName, "conf1",
+// NUM_SHARDS, REPLICATION_FACTOR);
+// create.process(solrClient);
+//
+// solrClient.add(collectionName, new SolrInputDocument("id", "1"));
+// solrClient.add(collectionName, new SolrInputDocument("id", "2"));
+// solrClient.commit(collectionName);
+//
+// SolrQuery query = new SolrQuery();
+// query.setQuery("*:*");
+// QueryResponse rsp = solrClient.query(collectionName, query);
+// assertEquals(2, rsp.getResults().getNumFound());
+//
+// CollectionAdminRequest.Delete deleteReq = CollectionAdminRequest.deleteCollection(collectionName);
+// deleteReq.process(solrClient);
+// AbstractDistribZkTestBase.waitForCollectionToDisappear(collectionName,
+// solrClient.getZkStateReader(), true, true, 330);
+// solrClient.close();
+// }
+
+ private Pair get(String url, String token) throws IOException {
+ URL createUrl = new URL(url);
+ HttpURLConnection createConn = (HttpURLConnection) createUrl.openConnection();
+ if (token != null)
+ createConn.setRequestProperty("Authorization", "Bearer " + token);
+ createConn.connect();
+ BufferedReader br2 = new BufferedReader(new InputStreamReader((InputStream) createConn.getContent()));
+ String result = br2.lines().collect(Collectors.joining("\n"));
+ int code = createConn.getResponseCode();
+ createConn.disconnect();
+ return new Pair<>(result, code);
}
+// SolrHttpClientBuilder getHttpClientBuilder(SolrHttpClientBuilder builder, String token) {
+// if (builder == null) {
+// builder = SolrHttpClientBuilder.create();
+// }
+// builder.setAuthSchemeRegistryProvider(() -> {
+// Lookup authProviders = RegistryBuilder.create()
+// .register("Bearer", new JWTAuthPlugin.JwtBearerAuthschemeProvider())
+// .build();
+// return authProviders;
+// });
+// builder.setDefaultCredentialsProvider(() -> {
+// CredentialsProvider credentialsProvider = new BasicCredentialsProvider();
+// credentialsProvider.setCredentials(AuthScope.ANY, new UsernamePasswordCredentials(token, null));
+// return credentialsProvider;
+// });
+// return builder;
+// }
+
+
}
diff --git a/solr/core/src/test/org/apache/solr/security/JWTAuthPluginTest.java b/solr/core/src/test/org/apache/solr/security/JWTAuthPluginTest.java
index f9529260ca78..725855176d62 100644
--- a/solr/core/src/test/org/apache/solr/security/JWTAuthPluginTest.java
+++ b/solr/core/src/test/org/apache/solr/security/JWTAuthPluginTest.java
@@ -46,12 +46,10 @@
public class JWTAuthPluginTest extends SolrTestCaseJ4 {
private static String testHeader;
- private static String slimJwt;
private static String slimHeader;
private JWTAuthPlugin plugin;
private HashMap testJwk;
private static RsaJsonWebKey rsaJsonWebKey;
- private static String testJwt;
private HashMap testConfig;
@@ -68,21 +66,18 @@ public static void beforeAll() throws Exception {
jws.setKeyIdHeaderValue(rsaJsonWebKey.getKeyId());
jws.setAlgorithmHeaderValue(AlgorithmIdentifiers.RSA_USING_SHA256);
- testJwt = jws.getCompactSerialization();
+ String testJwt = jws.getCompactSerialization();
testHeader = "Bearer" + " " + testJwt;
-
- System.out.println("Header:\n" + testHeader);
- System.out.println("JWK:\n" + rsaJsonWebKey.toJson());
claims.unsetClaim("iss");
claims.unsetClaim("aud");
claims.unsetClaim("exp");
jws.setPayload(claims.toJson());
- slimJwt = jws.getCompactSerialization();
+ String slimJwt = jws.getCompactSerialization();
slimHeader = "Bearer" + " " + slimJwt;
}
- protected static JwtClaims generateClaims() {
+ static JwtClaims generateClaims() {
JwtClaims claims = new JwtClaims();
claims.setIssuer("IDServer"); // who creates the token and signs it
claims.setAudience("Solr"); // to whom the token is intended to be sent
@@ -105,7 +100,7 @@ protected static JwtClaims generateClaims() {
public void setUp() throws Exception {
super.setUp();
// Create an auth plugin
- plugin = new JWTAuthPlugin();
+ plugin = new JWTAuthPlugin(null);
// Create a JWK config for security.json
testJwk = new HashMap<>();
@@ -130,9 +125,8 @@ public void tearDown() throws Exception {
}
@Test
- public void initWithoutRequired() throws Exception {
- HashMap authConf = new HashMap();
- plugin = new JWTAuthPlugin();
+ public void initWithoutRequired() {
+ HashMap authConf = new HashMap<>();
plugin.init(authConf);
assertEquals(AUTZ_HEADER_PROBLEM, plugin.authenticate("foo").getAuthCode());
}
@@ -160,18 +154,18 @@ public void initFromSecurityJSONUrlJwk() throws Exception {
}
@Test
- public void initWithJwk() throws Exception {
- HashMap authConf = new HashMap();
+ public void initWithJwk() {
+ HashMap authConf = new HashMap<>();
authConf.put("jwk", testJwk);
- plugin = new JWTAuthPlugin();
+ plugin = new JWTAuthPlugin(null);
plugin.init(authConf);
}
@Test
- public void initWithJwkUrl() throws Exception {
- HashMap authConf = new HashMap();
+ public void initWithJwkUrl() {
+ HashMap authConf = new HashMap<>();
authConf.put("jwk_url", "https://127.0.0.1:9999/foo.jwk");
- plugin = new JWTAuthPlugin();
+ plugin = new JWTAuthPlugin(null);
plugin.init(authConf);
}
@@ -187,14 +181,14 @@ public void parseJwkSet() throws Exception {
}
@Test
- public void authenticateOk() throws Exception {
+ public void authenticateOk() {
AuthenticationResponse resp = plugin.authenticate(testHeader);
assertTrue(resp.isAuthenticated());
assertEquals("solruser", resp.getPrincipal().getName());
}
@Test
- public void authFailedMissingSubject() throws Exception {
+ public void authFailedMissingSubject() {
testConfig.put("iss", "NA");
plugin.init(testConfig);
AuthenticationResponse resp = plugin.authenticate(testHeader);
@@ -208,7 +202,7 @@ public void authFailedMissingSubject() throws Exception {
}
@Test
- public void authFailedMissingAudience() throws Exception {
+ public void authFailedMissingAudience() {
testConfig.put("aud", "NA");
plugin.init(testConfig);
AuthenticationResponse resp = plugin.authenticate(testHeader);
@@ -222,7 +216,7 @@ public void authFailedMissingAudience() throws Exception {
}
@Test
- public void authFailedMissingPrincipal() throws Exception {
+ public void authFailedMissingPrincipal() {
testConfig.put("principal_claim", "customPrincipal");
plugin.init(testConfig);
AuthenticationResponse resp = plugin.authenticate(testHeader);
@@ -236,7 +230,7 @@ public void authFailedMissingPrincipal() throws Exception {
}
@Test
- public void claimMatch() throws Exception {
+ public void claimMatch() {
// all custom claims match regex
Map shouldMatch = new HashMap<>();
shouldMatch.put("claim1", "foo");
@@ -262,7 +256,7 @@ public void claimMatch() throws Exception {
}
@Test
- public void missingIssAudExp() throws Exception {
+ public void missingIssAudExp() {
testConfig.put("require_exp", "false");
testConfig.put("require_sub", "false");
plugin.init(testConfig);
@@ -283,7 +277,7 @@ public void missingIssAudExp() throws Exception {
}
@Test
- public void algWhitelist() throws Exception {
+ public void algWhitelist() {
testConfig.put("alg_whitelist", Arrays.asList("PS384", "PS512"));
plugin.init(testConfig);
AuthenticationResponse resp = plugin.authenticate(testHeader);
@@ -292,7 +286,7 @@ public void algWhitelist() throws Exception {
}
@Test
- public void roles() throws Exception {
+ public void roles() {
testConfig.put("roles_claim", "groups");
plugin.init(testConfig);
AuthenticationResponse resp = plugin.authenticate(testHeader);
@@ -312,7 +306,7 @@ public void roles() throws Exception {
}
@Test
- public void noHeaderBlockUnknown() throws Exception {
+ public void noHeaderBlockUnknown() {
testConfig.put("block_unknown", true);
plugin.init(testConfig);
AuthenticationResponse resp = plugin.authenticate(null);
@@ -320,12 +314,10 @@ public void noHeaderBlockUnknown() throws Exception {
}
@Test
- public void noHeaderNotBlockUnknown() throws Exception {
+ public void noHeaderNotBlockUnknown() {
testConfig.put("block_unknown", false);
plugin.init(testConfig);
AuthenticationResponse resp = plugin.authenticate(null);
assertEquals(AuthenticationResponse.AuthCode.PASS_THROUGH, resp.getAuthCode());
}
-
- // TODO: Add inter-node request tests
}
\ No newline at end of file
diff --git a/solr/solrj/src/java/org/apache/solr/client/solrj/SolrRequest.java b/solr/solrj/src/java/org/apache/solr/client/solrj/SolrRequest.java
index 7dbaab90915c..04b94f7aae84 100644
--- a/solr/solrj/src/java/org/apache/solr/client/solrj/SolrRequest.java
+++ b/solr/solrj/src/java/org/apache/solr/client/solrj/SolrRequest.java
@@ -18,6 +18,7 @@
import java.io.IOException;
import java.io.Serializable;
+import java.security.Principal;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
@@ -36,6 +37,16 @@
* @since solr 1.3
*/
public abstract class SolrRequest implements Serializable {
+ // This user principal is typically used by Auth plugins during distributed/sharded search
+ private Principal userPrincipal;
+
+ public void setUserPrincipal(Principal userPrincipal) {
+ this.userPrincipal = userPrincipal;
+ }
+
+ public Principal getUserPrincipal() {
+ return userPrincipal;
+ }
public enum METHOD {
GET,
diff --git a/solr/solrj/src/java/org/apache/solr/client/solrj/impl/HttpSolrClient.java b/solr/solrj/src/java/org/apache/solr/client/solrj/impl/HttpSolrClient.java
index 2b60e337e4a1..057122eb5fb3 100644
--- a/solr/solrj/src/java/org/apache/solr/client/solrj/impl/HttpSolrClient.java
+++ b/solr/solrj/src/java/org/apache/solr/client/solrj/impl/HttpSolrClient.java
@@ -24,6 +24,7 @@
import java.net.ConnectException;
import java.net.SocketTimeoutException;
import java.nio.charset.StandardCharsets;
+import java.security.Principal;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
@@ -252,7 +253,7 @@ public NamedList request(final SolrRequest request, final ResponseParser
throws SolrServerException, IOException {
HttpRequestBase method = createMethod(request, collection);
setBasicAuthHeader(request, method);
- return executeMethod(method, processor, isV2ApiRequest(request));
+ return executeMethod(method, request.getUserPrincipal(), processor, isV2ApiRequest(request));
}
private boolean isV2ApiRequest(final SolrRequest request) {
@@ -296,7 +297,7 @@ public HttpUriRequestResponse httpUriRequest(final SolrRequest request, final Re
ExecutorService pool = ExecutorUtil.newMDCAwareFixedThreadPool(1, new SolrjNamedThreadFactory("httpUriRequest"));
try {
MDC.put("HttpSolrClient.url", baseUrl);
- mrr.future = pool.submit(() -> executeMethod(method, processor, isV2ApiRequest(request)));
+ mrr.future = pool.submit(() -> executeMethod(method, request.getUserPrincipal(), processor, isV2ApiRequest(request)));
} finally {
pool.shutdown();
@@ -517,7 +518,7 @@ private HttpEntityEnclosingRequestBase fillContentStream(SolrRequest request, Co
private static final List errPath = Arrays.asList("metadata", "error-class");//Utils.getObjectByPath(err, false,"metadata/error-class")
- protected NamedList executeMethod(HttpRequestBase method, final ResponseParser processor, final boolean isV2Api) throws SolrServerException {
+ protected NamedList executeMethod(HttpRequestBase method, Principal userPrincipal, final ResponseParser processor, final boolean isV2Api) throws SolrServerException {
method.addHeader("User-Agent", AGENT);
org.apache.http.client.config.RequestConfig.Builder requestConfigBuilder = HttpClientUtil.createDefaultRequestConfigBuilder();
@@ -539,6 +540,12 @@ protected NamedList executeMethod(HttpRequestBase method, final Response
try {
// Execute the method.
HttpClientContext httpClientRequestContext = HttpClientUtil.createNewHttpClientRequestContext();
+ if (userPrincipal != null) {
+ // Normally the context contains a static userToken to enable reuse resources.
+ // However, if a personal Principal object exists, we use that instead, also as a means
+ // to transfer authentication information to Auth plugins that wish to intercept the request later
+ httpClientRequestContext.setUserToken(userPrincipal);
+ }
final HttpResponse response = httpClient.execute(method, httpClientRequestContext);
int httpStatus = response.getStatusLine().getStatusCode();
From 1fa8aacab56aceaaa85f5bb1a0e749c0a9c3825f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20H=C3=B8ydahl?=
Date: Mon, 27 Aug 2018 13:18:37 +0200
Subject: [PATCH 10/88] POST test
---
.../JWTAuthPluginIntegrationTest.java | 45 ++++++++++++++++++-
1 file changed, 43 insertions(+), 2 deletions(-)
diff --git a/solr/core/src/test/org/apache/solr/security/JWTAuthPluginIntegrationTest.java b/solr/core/src/test/org/apache/solr/security/JWTAuthPluginIntegrationTest.java
index 3fd3641bdd17..5f80c87ef52f 100644
--- a/solr/core/src/test/org/apache/solr/security/JWTAuthPluginIntegrationTest.java
+++ b/solr/core/src/test/org/apache/solr/security/JWTAuthPluginIntegrationTest.java
@@ -20,6 +20,7 @@
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
+import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.stream.Collectors;
@@ -97,10 +98,50 @@ public void createCollectionAndQueryDistributed() throws Exception {
// Now do a distributed query, using JWTAUth for inter-node
Pair result = get(baseUrl + "mycoll/query?q=*:*", jwtTestToken);
assertEquals(Integer.valueOf(200), result.second());
+
+ // Delete
+ assertEquals(200, get(baseUrl + "admin/collections?action=DELETE&name=mycoll", jwtTestToken).second().intValue());
}
-
-
+ @Test
+ public void createCollectionAndUpdateDistributed() throws Exception {
+ // Admin request will use PKI inter-node auth from Overseer, and succeed
+ assertEquals(200, get(baseUrl + "admin/collections?action=CREATE&name=mycoll&numShards=2", jwtTestToken).second().intValue());
+
+ // Now update two documents
+ Pair result = post(baseUrl + "mycoll/update", "{\"id\" : \"1\"}", jwtTestToken);
+ assertEquals(Integer.valueOf(200), result.second());
+ result = post(baseUrl + "mycoll/update", "{\"id\" : \"2\"}", jwtTestToken);
+ assertEquals(Integer.valueOf(200), result.second());
+ result = post(baseUrl + "mycoll/update", "{\"id\" : \"3\"}", jwtTestToken);
+ assertEquals(Integer.valueOf(200), result.second());
+
+ // Delete
+ assertEquals(200, get(baseUrl + "admin/collections?action=DELETE&name=mycoll", jwtTestToken).second().intValue());
+ }
+
+ private Pair post(String url, String json, String token) throws IOException {
+ URL createUrl = new URL(url);
+ HttpURLConnection con = (HttpURLConnection) createUrl.openConnection();
+ con.setRequestMethod("POST");
+ if (token != null)
+ con.setRequestProperty("Authorization", "Bearer " + token);
+
+ con.setDoOutput(true);
+ OutputStream os = con.getOutputStream();
+ os.write(json.getBytes());
+ os.flush();
+ os.close();
+
+ con.connect();
+ BufferedReader br2 = new BufferedReader(new InputStreamReader((InputStream) con.getContent()));
+ String result = br2.lines().collect(Collectors.joining("\n"));
+ int code = con.getResponseCode();
+ con.disconnect();
+ return new Pair<>(result, code);
+ }
+
+
// NOCOMMIT: Test using SolrJ as client
// private void testCollectionCreateSearchDelete(boolean enableJwt) throws Exception {
// if (enableJwt) {
From 51232a2eff3add9556a895c295f6cccfaa265263 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20H=C3=B8ydahl?=
Date: Mon, 27 Aug 2018 14:26:37 +0200
Subject: [PATCH 11/88] Transfer principal from original request to distributed
requests
---
.../solr/update/SolrCmdDistributor.java | 3 +
.../JWTAuthPluginIntegrationTest.java | 91 +++++++++++++------
2 files changed, 68 insertions(+), 26 deletions(-)
diff --git a/solr/core/src/java/org/apache/solr/update/SolrCmdDistributor.java b/solr/core/src/java/org/apache/solr/update/SolrCmdDistributor.java
index d5aafeca49ee..9b736d2032d0 100644
--- a/solr/core/src/java/org/apache/solr/update/SolrCmdDistributor.java
+++ b/solr/core/src/java/org/apache/solr/update/SolrCmdDistributor.java
@@ -47,6 +47,7 @@
import org.apache.solr.common.params.ModifiableSolrParams;
import org.apache.solr.common.util.NamedList;
import org.apache.solr.core.Diagnostics;
+import org.apache.solr.request.SolrRequestInfo;
import org.apache.solr.update.processor.DistributedUpdateProcessor;
import org.apache.solr.update.processor.DistributedUpdateProcessor.LeaderRequestReplicationTracker;
import org.apache.solr.update.processor.DistributedUpdateProcessor.RollupRequestReplicationTracker;
@@ -278,6 +279,8 @@ void addCommit(UpdateRequest ureq, CommitUpdateCommand cmd) {
}
private void submit(final Req req, boolean isCommit) {
+ // Copy user principal from the original request to the new update request, for later authentication interceptor use
+ req.uReq.setUserPrincipal(SolrRequestInfo.getRequestInfo().getReq().getUserPrincipal());
if (req.synchronous) {
blockAndDoRetries();
diff --git a/solr/core/src/test/org/apache/solr/security/JWTAuthPluginIntegrationTest.java b/solr/core/src/test/org/apache/solr/security/JWTAuthPluginIntegrationTest.java
index 5f80c87ef52f..ce5ab73fbbf7 100644
--- a/solr/core/src/test/org/apache/solr/security/JWTAuthPluginIntegrationTest.java
+++ b/solr/core/src/test/org/apache/solr/security/JWTAuthPluginIntegrationTest.java
@@ -23,8 +23,17 @@
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
+import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
+import org.apache.http.Header;
+import org.apache.http.HttpException;
+import org.apache.http.HttpHeaders;
+import org.apache.http.HttpRequest;
+import org.apache.http.HttpRequestInterceptor;
+import org.apache.http.entity.ContentType;
+import org.apache.http.protocol.HttpContext;
+import org.apache.solr.client.solrj.impl.HttpClientUtil;
import org.apache.solr.cloud.SolrCloudTestCase;
import org.apache.solr.common.util.Pair;
import org.jose4j.jwk.PublicJsonWebKey;
@@ -33,6 +42,7 @@
import org.jose4j.jws.JsonWebSignature;
import org.jose4j.jwt.JwtClaims;
import org.junit.AfterClass;
+import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
@@ -42,6 +52,9 @@ public class JWTAuthPluginIntegrationTest extends SolrCloudTestCase {
protected static final int REPLICATION_FACTOR = 1;
private static String jwtTestToken;
private static String baseUrl;
+ private static AtomicInteger jwtInterceptCount = new AtomicInteger();
+ private static AtomicInteger pkiInterceptCount = new AtomicInteger();
+ private static final CountInterceptor interceptor = new CountInterceptor();
@BeforeClass
public static void setupClass() throws Exception {
@@ -71,6 +84,9 @@ public static void setupClass() throws Exception {
jws.setAlgorithmHeaderValue(AlgorithmIdentifiers.RSA_USING_SHA256);
jwtTestToken = jws.getCompactSerialization();
+
+ HttpClientUtil.removeRequestInterceptor(interceptor);
+ HttpClientUtil.addRequestInterceptor(interceptor);
}
@AfterClass
@@ -79,6 +95,12 @@ public static void tearDownClass() throws Exception {
shutdownCluster();
}
+ @Before
+ public void before() {
+ jwtInterceptCount.set(0);
+ pkiInterceptCount.set(0);
+ }
+
@Test(expected = IOException.class)
public void infoRequestWithoutToken() throws Exception {
get(baseUrl + "admin/info/system", null);
@@ -88,6 +110,7 @@ public void infoRequestWithoutToken() throws Exception {
public void infoRequestWithToken() throws IOException {
Pair result = get(baseUrl + "admin/info/system", jwtTestToken);
assertEquals(Integer.valueOf(200), result.second());
+ verifyInterRequestHeaderCounts(0,0);
}
@Test
@@ -101,6 +124,7 @@ public void createCollectionAndQueryDistributed() throws Exception {
// Delete
assertEquals(200, get(baseUrl + "admin/collections?action=DELETE&name=mycoll", jwtTestToken).second().intValue());
+ verifyInterRequestHeaderCounts(2,2);
}
@Test
@@ -109,37 +133,14 @@ public void createCollectionAndUpdateDistributed() throws Exception {
assertEquals(200, get(baseUrl + "admin/collections?action=CREATE&name=mycoll&numShards=2", jwtTestToken).second().intValue());
// Now update two documents
- Pair result = post(baseUrl + "mycoll/update", "{\"id\" : \"1\"}", jwtTestToken);
- assertEquals(Integer.valueOf(200), result.second());
- result = post(baseUrl + "mycoll/update", "{\"id\" : \"2\"}", jwtTestToken);
- assertEquals(Integer.valueOf(200), result.second());
- result = post(baseUrl + "mycoll/update", "{\"id\" : \"3\"}", jwtTestToken);
+ Pair result = post(baseUrl + "mycoll/update?commit=true", "[{\"id\" : \"1\"}, {\"id\": \"2\"}, {\"id\": \"3\"}]", jwtTestToken);
assertEquals(Integer.valueOf(200), result.second());
// Delete
assertEquals(200, get(baseUrl + "admin/collections?action=DELETE&name=mycoll", jwtTestToken).second().intValue());
+ verifyInterRequestHeaderCounts(2,2);
}
- private Pair post(String url, String json, String token) throws IOException {
- URL createUrl = new URL(url);
- HttpURLConnection con = (HttpURLConnection) createUrl.openConnection();
- con.setRequestMethod("POST");
- if (token != null)
- con.setRequestProperty("Authorization", "Bearer " + token);
-
- con.setDoOutput(true);
- OutputStream os = con.getOutputStream();
- os.write(json.getBytes());
- os.flush();
- os.close();
-
- con.connect();
- BufferedReader br2 = new BufferedReader(new InputStreamReader((InputStream) con.getContent()));
- String result = br2.lines().collect(Collectors.joining("\n"));
- int code = con.getResponseCode();
- con.disconnect();
- return new Pair<>(result, code);
- }
// NOCOMMIT: Test using SolrJ as client
@@ -201,6 +202,11 @@ private Pair post(String url, String json, String token) throws
// solrClient.close();
// }
+ private void verifyInterRequestHeaderCounts(int jwt, int pki) {
+ assertEquals(jwt, jwtInterceptCount.get());
+ assertEquals(pki, jwtInterceptCount.get());
+ }
+
private Pair get(String url, String token) throws IOException {
URL createUrl = new URL(url);
HttpURLConnection createConn = (HttpURLConnection) createUrl.openConnection();
@@ -214,6 +220,28 @@ private Pair get(String url, String token) throws IOException {
return new Pair<>(result, code);
}
+ private Pair post(String url, String json, String token) throws IOException {
+ URL createUrl = new URL(url);
+ HttpURLConnection con = (HttpURLConnection) createUrl.openConnection();
+ con.setRequestMethod("POST");
+ con.setRequestProperty(HttpHeaders.CONTENT_TYPE, ContentType.APPLICATION_JSON.getMimeType());
+ if (token != null)
+ con.setRequestProperty("Authorization", "Bearer " + token);
+
+ con.setDoOutput(true);
+ OutputStream os = con.getOutputStream();
+ os.write(json.getBytes());
+ os.flush();
+ os.close();
+
+ con.connect();
+ BufferedReader br2 = new BufferedReader(new InputStreamReader((InputStream) con.getContent()));
+ String result = br2.lines().collect(Collectors.joining("\n"));
+ int code = con.getResponseCode();
+ con.disconnect();
+ return new Pair<>(result, code);
+ }
+
// SolrHttpClientBuilder getHttpClientBuilder(SolrHttpClientBuilder builder, String token) {
// if (builder == null) {
// builder = SolrHttpClientBuilder.create();
@@ -232,5 +260,16 @@ private Pair get(String url, String token) throws IOException {
// return builder;
// }
-
+ private static class CountInterceptor implements HttpRequestInterceptor {
+ @Override
+ public void process(HttpRequest request, HttpContext context) throws HttpException, IOException {
+ Header ah = request.getFirstHeader(HttpHeaders.AUTHORIZATION);
+ if (ah != null && ah.getValue().startsWith("Bearer"))
+ jwtInterceptCount.addAndGet(1);
+
+ Header ph = request.getFirstHeader(PKIAuthenticationPlugin.HEADER);
+ if (ph != null)
+ pkiInterceptCount.addAndGet(1);
+ }
+ }
}
From 711303281946e0fba768fe62ab9826a0090069e4 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20H=C3=B8ydahl?=
Date: Mon, 27 Aug 2018 15:03:33 +0200
Subject: [PATCH 12/88] Debug level for query test
---
.../solr/security/JWTAuthPluginIntegrationTest.java | 11 ++++++++++-
1 file changed, 10 insertions(+), 1 deletion(-)
diff --git a/solr/core/src/test/org/apache/solr/security/JWTAuthPluginIntegrationTest.java b/solr/core/src/test/org/apache/solr/security/JWTAuthPluginIntegrationTest.java
index ce5ab73fbbf7..63c24f70c004 100644
--- a/solr/core/src/test/org/apache/solr/security/JWTAuthPluginIntegrationTest.java
+++ b/solr/core/src/test/org/apache/solr/security/JWTAuthPluginIntegrationTest.java
@@ -36,6 +36,7 @@
import org.apache.solr.client.solrj.impl.HttpClientUtil;
import org.apache.solr.cloud.SolrCloudTestCase;
import org.apache.solr.common.util.Pair;
+import org.apache.solr.util.LogLevel;
import org.jose4j.jwk.PublicJsonWebKey;
import org.jose4j.jwk.RsaJsonWebKey;
import org.jose4j.jws.AlgorithmIdentifiers;
@@ -114,13 +115,21 @@ public void infoRequestWithToken() throws IOException {
}
@Test
+ @LogLevel("org.apache.solr.security=DEBUG")
public void createCollectionAndQueryDistributed() throws Exception {
// Admin request will use PKI inter-node auth from Overseer, and succeed
assertEquals(200, get(baseUrl + "admin/collections?action=CREATE&name=mycoll&numShards=2", jwtTestToken).second().intValue());
+ verifyInterRequestHeaderCounts(0,0);
+ // First a non distributed query
+ Pair result = get(baseUrl + "mycoll/query?q=*:*&distrib=false", jwtTestToken);
+ assertEquals(Integer.valueOf(200), result.second());
+ verifyInterRequestHeaderCounts(0,0);
+
// Now do a distributed query, using JWTAUth for inter-node
- Pair result = get(baseUrl + "mycoll/query?q=*:*", jwtTestToken);
+ result = get(baseUrl + "mycoll/query?q=*:*", jwtTestToken);
assertEquals(Integer.valueOf(200), result.second());
+ verifyInterRequestHeaderCounts(2,2);
// Delete
assertEquals(200, get(baseUrl + "admin/collections?action=DELETE&name=mycoll", jwtTestToken).second().intValue());
From d7a1c38f503ac1df15af8e4007aa9722cc8028fb Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20H=C3=B8ydahl?=
Date: Wed, 5 Sep 2018 14:46:27 +0200
Subject: [PATCH 13/88] Support for well-known config and scopes
---
.../apache/solr/security/JWTAuthPlugin.java | 406 ++++++++++--------
.../apache/solr/security/JWTPrincipal.java | 88 ++++
.../security/JWTPrincipalWithUserRoles.java | 71 +++
.../solr/security/jwt_well-known-config.json | 84 ++++
.../solr/security/JWTAuthPluginTest.java | 137 ++++--
.../src/jwt-authentication-plugin.adoc | 63 ++-
6 files changed, 604 insertions(+), 245 deletions(-)
create mode 100644 solr/core/src/java/org/apache/solr/security/JWTPrincipal.java
create mode 100644 solr/core/src/java/org/apache/solr/security/JWTPrincipalWithUserRoles.java
create mode 100644 solr/core/src/test-files/solr/security/jwt_well-known-config.json
diff --git a/solr/core/src/java/org/apache/solr/security/JWTAuthPlugin.java b/solr/core/src/java/org/apache/solr/security/JWTAuthPlugin.java
index a9a5f76b6a37..20a0de159319 100644
--- a/solr/core/src/java/org/apache/solr/security/JWTAuthPlugin.java
+++ b/solr/core/src/java/org/apache/solr/security/JWTAuthPlugin.java
@@ -22,19 +22,22 @@
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletResponse;
+import java.io.ByteArrayInputStream;
import java.io.IOException;
-import java.io.Serializable;
+import java.io.InputStream;
import java.lang.invoke.MethodHandles;
import java.net.MalformedURLException;
import java.net.URL;
+import java.nio.charset.Charset;
import java.security.Principal;
+import java.time.Instant;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
-import java.util.Objects;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.regex.Pattern;
@@ -45,15 +48,15 @@
import org.apache.http.HttpRequestInterceptor;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.protocol.HttpContext;
-import org.apache.http.util.Args;
import org.apache.solr.client.solrj.impl.HttpClientUtil;
import org.apache.solr.client.solrj.impl.SolrHttpClientBuilder;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.SpecProvider;
+import org.apache.solr.common.StringUtils;
import org.apache.solr.common.util.Utils;
import org.apache.solr.common.util.ValidatingJsonMap;
import org.apache.solr.core.CoreContainer;
-import org.apache.solr.security.JWTAuthPlugin.AuthenticationResponse.AuthCode;
+import org.apache.solr.security.JWTAuthPlugin.JWTAuthenticationResponse.AuthCode;
import org.jose4j.jwa.AlgorithmConstraints;
import org.jose4j.jwk.HttpsJwks;
import org.jose4j.jwk.JsonWebKey;
@@ -82,14 +85,18 @@ public class JWTAuthPlugin extends AuthenticationPlugin implements HttpClientBui
private static final String PARAM_AUDIENCE = "aud";
private static final String PARAM_REQUIRE_SUBJECT = "require_sub";
private static final String PARAM_PRINCIPAL_CLAIM = "principal_claim";
- private static final String PARAM_ROLES_CLAIM = "roles_claim";
private static final String PARAM_REQUIRE_EXPIRATIONTIME = "require_exp";
private static final String PARAM_ALG_WHITELIST = "alg_whitelist";
private static final String PARAM_JWK_CACHE_DURATION = "jwk_cache_dur";
private static final String PARAM_CLAIMS_MATCH = "claims_match";
- private static final String AUTH_REALM = "solr";
+ private static final String PARAM_SCOPE = "scope";
+ private static final String PARAM_ADMIN_SCOPE = "admin_scope";
+ private static final String PARAM_CLIENT_ID = "client_id";
+ private static final String PARAM_WELL_KNOWN_URL = "well_known_url";
+ private static final String AUTH_REALM = "solr-jwt";
+ private static final String CLAIM_SCOPE = "scope";
- private final PkiDelegationInterceptor interceptor = new PkiDelegationInterceptor();
+ private final JwtPkiDelegationInterceptor interceptor = new JwtPkiDelegationInterceptor();
private JwtConsumer jwtConsumer;
private String iss;
@@ -99,9 +106,16 @@ public class JWTAuthPlugin extends AuthenticationPlugin implements HttpClientBui
private List algWhitelist;
private VerificationKeyResolver verificationKeyResolver;
private String principalClaim;
- private String rolesClaim;
private HashMap claimsMatchCompiled;
private boolean blockUnknown;
+ private String adminScope;
+ private HashSet requiredScopes = new HashSet<>();
+ private String clientId;
+ private long jwkCacheDuration;
+ private OidcDiscoveryConfig oidcDiscoveryConfig;
+ private String confIdpConfigUrl;
+ private Map pluginConfig;
+ private Instant lastInitTime = Instant.now();
private CoreContainer coreContainer;
/**
@@ -115,16 +129,41 @@ public JWTAuthPlugin(CoreContainer coreContainer) {
@Override
public void init(Map pluginConfig) {
blockUnknown = Boolean.parseBoolean(String.valueOf(pluginConfig.getOrDefault(PARAM_BLOCK_UNKNOWN, false)));
- String jwk_url = (String) pluginConfig.get(PARAM_JWK_URL);
- Map jwk = (Map) pluginConfig.get(PARAM_JWK);
- iss = (String) pluginConfig.get(PARAM_ISSUER);
- aud = (String) pluginConfig.get(PARAM_AUDIENCE);
+ clientId = (String) pluginConfig.get(PARAM_CLIENT_ID);
requireSubject = Boolean.parseBoolean(String.valueOf(pluginConfig.getOrDefault(PARAM_REQUIRE_SUBJECT, "true")));
requireExpirationTime = Boolean.parseBoolean(String.valueOf(pluginConfig.getOrDefault(PARAM_REQUIRE_EXPIRATIONTIME, "true")));
- algWhitelist = (List) pluginConfig.get(PARAM_ALG_WHITELIST);
- long jwkCacheDuration = Long.parseLong((String) pluginConfig.getOrDefault(PARAM_JWK_CACHE_DURATION, "3600"));
principalClaim = (String) pluginConfig.getOrDefault(PARAM_PRINCIPAL_CLAIM, "sub");
- rolesClaim = (String) pluginConfig.get(PARAM_ROLES_CLAIM);
+ confIdpConfigUrl = (String) pluginConfig.get(PARAM_WELL_KNOWN_URL);
+
+ if (confIdpConfigUrl != null) {
+ log.debug("Initializing well-known oidc config from {}", confIdpConfigUrl);
+ oidcDiscoveryConfig = OidcDiscoveryConfig.parse(confIdpConfigUrl);
+ iss = oidcDiscoveryConfig.getIssuer();
+ }
+
+ if (pluginConfig.containsKey(PARAM_ISSUER)) {
+ if (iss != null) {
+ log.debug("Explicitly setting required issuer instead of using issuer from well-known config");
+ }
+ iss = (String) pluginConfig.get(PARAM_ISSUER);
+ }
+
+ if (pluginConfig.containsKey(PARAM_AUDIENCE)) {
+ if (clientId != null) {
+ log.debug("Explicitly setting required audience instead of using configured clientId");
+ }
+ aud = (String) pluginConfig.get(PARAM_AUDIENCE);
+ } else {
+ aud = clientId;
+ }
+
+ algWhitelist = (List) pluginConfig.get(PARAM_ALG_WHITELIST);
+
+ String requiredScopesStr = (String) pluginConfig.get(PARAM_SCOPE);
+ if (!StringUtils.isEmpty(requiredScopesStr)) {
+ requiredScopes = new HashSet<>(Arrays.asList(requiredScopesStr.split("\\s+")));
+ }
+ adminScope = (String) pluginConfig.get(PARAM_ADMIN_SCOPE);
Map claimsMatch = (Map) pluginConfig.get(PARAM_CLAIMS_MATCH);
claimsMatchCompiled = new HashMap<>();
if (claimsMatch != null) {
@@ -133,32 +172,58 @@ public void init(Map pluginConfig) {
}
}
+ initJwk(pluginConfig);
+
+ lastInitTime = Instant.now();
+ }
+
+ private void initJwk(Map pluginConfig) {
+ this.pluginConfig = pluginConfig;
+ String confJwkUrl = (String) pluginConfig.get(PARAM_JWK_URL);
+ Map confJwk = (Map) pluginConfig.get(PARAM_JWK);
+ jwkCacheDuration = Long.parseLong((String) pluginConfig.getOrDefault(PARAM_JWK_CACHE_DURATION, "3600"));
+
jwtConsumer = null;
- if (jwk_url != null) {
- // The HttpsJwks retrieves and caches keys from a the given HTTPS JWKS endpoint.
- try {
- URL jwkUrl = new URL(jwk_url);
- if (!"https".equalsIgnoreCase(jwkUrl.getProtocol())) {
- throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "jwk_url must be an HTTPS url");
- }
- } catch (MalformedURLException e) {
- throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "jwk_url must be a valid https URL");
- }
- HttpsJwks httpsJkws = new HttpsJwks(jwk_url);
- httpsJkws.setDefaultCacheDuration(jwkCacheDuration);
- verificationKeyResolver = new HttpsJwksVerificationKeyResolver(httpsJkws);
- initConsumer();
- } else if (jwk != null) {
+ int jwkConfigured = confIdpConfigUrl != null ? 1 : 0;
+ jwkConfigured += confJwkUrl != null ? 1 : 0;
+ jwkConfigured += confJwk != null ? 1 : 0;
+ if (jwkConfigured > 1) {
+ throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "JWTAuthPlugin needs to configure exactly one of " +
+ PARAM_WELL_KNOWN_URL + ", " + PARAM_JWK_URL + " and " + PARAM_JWK);
+ }
+ if (jwkConfigured == 0) {
+ log.warn("Initialized JWTAuthPlugin without any JWK config. No requests will succeed");
+ }
+ if (oidcDiscoveryConfig != null) {
+ String jwkUrl = oidcDiscoveryConfig.getJwksUrl();
+ setupJwkUrl(jwkUrl);
+ } else if (confJwkUrl != null) {
+ setupJwkUrl(confJwkUrl);
+ } else if (confJwk != null) {
try {
- JsonWebKeySet jwks = parseJwkSet(jwk);
+ JsonWebKeySet jwks = parseJwkSet(confJwk);
verificationKeyResolver = new JwksVerificationKeyResolver(jwks.getJsonWebKeys());
- initConsumer();
} catch (JoseException e) {
throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "Invalid JWTAuthPlugin configuration, jwk field parse error", e);
}
- } else {
- log.warn("JWTAuthPlugin needs to specify either 'jwk' or 'jwk_url' parameters.");
}
+ initConsumer();
+ log.debug("JWK configured");
+ }
+
+ private void setupJwkUrl(String url) {
+ // The HttpsJwks retrieves and caches keys from a the given HTTPS JWKS endpoint.
+ try {
+ URL jwkUrl = new URL(url);
+ if (!"https".equalsIgnoreCase(jwkUrl.getProtocol())) {
+ throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "jwk_url must be an HTTPS url");
+ }
+ } catch (MalformedURLException e) {
+ throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "jwk_url must be a valid URL");
+ }
+ HttpsJwks httpsJkws = new HttpsJwks(url);
+ httpsJkws.setDefaultCacheDuration(jwkCacheDuration);
+ verificationKeyResolver = new HttpsJwksVerificationKeyResolver(httpsJkws);
}
JsonWebKeySet parseJwkSet(Map jwkObj) throws JoseException {
@@ -185,18 +250,24 @@ public boolean doAuthenticate(ServletRequest servletRequest, ServletResponse ser
String header = request.getHeader(HttpHeaders.AUTHORIZATION);
if (jwtConsumer == null) {
- //
if (header == null && !blockUnknown) {
log.info("JWTAuth not configured, but allowing anonymous access since blockUnknown==false");
filterChain.doFilter(request, response);
return true;
}
- log.warn("JWTAuth not configured");
- throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "JWTAuth plugin not correctly configured");
+ // Retry config
+ if (lastInitTime.plusSeconds(10).isAfter(Instant.now())) {
+ log.info("Retrying JWTAuthPlugin initialization");
+ init(pluginConfig);
+ }
+ if (jwtConsumer == null) {
+ log.warn("JWTAuth not configured");
+ throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "JWTAuth plugin not correctly configured");
+ }
}
- AuthenticationResponse authResponse = authenticate(header);
- switch(authResponse.authCode) {
+ JWTAuthenticationResponse authResponse = authenticate(header);
+ switch(authResponse.getAuthCode()) {
case AUTHENTICATED:
HttpServletRequestWrapper wrapper = new HttpServletRequestWrapper(request) {
@Override
@@ -219,8 +290,8 @@ public Principal getUserPrincipal() {
return true;
case AUTZ_HEADER_PROBLEM:
- log.debug("Authentication failed with reason {}, message {}", authResponse.authCode, authResponse.errorMessage);
- authenticationFailure(response, authResponse.getAuthCode().msg, HttpServletResponse.SC_BAD_REQUEST, BearerWwwAuthErrorCode.invalid_request);
+ log.debug("Authentication failed with reason {}, message {}", authResponse.getAuthCode(), authResponse.getErrorMessage());
+ authenticationFailure(response, authResponse.getAuthCode().getMsg(), HttpServletResponse.SC_BAD_REQUEST, BearerWwwAuthErrorCode.invalid_request);
return false;
case CLAIM_MISMATCH:
@@ -228,17 +299,22 @@ public Principal getUserPrincipal() {
case JWT_PARSE_ERROR:
case JWT_VALIDATION_EXCEPTION:
case PRINCIPAL_MISSING:
- log.debug("Authentication failed with reason {}, message {}", authResponse.authCode, authResponse.errorMessage);
+ log.debug("Authentication failed with reason {}, message {}", authResponse.getAuthCode(), authResponse.getErrorMessage());
if (authResponse.getJwtException() != null) {
log.warn("Exception: {}", authResponse.getJwtException().getMessage());
}
- authenticationFailure(response, authResponse.getAuthCode().msg, HttpServletResponse.SC_UNAUTHORIZED, BearerWwwAuthErrorCode.invalid_token);
+ authenticationFailure(response, authResponse.getAuthCode().getMsg(), HttpServletResponse.SC_UNAUTHORIZED, BearerWwwAuthErrorCode.invalid_token);
return false;
+ case SCOPE_MISSING:
+ log.debug("Authentication failed with reason {}, message {}", authResponse.getAuthCode(), authResponse.getErrorMessage());
+ authenticationFailure(response, authResponse.getAuthCode().getMsg(), HttpServletResponse.SC_UNAUTHORIZED, BearerWwwAuthErrorCode.insufficient_scope);
+ return false;
+
case NO_AUTZ_HEADER:
default:
- log.debug("Authentication failed with reason {}, message {}", authResponse.authCode, authResponse.errorMessage);
- authenticationFailure(response, authResponse.getAuthCode().msg);
+ log.debug("Authentication failed with reason {}, message {}", authResponse.getAuthCode(), authResponse.getErrorMessage());
+ authenticationFailure(response, authResponse.getAuthCode().getMsg());
return false;
}
}
@@ -249,7 +325,7 @@ public Principal getUserPrincipal() {
* @param authorizationHeader the http header "Authentication"
* @return AuthenticationResponse object
*/
- protected AuthenticationResponse authenticate(String authorizationHeader) {
+ protected JWTAuthenticationResponse authenticate(String authorizationHeader) {
if (authorizationHeader != null) {
StringTokenizer st = new StringTokenizer(authorizationHeader);
if (st.hasMoreTokens()) {
@@ -261,61 +337,70 @@ protected AuthenticationResponse authenticate(String authorizationHeader) {
JwtClaims jwtClaims = jwtConsumer.processToClaims(jwtCompact);
String principal = jwtClaims.getStringClaimValue(principalClaim);
if (principal == null || principal.isEmpty()) {
- return new AuthenticationResponse(AuthCode.PRINCIPAL_MISSING, "Cannot identify principal from JWT. Required claim " + principalClaim + " missing. Cannot authenticate");
+ return new JWTAuthenticationResponse(AuthCode.PRINCIPAL_MISSING, "Cannot identify principal from JWT. Required claim " + principalClaim + " missing. Cannot authenticate");
}
if (claimsMatchCompiled != null) {
for (Map.Entry entry : claimsMatchCompiled.entrySet()) {
String claim = entry.getKey();
if (jwtClaims.hasClaim(claim)) {
if (!entry.getValue().matcher(jwtClaims.getStringClaimValue(claim)).matches()) {
- return new AuthenticationResponse(AuthCode.CLAIM_MISMATCH,
+ return new JWTAuthenticationResponse(AuthCode.CLAIM_MISMATCH,
"Claim " + claim + "=" + jwtClaims.getStringClaimValue(claim)
+ " does not match required regular expression " + entry.getValue().pattern());
}
} else {
- return new AuthenticationResponse(AuthCode.CLAIM_MISMATCH, "Claim " + claim + " is required but does not exist in JWT");
+ return new JWTAuthenticationResponse(AuthCode.CLAIM_MISMATCH, "Claim " + claim + " is required but does not exist in JWT");
}
}
}
- Set roles = Collections.emptySet();
- if (rolesClaim != null) {
- if (!jwtClaims.hasClaim(rolesClaim)) {
- return new AuthenticationResponse(AuthCode.CLAIM_MISMATCH, "Roles claim " + rolesClaim + " is required but does not exist in JWT");
+ if (!requiredScopes.isEmpty() && !jwtClaims.hasClaim(CLAIM_SCOPE)) {
+ // Fail if we require scopes but they don't exist
+ return new JWTAuthenticationResponse(AuthCode.CLAIM_MISMATCH, "Claim " + CLAIM_SCOPE + " is required but does not exist in JWT");
+ }
+ Set scopes = Collections.emptySet();
+ Object scopesObj = jwtClaims.getClaimValue(CLAIM_SCOPE);
+ if (scopesObj != null) {
+ if (scopesObj instanceof String) {
+ scopes = new HashSet<>(Arrays.asList(((String) scopesObj).split("\\s+")));
+ } else if (scopesObj instanceof List) {
+ scopes = new HashSet<>(jwtClaims.getStringListClaimValue(CLAIM_SCOPE));
}
- Object rolesObj = jwtClaims.getClaimValue(rolesClaim);
- if (rolesObj instanceof String) {
- roles = Collections.singleton((String) rolesObj);
- } else if (rolesObj instanceof List) {
- roles = new HashSet<>(jwtClaims.getStringListClaimValue(rolesClaim));
+ // Validate that at least one of the required scopes are present in the scope claim
+ if (!requiredScopes.isEmpty()) {
+ if (scopes.stream().noneMatch(requiredScopes::contains)) {
+ return new JWTAuthenticationResponse(AuthCode.SCOPE_MISSING, "'scope' claim does not contain any of the required scopes: " + requiredScopes);
+ }
}
- // Pass roles with principal to signal to any Authorization plugins that user has some verified role claims
- return new AuthenticationResponse(AuthCode.AUTHENTICATED, new JWTPrincipalWithUserRoles(principal, jwtCompact, jwtClaims.getClaimsMap(), roles));
+ final Set roles = new HashSet<>(scopes);
+ roles.remove("openid"); // Remove standard claims
+ // Pass scopes with principal to signal to any Authorization plugins that user has some verified role claims
+ return new JWTAuthenticationResponse(AuthCode.AUTHENTICATED, new JWTPrincipalWithUserRoles(principal, jwtCompact, jwtClaims.getClaimsMap(), roles));
} else {
- return new AuthenticationResponse(AuthCode.AUTHENTICATED, new JWTPrincipal(principal, jwtCompact, jwtClaims.getClaimsMap()));
+ return new JWTAuthenticationResponse(AuthCode.AUTHENTICATED, new JWTPrincipal(principal, jwtCompact, jwtClaims.getClaimsMap()));
}
} catch (InvalidJwtException e) {
// Whether or not the JWT has expired being one common reason for invalidity
if (e.hasExpired()) {
- return new AuthenticationResponse(AuthCode.JWT_EXPIRED, "Authentication failed due to expired JWT token. Expired at " + e.getJwtContext().getJwtClaims().getExpirationTime());
+ return new JWTAuthenticationResponse(AuthCode.JWT_EXPIRED, "Authentication failed due to expired JWT token. Expired at " + e.getJwtContext().getJwtClaims().getExpirationTime());
}
- return new AuthenticationResponse(AuthCode.JWT_VALIDATION_EXCEPTION, e);
+ return new JWTAuthenticationResponse(AuthCode.JWT_VALIDATION_EXCEPTION, e);
}
} catch (MalformedClaimException e) {
- return new AuthenticationResponse(AuthCode.JWT_PARSE_ERROR, "Malformed claim, error was: " + e.getMessage());
+ return new JWTAuthenticationResponse(AuthCode.JWT_PARSE_ERROR, "Malformed claim, error was: " + e.getMessage());
}
} else {
- return new AuthenticationResponse(AuthCode.AUTZ_HEADER_PROBLEM, "Authorization header is not in correct format");
+ return new JWTAuthenticationResponse(AuthCode.AUTZ_HEADER_PROBLEM, "Authorization header is not in correct format");
}
} else {
- return new AuthenticationResponse(AuthCode.AUTZ_HEADER_PROBLEM, "Authorization header is not in correct format");
+ return new JWTAuthenticationResponse(AuthCode.AUTZ_HEADER_PROBLEM, "Authorization header is not in correct format");
}
} else {
// No Authorization header
if (blockUnknown) {
- return new AuthenticationResponse(AuthCode.NO_AUTZ_HEADER, "Missing Authorization header");
+ return new JWTAuthenticationResponse(AuthCode.NO_AUTZ_HEADER, "Missing Authorization header");
} else {
log.debug("No user authenticated, but block_unknown=false, so letting request through");
- return new AuthenticationResponse(AuthCode.PASS_THROUGH);
+ return new JWTAuthenticationResponse(AuthCode.PASS_THROUGH);
}
}
}
@@ -387,12 +472,12 @@ private void authenticationFailure(HttpServletResponse response, String message,
/**
* Response for authentication attempt
*/
- static class AuthenticationResponse {
+ static class JWTAuthenticationResponse {
private final Principal principal;
private String errorMessage;
private AuthCode authCode;
private InvalidJwtException jwtException;
-
+
enum AuthCode {
PASS_THROUGH("No user, pass through"), // Returned when no user authentication but block_unknown=false
AUTHENTICATED("Authenticated"), // Returned when authentication OK
@@ -402,183 +487,130 @@ enum AuthCode {
NO_AUTZ_HEADER("Require authentication"), // The Authorization header is missing
JWT_EXPIRED("JWT token expired"), // JWT token has expired
CLAIM_MISMATCH("Required JWT claim missing"), // Some required claims are missing or wrong
- JWT_VALIDATION_EXCEPTION("JWT validation failed"); // The JWT parser failed validation. More details in exception
-
+ JWT_VALIDATION_EXCEPTION("JWT validation failed"), // The JWT parser failed validation. More details in exception
+ SCOPE_MISSING("Required scope missing in JWT"); // None of the required scopes were present in JWT
+
+ public String getMsg() {
+ return msg;
+ }
+
private final String msg;
-
+
AuthCode(String msg) {
this.msg = msg;
}
}
-
- public AuthenticationResponse(AuthCode authCode, InvalidJwtException e) {
+
+ JWTAuthenticationResponse(AuthCode authCode, InvalidJwtException e) {
this.authCode = authCode;
this.jwtException = e;
principal = null;
this.errorMessage = e.getMessage();
}
- public AuthenticationResponse(AuthCode authCode, String errorMessage) {
+ JWTAuthenticationResponse(AuthCode authCode, String errorMessage) {
this.authCode = authCode;
this.errorMessage = errorMessage;
principal = null;
}
-
- public AuthenticationResponse(AuthCode authCode, Principal principal) {
+
+ JWTAuthenticationResponse(AuthCode authCode, Principal principal) {
this.authCode = authCode;
this.principal = principal;
}
- public AuthenticationResponse(AuthCode authCode) {
+ JWTAuthenticationResponse(AuthCode authCode) {
this.authCode = authCode;
principal = null;
}
-
- public boolean isAuthenticated() {
+
+ boolean isAuthenticated() {
return authCode.equals(AuthCode.AUTHENTICATED);
}
-
+
public Principal getPrincipal() {
return principal;
}
-
- public String getErrorMessage() {
+
+ String getErrorMessage() {
return errorMessage;
}
-
- public InvalidJwtException getJwtException() {
+
+ InvalidJwtException getJwtException() {
return jwtException;
}
-
- public AuthCode getAuthCode() {
+
+ AuthCode getAuthCode() {
return authCode;
}
}
/**
- * Principal object that carries JWT token and claims for authenticated user.
+ * Config object for a OpenId Connect well-known config
+ * Typically exposed through /.well-known/openid-configuration endpoint
*/
- public static class JWTPrincipal implements Principal, Serializable {
- private static final long serialVersionUID = 4144666467522831388L;
- final String username;
- String token;
- Map claims;
+ public static class OidcDiscoveryConfig {
+ private static Map securityConf;
- /**
- * User principal with user name as well as one or more roles that he/she belong to
- * @param username string with user name for user
- * @param token compact string representation of JWT token
- * @param claims list of verified JWT claims as a map
- */
- public JWTPrincipal(final String username, String token, Map claims) {
- super();
- Args.notNull(username, "User name");
- Args.notNull(token, "JWT token");
- Args.notNull(claims, "JWT claims");
- this.token = token;
- this.claims = claims;
- this.username = username;
+ OidcDiscoveryConfig(Map securityConf) {
+ OidcDiscoveryConfig.securityConf = securityConf;
}
- @Override
- public String getName() {
- return this.username;
- }
-
- public String getToken() {
- return token;
- }
-
- public Map getClaims() {
- return claims;
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- JWTPrincipal that = (JWTPrincipal) o;
- return Objects.equals(username, that.username) &&
- Objects.equals(token, that.token) &&
- Objects.equals(claims, that.claims);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(username, token, claims);
+ public static OidcDiscoveryConfig parse(String urlString) {
+ try {
+ URL url = new URL(urlString);
+ if (!Arrays.asList("https", "file").contains(url.getProtocol())) {
+ throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Well-known config URL must be HTTPS or file");
+ }
+ return parse(url.openStream());
+ } catch (MalformedURLException e) {
+ throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "Well-known config URL " + urlString + " is malformed", e);
+ } catch (IOException e) {
+ throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "Well-known config could not be read from url " + urlString, e);
+ }
}
-
- @Override
- public String toString() {
- return "JWTPrincipal{" +
- "username='" + username + '\'' +
- ", token='" + token + '\'' +
- ", claims=" + claims +
- '}';
+
+ public static OidcDiscoveryConfig parse(String json, Charset charset) {
+ return parse(new ByteArrayInputStream(json.getBytes(charset)));
}
- }
- /**
- * JWT principal that contains username, token, claims and a list of roles the user has,
- * so one can keep track of user-role mappings in an Identity Server external to Solr and
- * pass the information to Solr in a signed JWT token. The role information can then be used to authorize
- * requests without the need to maintain or lookup what roles each user belongs to.
- */
- public static class JWTPrincipalWithUserRoles extends JWTPrincipal implements VerifiedUserRoles {
- private final Set roles;
-
- public JWTPrincipalWithUserRoles(final String username, String token, Map claims, Set roles) {
- super(username, token, claims);
- Args.notNull(roles, "User roles");
- this.roles = roles;
+ public static OidcDiscoveryConfig parse(InputStream configStream) {
+ securityConf = (Map) Utils.fromJSON(configStream);
+ return new OidcDiscoveryConfig(securityConf);
}
+
- /**
- * Gets the list of roles
- */
- @Override
- public Set getVerifiedRoles() {
- return roles;
- }
-
- @Override
- public boolean equals(Object o) {
- if (!(o instanceof JWTPrincipalWithUserRoles))
- return false;
- JWTPrincipalWithUserRoles that = (JWTPrincipalWithUserRoles) o;
- return super.equals(o) && roles.equals(that.roles);
+ public String getJwksUrl() {
+ return (String) securityConf.get("jwks_uri");
}
-
- @Override
- public int hashCode() {
- return Objects.hash(username, token, claims, roles);
+
+ public String getIssuer() {
+ return (String) securityConf.get("issuer");
}
-
- @Override
- public String toString() {
- return "JWTPrincipalWithUserRoles{" +
- "username='" + username + '\'' +
- ", token='" + token + '\'' +
- ", claims=" + claims +
- ", roles=" + roles +
- '}';
+
+ public String getAuthorizationEndpoint() {
+ return (String) securityConf.get("authorization_endpoint");
}
}
- // The interceptor class that adds correct header or delegates to PKI
- private class PkiDelegationInterceptor implements HttpRequestInterceptor {
+ /**
+ * The interceptor class that adds correct header or delegates to PKI.
+ */
+ public class JwtPkiDelegationInterceptor implements HttpRequestInterceptor {
+ private final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
@Override
public void process(HttpRequest request, HttpContext context) throws HttpException, IOException {
if (context instanceof HttpClientContext) {
HttpClientContext httpClientContext = (HttpClientContext) context;
if (httpClientContext.getUserToken() instanceof JWTPrincipal) {
JWTPrincipal jwtPrincipal = (JWTPrincipal) httpClientContext.getUserToken();
- request.setHeader(HttpHeaders.AUTHORIZATION, "Bearer " + jwtPrincipal.token);
+ request.setHeader(HttpHeaders.AUTHORIZATION, "Bearer " + jwtPrincipal.getToken());
log.debug("Set JWT header on inter-node request");
return;
}
}
-
+
if (coreContainer.getPkiAuthenticationPlugin() != null) {
log.debug("Inter-node request delegated from JWTAuthPlugin to PKIAuthenticationPlugin");
coreContainer.getPkiAuthenticationPlugin().setHeader(request);
diff --git a/solr/core/src/java/org/apache/solr/security/JWTPrincipal.java b/solr/core/src/java/org/apache/solr/security/JWTPrincipal.java
new file mode 100644
index 000000000000..eaa14e4a72d5
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/security/JWTPrincipal.java
@@ -0,0 +1,88 @@
+/*
+ * 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.
+ */
+
+package org.apache.solr.security;
+
+import java.io.Serializable;
+import java.security.Principal;
+import java.util.Map;
+import java.util.Objects;
+
+import org.apache.http.util.Args;
+
+/**
+ * Principal object that carries JWT token and claims for authenticated user.
+ */
+public class JWTPrincipal implements Principal, Serializable {
+ private static final long serialVersionUID = 4144666467522831388L;
+ final String username;
+ String token;
+ Map claims;
+
+ /**
+ * User principal with user name as well as one or more roles that he/she belong to
+ * @param username string with user name for user
+ * @param token compact string representation of JWT token
+ * @param claims list of verified JWT claims as a map
+ */
+ public JWTPrincipal(final String username, String token, Map claims) {
+ super();
+ Args.notNull(username, "User name");
+ Args.notNull(token, "JWT token");
+ Args.notNull(claims, "JWT claims");
+ this.token = token;
+ this.claims = claims;
+ this.username = username;
+ }
+
+ @Override
+ public String getName() {
+ return this.username;
+ }
+
+ public String getToken() {
+ return token;
+ }
+
+ public Map getClaims() {
+ return claims;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ JWTPrincipal that = (JWTPrincipal) o;
+ return Objects.equals(username, that.username) &&
+ Objects.equals(token, that.token) &&
+ Objects.equals(claims, that.claims);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(username, token, claims);
+ }
+
+ @Override
+ public String toString() {
+ return "JWTPrincipal{" +
+ "username='" + username + '\'' +
+ ", token='" + token + '\'' +
+ ", claims=" + claims +
+ '}';
+ }
+}
diff --git a/solr/core/src/java/org/apache/solr/security/JWTPrincipalWithUserRoles.java b/solr/core/src/java/org/apache/solr/security/JWTPrincipalWithUserRoles.java
new file mode 100644
index 000000000000..473f2f4f6e51
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/security/JWTPrincipalWithUserRoles.java
@@ -0,0 +1,71 @@
+/*
+ * 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.
+ */
+
+package org.apache.solr.security;
+
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+
+import org.apache.http.util.Args;
+
+/**
+ * JWT principal that contains username, token, claims and a list of roles the user has,
+ * so one can keep track of user-role mappings in an Identity Server external to Solr and
+ * pass the information to Solr in a signed JWT token. The role information can then be used to authorize
+ * requests without the need to maintain or lookup what roles each user belongs to.
+ */
+ public class JWTPrincipalWithUserRoles extends JWTPrincipal implements VerifiedUserRoles {
+ private final Set roles;
+
+ public JWTPrincipalWithUserRoles(final String username, String token, Map claims, Set roles) {
+ super(username, token, claims);
+ Args.notNull(roles, "User roles");
+ this.roles = roles;
+ }
+
+ /**
+ * Gets the list of roles
+ */
+ @Override
+ public Set getVerifiedRoles() {
+ return roles;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (!(o instanceof JWTPrincipalWithUserRoles))
+ return false;
+ JWTPrincipalWithUserRoles that = (JWTPrincipalWithUserRoles) o;
+ return super.equals(o) && roles.equals(that.roles);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(username, token, claims, roles);
+ }
+
+ @Override
+ public String toString() {
+ return "JWTPrincipalWithUserRoles{" +
+ "username='" + username + '\'' +
+ ", token='" + token + '\'' +
+ ", claims=" + claims +
+ ", roles=" + roles +
+ '}';
+ }
+}
diff --git a/solr/core/src/test-files/solr/security/jwt_well-known-config.json b/solr/core/src/test-files/solr/security/jwt_well-known-config.json
new file mode 100644
index 000000000000..ea6d9f10bb38
--- /dev/null
+++ b/solr/core/src/test-files/solr/security/jwt_well-known-config.json
@@ -0,0 +1,84 @@
+{
+ "issuer":"http://acmepaymentscorp",
+ "authorization_endpoint":"http://acmepaymentscorp/oauth/auz/authorize",
+ "token_endpoint":"http://acmepaymentscorp/oauth/oauth20/token",
+ "userinfo_endpoint":"http://acmepaymentscorp/oauth/userinfo",
+ "jwks_uri":"https://acmepaymentscorp/oauth/jwks",
+ "scopes_supported":[
+ "READ",
+ "WRITE",
+ "DELETE",
+ "openid",
+ "scope",
+ "profile",
+ "email",
+ "address",
+ "phone"
+ ],
+ "response_types_supported":[
+ "code",
+ "code id_token",
+ "code token",
+ "code id_token token",
+ "token",
+ "id_token",
+ "id_token token"
+ ],
+ "grant_types_supported":[
+ "authorization_code",
+ "implicit",
+ "password",
+ "client_credentials",
+ "urn:ietf:params:oauth:grant-type:jwt-bearer"
+ ],
+ "subject_types_supported":[
+ "public"
+ ],
+ "id_token_signing_alg_values_supported":[
+ "HS256",
+ "HS384",
+ "HS512",
+ "RS256",
+ "RS384",
+ "RS512",
+ "ES256",
+ "ES384",
+ "ES512",
+ "PS256",
+ "PS384",
+ "PS512"
+ ],
+ "id_token_encryption_alg_values_supported":[
+ "RSA1_5",
+ "RSA-OAEP",
+ "RSA-OAEP-256",
+ "A128KW",
+ "A192KW",
+ "A256KW",
+ "A128GCMKW",
+ "A192GCMKW",
+ "A256GCMKW",
+ "dir"
+ ],
+ "id_token_encryption_enc_values_supported":[
+ "A128CBC-HS256",
+ "A192CBC-HS384",
+ "A256CBC-HS512",
+ "A128GCM",
+ "A192GCM",
+ "A256GCM"
+ ],
+ "token_endpoint_auth_methods_supported":[
+ "client_secret_post",
+ "client_secret_basic",
+ "client_secret_jwt",
+ "private_key_jwt"
+ ],
+ "token_endpoint_auth_signing_alg_values_supported":[
+ "HS256",
+ "RS256"
+ ],
+ "claims_parameter_supported":false,
+ "request_parameter_supported":false,
+ "request_uri_parameter_supported":false
+}
\ No newline at end of file
diff --git a/solr/core/src/test/org/apache/solr/security/JWTAuthPluginTest.java b/solr/core/src/test/org/apache/solr/security/JWTAuthPluginTest.java
index 725855176d62..8e15cd36e466 100644
--- a/solr/core/src/test/org/apache/solr/security/JWTAuthPluginTest.java
+++ b/solr/core/src/test/org/apache/solr/security/JWTAuthPluginTest.java
@@ -16,7 +16,9 @@
*/
package org.apache.solr.security;
+import java.io.IOException;
import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.Principal;
@@ -27,9 +29,10 @@
import java.util.Map;
import java.util.Set;
+import org.apache.commons.lang3.StringUtils;
import org.apache.solr.SolrTestCaseJ4;
+import org.apache.solr.common.SolrException;
import org.apache.solr.common.util.Utils;
-import org.apache.solr.security.JWTAuthPlugin.AuthenticationResponse;
import org.jose4j.jwk.RsaJsonWebKey;
import org.jose4j.jwk.RsaJwkGenerator;
import org.jose4j.jws.AlgorithmIdentifiers;
@@ -41,8 +44,9 @@
import org.junit.BeforeClass;
import org.junit.Test;
-import static org.apache.solr.security.JWTAuthPlugin.AuthenticationResponse.AuthCode.AUTZ_HEADER_PROBLEM;
-import static org.apache.solr.security.JWTAuthPlugin.AuthenticationResponse.AuthCode.NO_AUTZ_HEADER;
+import static org.apache.solr.security.JWTAuthPlugin.JWTAuthenticationResponse.AuthCode.AUTZ_HEADER_PROBLEM;
+import static org.apache.solr.security.JWTAuthPlugin.JWTAuthenticationResponse.AuthCode.NO_AUTZ_HEADER;
+import static org.apache.solr.security.JWTAuthPlugin.JWTAuthenticationResponse.AuthCode.SCOPE_MISSING;
public class JWTAuthPluginTest extends SolrTestCaseJ4 {
private static String testHeader;
@@ -51,8 +55,9 @@ public class JWTAuthPluginTest extends SolrTestCaseJ4 {
private HashMap testJwk;
private static RsaJsonWebKey rsaJsonWebKey;
private HashMap testConfig;
+ private HashMap minimalConfig;
+
-
@BeforeClass
public static void beforeAll() throws Exception {
// Generate an RSA key pair, which will be used for signing and verification of the JWT, wrapped in a JWK
@@ -86,6 +91,7 @@ static JwtClaims generateClaims() {
claims.setIssuedAtToNow(); // when the token was issued/created (now)
claims.setNotBeforeMinutesInThePast(2); // time before which the token is not yet valid (2 minutes ago)
claims.setSubject("solruser"); // the subject/principal is whom the token is about
+ claims.setStringClaim("scope", "solr:read");
claims.setClaim("name", "Solr User"); // additional claims/attributes about the subject can be added
claims.setClaim("customPrincipal", "custom"); // additional claims/attributes about the subject can be added
claims.setClaim("claim1", "foo"); // additional claims/attributes about the subject can be added
@@ -115,6 +121,9 @@ public void setUp() throws Exception {
testConfig.put("class", "org.apache.solr.security.JWTAuthPlugin");
testConfig.put("jwk", testJwk);
plugin.init(testConfig);
+
+ minimalConfig = new HashMap<>();
+ minimalConfig.put("class", "org.apache.solr.security.JWTAuthPlugin");
}
@Override
@@ -126,8 +135,7 @@ public void tearDown() throws Exception {
@Test
public void initWithoutRequired() {
- HashMap authConf = new HashMap<>();
- plugin.init(authConf);
+ plugin.init(testConfig);
assertEquals(AUTZ_HEADER_PROBLEM, plugin.authenticate("foo").getAuthCode());
}
@@ -148,8 +156,8 @@ public void initFromSecurityJSONUrlJwk() throws Exception {
Map authConf = (Map) securityConf.get("authentication");
plugin.init(authConf);
- AuthenticationResponse resp = plugin.authenticate(testHeader);
- assertEquals(AuthenticationResponse.AuthCode.JWT_VALIDATION_EXCEPTION, resp.getAuthCode());
+ JWTAuthPlugin.JWTAuthenticationResponse resp = plugin.authenticate(testHeader);
+ assertEquals(JWTAuthPlugin.JWTAuthenticationResponse.AuthCode.JWT_VALIDATION_EXCEPTION, resp.getAuthCode());
assertTrue(resp.getJwtException().getMessage().contains("Connection refused"));
}
@@ -182,7 +190,7 @@ public void parseJwkSet() throws Exception {
@Test
public void authenticateOk() {
- AuthenticationResponse resp = plugin.authenticate(testHeader);
+ JWTAuthPlugin.JWTAuthenticationResponse resp = plugin.authenticate(testHeader);
assertTrue(resp.isAuthenticated());
assertEquals("solruser", resp.getPrincipal().getName());
}
@@ -191,9 +199,9 @@ public void authenticateOk() {
public void authFailedMissingSubject() {
testConfig.put("iss", "NA");
plugin.init(testConfig);
- AuthenticationResponse resp = plugin.authenticate(testHeader);
+ JWTAuthPlugin.JWTAuthenticationResponse resp = plugin.authenticate(testHeader);
assertFalse(resp.isAuthenticated());
- assertEquals(AuthenticationResponse.AuthCode.JWT_VALIDATION_EXCEPTION, resp.getAuthCode());
+ assertEquals(JWTAuthPlugin.JWTAuthenticationResponse.AuthCode.JWT_VALIDATION_EXCEPTION, resp.getAuthCode());
testConfig.put("iss", "IDServer");
plugin.init(testConfig);
@@ -205,9 +213,9 @@ public void authFailedMissingSubject() {
public void authFailedMissingAudience() {
testConfig.put("aud", "NA");
plugin.init(testConfig);
- AuthenticationResponse resp = plugin.authenticate(testHeader);
+ JWTAuthPlugin.JWTAuthenticationResponse resp = plugin.authenticate(testHeader);
assertFalse(resp.isAuthenticated());
- assertEquals(AuthenticationResponse.AuthCode.JWT_VALIDATION_EXCEPTION, resp.getAuthCode());
+ assertEquals(JWTAuthPlugin.JWTAuthenticationResponse.AuthCode.JWT_VALIDATION_EXCEPTION, resp.getAuthCode());
testConfig.put("aud", "Solr");
plugin.init(testConfig);
@@ -219,14 +227,14 @@ public void authFailedMissingAudience() {
public void authFailedMissingPrincipal() {
testConfig.put("principal_claim", "customPrincipal");
plugin.init(testConfig);
- AuthenticationResponse resp = plugin.authenticate(testHeader);
+ JWTAuthPlugin.JWTAuthenticationResponse resp = plugin.authenticate(testHeader);
assertTrue(resp.isAuthenticated());
testConfig.put("principal_claim", "NA");
plugin.init(testConfig);
resp = plugin.authenticate(testHeader);
assertFalse(resp.isAuthenticated());
- assertEquals(AuthenticationResponse.AuthCode.PRINCIPAL_MISSING, resp.getAuthCode());
+ assertEquals(JWTAuthPlugin.JWTAuthenticationResponse.AuthCode.PRINCIPAL_MISSING, resp.getAuthCode());
}
@Test
@@ -238,7 +246,7 @@ public void claimMatch() {
shouldMatch.put("claim3", "f\\w{2}$");
testConfig.put("claims_match", shouldMatch);
plugin.init(testConfig);
- AuthenticationResponse resp = plugin.authenticate(testHeader);
+ JWTAuthPlugin.JWTAuthenticationResponse resp = plugin.authenticate(testHeader);
assertTrue(resp.isAuthenticated());
// Required claim does not exist
@@ -246,13 +254,13 @@ public void claimMatch() {
shouldMatch.put("claim9", "NA");
plugin.init(testConfig);
resp = plugin.authenticate(testHeader);
- assertEquals(AuthenticationResponse.AuthCode.CLAIM_MISMATCH, resp.getAuthCode());
+ assertEquals(JWTAuthPlugin.JWTAuthenticationResponse.AuthCode.CLAIM_MISMATCH, resp.getAuthCode());
// Required claim does not match regex
shouldMatch.clear();
shouldMatch.put("claim1", "NA");
resp = plugin.authenticate(testHeader);
- assertEquals(AuthenticationResponse.AuthCode.CLAIM_MISMATCH, resp.getAuthCode());
+ assertEquals(JWTAuthPlugin.JWTAuthenticationResponse.AuthCode.CLAIM_MISMATCH, resp.getAuthCode());
}
@Test
@@ -260,56 +268,60 @@ public void missingIssAudExp() {
testConfig.put("require_exp", "false");
testConfig.put("require_sub", "false");
plugin.init(testConfig);
- AuthenticationResponse resp = plugin.authenticate(slimHeader);
+ JWTAuthPlugin.JWTAuthenticationResponse resp = plugin.authenticate(slimHeader);
assertTrue(resp.isAuthenticated());
// Missing exp header
testConfig.put("require_exp", true);
plugin.init(testConfig);
resp = plugin.authenticate(slimHeader);
- assertEquals(AuthenticationResponse.AuthCode.JWT_VALIDATION_EXCEPTION, resp.getAuthCode());
+ assertEquals(JWTAuthPlugin.JWTAuthenticationResponse.AuthCode.JWT_VALIDATION_EXCEPTION, resp.getAuthCode());
// Missing sub header
testConfig.put("require_sub", true);
plugin.init(testConfig);
resp = plugin.authenticate(slimHeader);
- assertEquals(AuthenticationResponse.AuthCode.JWT_VALIDATION_EXCEPTION, resp.getAuthCode());
+ assertEquals(JWTAuthPlugin.JWTAuthenticationResponse.AuthCode.JWT_VALIDATION_EXCEPTION, resp.getAuthCode());
}
@Test
public void algWhitelist() {
testConfig.put("alg_whitelist", Arrays.asList("PS384", "PS512"));
plugin.init(testConfig);
- AuthenticationResponse resp = plugin.authenticate(testHeader);
- assertEquals(AuthenticationResponse.AuthCode.JWT_VALIDATION_EXCEPTION, resp.getAuthCode());
+ JWTAuthPlugin.JWTAuthenticationResponse resp = plugin.authenticate(testHeader);
+ assertEquals(JWTAuthPlugin.JWTAuthenticationResponse.AuthCode.JWT_VALIDATION_EXCEPTION, resp.getAuthCode());
assertTrue(resp.getErrorMessage().contains("not a whitelisted"));
}
@Test
- public void roles() {
- testConfig.put("roles_claim", "groups");
+ public void scope() {
+ testConfig.put("scope", "solr:read solr:admin");
plugin.init(testConfig);
- AuthenticationResponse resp = plugin.authenticate(testHeader);
+ JWTAuthPlugin.JWTAuthenticationResponse resp = plugin.authenticate(testHeader);
assertTrue(resp.isAuthenticated());
Principal principal = resp.getPrincipal();
assertTrue(principal instanceof VerifiedUserRoles);
Set roles = ((VerifiedUserRoles)principal).getVerifiedRoles();
- assertEquals(3, roles.size());
- assertTrue(roles.contains("group-one"));
+ assertEquals(1, roles.size());
+ assertTrue(roles.contains("solr:read"));
+ }
- // Wrong claim ID
- testConfig.put("roles_claim", "NA");
+ @Test
+ public void wrongScope() {
+ testConfig.put("scope", "wrong");
plugin.init(testConfig);
- resp = plugin.authenticate(testHeader);
- assertEquals(AuthenticationResponse.AuthCode.CLAIM_MISMATCH, resp.getAuthCode());
+ JWTAuthPlugin.JWTAuthenticationResponse resp = plugin.authenticate(testHeader);
+ assertFalse(resp.isAuthenticated());
+ assertNull(resp.getPrincipal());
+ assertEquals(SCOPE_MISSING, resp.getAuthCode());
}
-
+
@Test
public void noHeaderBlockUnknown() {
testConfig.put("block_unknown", true);
plugin.init(testConfig);
- AuthenticationResponse resp = plugin.authenticate(null);
+ JWTAuthPlugin.JWTAuthenticationResponse resp = plugin.authenticate(null);
assertEquals(NO_AUTZ_HEADER, resp.getAuthCode());
}
@@ -317,7 +329,60 @@ public void noHeaderBlockUnknown() {
public void noHeaderNotBlockUnknown() {
testConfig.put("block_unknown", false);
plugin.init(testConfig);
- AuthenticationResponse resp = plugin.authenticate(null);
- assertEquals(AuthenticationResponse.AuthCode.PASS_THROUGH, resp.getAuthCode());
+ JWTAuthPlugin.JWTAuthenticationResponse resp = plugin.authenticate(null);
+ assertEquals(JWTAuthPlugin.JWTAuthenticationResponse.AuthCode.PASS_THROUGH, resp.getAuthCode());
+ }
+
+ @Test
+ public void minimalConfigPassThrough() {
+ testConfig.put("block_unknown", false);
+ plugin.init(testConfig);
+ JWTAuthPlugin.JWTAuthenticationResponse resp = plugin.authenticate(null);
+ assertEquals(JWTAuthPlugin.JWTAuthenticationResponse.AuthCode.PASS_THROUGH, resp.getAuthCode());
+ }
+
+ @Test
+ public void wellKnownConfig() throws IOException {
+ String wellKnownUrl = TEST_PATH().resolve("security").resolve("jwt_well-known-config.json").toAbsolutePath().toUri().toString();
+ testConfig.put("well_known_url", wellKnownUrl);
+ testConfig.remove("jwk");
+ plugin.init(testConfig);
+ JWTAuthPlugin.JWTAuthenticationResponse resp = plugin.authenticate(null);
+ assertEquals(JWTAuthPlugin.JWTAuthenticationResponse.AuthCode.PASS_THROUGH, resp.getAuthCode());
+ }
+
+ @Test(expected = SolrException.class)
+ public void onlyOneJwkConfig() throws IOException {
+ testConfig.put("jwk_url", "http://127.0.0.1:45678/.well-known/config");
+ plugin.init(testConfig);
+ }
+
+ @Test(expected = SolrException.class)
+ public void wellKnownConfigNotHttps() throws IOException {
+ testConfig.put("well_known_url", "http://127.0.0.1:45678/.well-known/config");
+ plugin.init(testConfig);
+ }
+
+ @Test(expected = SolrException.class)
+ public void wellKnownConfigNotReachable() {
+ testConfig.put("well_known_url", "https://127.0.0.1:45678/.well-known/config");
+ plugin.init(testConfig);
+ }
+
+ @Test
+ public void wellKnownConfigFromInputstream() throws IOException {
+ Path configJson = TEST_PATH().resolve("security").resolve("jwt_well-known-config.json");
+ JWTAuthPlugin.OidcDiscoveryConfig config = JWTAuthPlugin.OidcDiscoveryConfig.parse(Files.newInputStream(configJson));
+ assertEquals("https://acmepaymentscorp/oauth/jwks", config.getJwksUrl());
+ }
+
+ @Test
+ public void wellKnownConfigFromString() throws IOException {
+ Path configJson = TEST_PATH().resolve("security").resolve("jwt_well-known-config.json");
+ String configString = StringUtils.join(Files.readAllLines(configJson), "\n");
+ JWTAuthPlugin.OidcDiscoveryConfig config = JWTAuthPlugin.OidcDiscoveryConfig.parse(configString, StandardCharsets.UTF_8);
+ assertEquals("https://acmepaymentscorp/oauth/jwks", config.getJwksUrl());
+ assertEquals("http://acmepaymentscorp", config.getIssuer());
+ assertEquals("http://acmepaymentscorp/oauth/auz/authorize", config.getAuthorizationEndpoint());
}
}
\ No newline at end of file
diff --git a/solr/solr-ref-guide/src/jwt-authentication-plugin.adoc b/solr/solr-ref-guide/src/jwt-authentication-plugin.adoc
index cb78aef3df1d..6214b3b9dc6f 100644
--- a/solr/solr-ref-guide/src/jwt-authentication-plugin.adoc
+++ b/solr/solr-ref-guide/src/jwt-authentication-plugin.adoc
@@ -50,7 +50,22 @@ To start enforcing authentication for all users, requiring a valid JWT token in
}
----
-A more complex configuration:
+The next example shows configuring using https://openid.net/specs/openid-connect-discovery-1_0.html[OpenID Connect Discovery] with a well-konwn URI.
+
+[source,json]
+----
+{
+ "authentication": {
+ "class": "solr.JWTAuthPlugin",
+ "block_unknown": true,
+ "well_known_url": "https://my.key.server/.well-known/openid-configuration"
+ }
+}
+----
+
+In this case, `jwk_url` and `iss` will be automatically configured from the fetched configuration.
+
+A more complex configuration with static JWK:
[source,json]
----
@@ -64,27 +79,29 @@ A more complex configuration:
"kty": "RSA",
"n": "3ZF6wBGPMsLzsS1KLghxaVpZtXD3nTLzDm0c974i9-KNU_1rhhBeiVfS64VfEQmP8SA470jEy7yWcvnz9GvG-YAlm9iOwVF7jLl2awdws0ocFjdSPT3SjPQKzOeMO7G9XqNTkrvoFCn1YAi26fbhhcqkwZDoeTcHQdRN32frzccuPhZrwImApIedroKLlKWv2IvPDnz2Bpe2WWVc2HdoWYqEVD3p_BEy8f-RTSHK3_8kDDF9yAwI9jx7CK1_C-eYxXltm-6rpS5NGyFm0UNTZMxVU28Tl7LX8Vb6CikyCQ9YRCtk_CvpKWmEuKEp9I28KHQNmGkDYT90nt3vjbCXxw"
},
- "iss": "acme", <4>
- "aud": "solr", <5>
- "principal_claim": "solruid", <6>
- "claims_match": { "roles" : "admin|search", "dept" : "IT" }, <7>
- "roles_claim": "roles", <8>
- "alg_whitelist" : [ "RS256", "RS384", "RS512" ] <9>
+ "client_id": "solr-client-12345", <4>
+ "iss": "https://example.com/idp", <5>
+ "aud": "https://example.com/solr", <6>
+ "principal_claim": "solruid", <7>
+ "claims_match": { "roles" : "admin|search", "dept" : "IT" }, <8>
+ "scope": "solr:read solr:write solr:admin", <9>
+ "alg_whitelist" : [ "RS256", "RS384", "RS512" ] <10>
}
}
----
-Let's comment on the config:
+Let's comment on this config:
<1> Plugin class
<2> Make sure to block anyone without a valid token
<3> Here we pass the JWK inline instead of referring to a URL with `jwk_url`
-<4> The issuer claim must match "acme"
-<5> The audience claim must match "solr"
-<6> Fetch the user id from another claim than the default `sub`
-<7> Require that the `roles` claim is one of "admin" or "search" and that the `dept` claim is "IT"
-<8> Fetch user's roles from the "roles" claim and pass this on the request for use in Authorization
-<9> Only accept RSA algorithms for signatures
+<4> Set the client id registered with Identiy Provider
+<5> The issuer claim must match "https://example.com/idp"
+<6> The audience claim must match "https://example.com/solr"
+<7> Fetch the user id from another claim than the default `sub`
+<8> Require that the `roles` claim is one of "admin" or "search" and that the `dept` claim is "IT"
+<9> Require one of the scopes `solr:read`, `solr:write` or `solr:amin`
+<10> Only accept RSA algorithms for signatures
== Configuration parameters
@@ -92,17 +109,19 @@ Let's comment on the config:
|===
Key ; Description ; Default
block_unknown ; Set to `false` in order to pass requests from unknown users without a token ; `true`
-jwk_url ; An https URL to a https://mkjwk.org[JWK] keys file ;
-jwk ; As an alternative to `jwk_url` you may provide a JSON object here containing the public key(s) of the issuer. See https://mkjwk.org for an online JWK generator ;
-iss ; Validates that `iss` (issuer) equals this string ; (`iss` not required)
-aud ; Validates that `aud` (audience) equals this string ; (`aud` not required)
+well_known_url ; URL to an https://openid.net/specs/openid-connect-discovery-1_0.html[OpenID Connect Discovery] endpoint ;
+client_id ; Client identifier for use with OpenID Connect ;
+scope ; Whitespace separate list of valid scopes. If configured, the JWT access token MUST contain a `scope` claim with at least one of the listed scopes. Example: `solr:read solr:admin` ;
+jwk_url ; An https URL to a https://tools.ietf.org/html/rfc7517[JWK] keys file. Not required if `well_known_url` is provided ; Auto configured if `well_known_url` is provided
+jwk ; As an alternative to `jwk_url` you may provide a JSON object here containing the public key(s) of the issuer. ; Auto configured if `well_known_url` is provided
+iss ; Validates that `iss` (issuer) equals this string ; If `well_known_url` is provided, use `issuer` from there.
+aud ; Validates that `aud` (audience) equals this string ; If `client_id` is configured, require `aud` to match it
require_sub ; Makes `sub` (subject) mandatory ; `true`
require_exp ; Makes `exp` (expiry time) mandatory ; `true`
-alg_whitelist ; JSON array with algorithms to accept: `HS256`, `HS384`, `HS512`, `RS256`, `RS384`, `RS512`, `ES256`, `ES384`, `ES512`, `PS256`, `PS384`, `PS512`, `none ; (any)
-jwk_cache_dur ; Duration of JWK cache in seconds ; 3600 (1h)
+alg_whitelist ; JSON array with algorithms to accept: `HS256`, `HS384`, `HS512`, `RS256`, `RS384`, `RS512`, `ES256`, `ES384`, `ES512`, `PS256`, `PS384`, `PS512`, `none ;
+jwk_cache_dur ; Duration of JWK cache in seconds ; `3600` (1 hour)
principal_claim ; What claim id to pull principal from ; `sub`
-roles_claim ; What claim id to pull roles from. If specified, all requests MUST supply this claim ;
-claims_match ; JSON object of claims (key) that must match a regular expression (value). ; (none)
+claims_match ; JSON object of claims (key) that must match a regular expression (value). Example: `{ "roles" : "A|B" }` will require the `roles` claim to be either "A" or "B". ; (none)
|===
From 3e72685339728058d9d2a6b3e078e67014f2535c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20H=C3=B8ydahl?=
Date: Wed, 5 Sep 2018 15:07:35 +0200
Subject: [PATCH 14/88] Fix some precommit stuff
---
.../apache/solr/security/JWTAuthPlugin.java | 6 ++--
.../JWTAuthPluginIntegrationTest.java | 32 +++++++++----------
.../src/jwt-authentication-plugin.adoc | 4 +--
3 files changed, 21 insertions(+), 21 deletions(-)
diff --git a/solr/core/src/java/org/apache/solr/security/JWTAuthPlugin.java b/solr/core/src/java/org/apache/solr/security/JWTAuthPlugin.java
index 20a0de159319..38f6fde9fdcb 100644
--- a/solr/core/src/java/org/apache/solr/security/JWTAuthPlugin.java
+++ b/solr/core/src/java/org/apache/solr/security/JWTAuthPlugin.java
@@ -371,10 +371,10 @@ protected JWTAuthenticationResponse authenticate(String authorizationHeader) {
return new JWTAuthenticationResponse(AuthCode.SCOPE_MISSING, "'scope' claim does not contain any of the required scopes: " + requiredScopes);
}
}
- final Set roles = new HashSet<>(scopes);
- roles.remove("openid"); // Remove standard claims
+ final Set finalScopes = new HashSet<>(scopes);
+ finalScopes.remove("openid"); // Remove standard scope
// Pass scopes with principal to signal to any Authorization plugins that user has some verified role claims
- return new JWTAuthenticationResponse(AuthCode.AUTHENTICATED, new JWTPrincipalWithUserRoles(principal, jwtCompact, jwtClaims.getClaimsMap(), roles));
+ return new JWTAuthenticationResponse(AuthCode.AUTHENTICATED, new JWTPrincipalWithUserRoles(principal, jwtCompact, jwtClaims.getClaimsMap(), finalScopes));
} else {
return new JWTAuthenticationResponse(AuthCode.AUTHENTICATED, new JWTPrincipal(principal, jwtCompact, jwtClaims.getClaimsMap()));
}
diff --git a/solr/core/src/test/org/apache/solr/security/JWTAuthPluginIntegrationTest.java b/solr/core/src/test/org/apache/solr/security/JWTAuthPluginIntegrationTest.java
index 63c24f70c004..5b106ead669a 100644
--- a/solr/core/src/test/org/apache/solr/security/JWTAuthPluginIntegrationTest.java
+++ b/solr/core/src/test/org/apache/solr/security/JWTAuthPluginIntegrationTest.java
@@ -75,7 +75,7 @@ public static void setupClass() throws Exception {
" \"alg\": \"RS256\",\n" +
" \"n\": \"jeyrvOaZrmKWjyNXt0myAc_pJ1hNt3aRupExJEx1ewPaL9J9HFgSCjMrYxCB1ETO1NDyZ3nSgjZis-jHHDqBxBjRdq_t1E2rkGFaYbxAyKt220Pwgme_SFTB9MXVrFQGkKyjmQeVmOmV6zM3KK8uMdKQJ4aoKmwBcF5Zg7EZdDcKOFgpgva1Jq-FlEsaJ2xrYDYo3KnGcOHIt9_0NQeLsqZbeWYLxYni7uROFncXYV5FhSJCeR4A_rrbwlaCydGxE0ToC_9HNYibUHlkJjqyUhAgORCbNS8JLCJH8NUi5sDdIawK9GTSyvsJXZ-QHqo4cMUuxWV5AJtaRGghuMUfqQ\"\n" +
"}";
-
+
PublicJsonWebKey jwk = RsaJsonWebKey.Factory.newPublicJwk(jwkJSON);
JwtClaims claims = JWTAuthPluginTest.generateClaims();
JsonWebSignature jws = new JsonWebSignature();
@@ -84,7 +84,7 @@ public static void setupClass() throws Exception {
jws.setKeyIdHeaderValue(jwk.getKeyId());
jws.setAlgorithmHeaderValue(AlgorithmIdentifiers.RSA_USING_SHA256);
- jwtTestToken = jws.getCompactSerialization();
+ jwtTestToken = jws.getCompactSerialization();
HttpClientUtil.removeRequestInterceptor(interceptor);
HttpClientUtil.addRequestInterceptor(interceptor);
@@ -101,7 +101,7 @@ public void before() {
jwtInterceptCount.set(0);
pkiInterceptCount.set(0);
}
-
+
@Test(expected = IOException.class)
public void infoRequestWithoutToken() throws Exception {
get(baseUrl + "admin/info/system", null);
@@ -120,7 +120,7 @@ public void createCollectionAndQueryDistributed() throws Exception {
// Admin request will use PKI inter-node auth from Overseer, and succeed
assertEquals(200, get(baseUrl + "admin/collections?action=CREATE&name=mycoll&numShards=2", jwtTestToken).second().intValue());
verifyInterRequestHeaderCounts(0,0);
-
+
// First a non distributed query
Pair result = get(baseUrl + "mycoll/query?q=*:*&distrib=false", jwtTestToken);
assertEquals(Integer.valueOf(200), result.second());
@@ -130,7 +130,7 @@ public void createCollectionAndQueryDistributed() throws Exception {
result = get(baseUrl + "mycoll/query?q=*:*", jwtTestToken);
assertEquals(Integer.valueOf(200), result.second());
verifyInterRequestHeaderCounts(2,2);
-
+
// Delete
assertEquals(200, get(baseUrl + "admin/collections?action=DELETE&name=mycoll", jwtTestToken).second().intValue());
verifyInterRequestHeaderCounts(2,2);
@@ -140,7 +140,7 @@ public void createCollectionAndQueryDistributed() throws Exception {
public void createCollectionAndUpdateDistributed() throws Exception {
// Admin request will use PKI inter-node auth from Overseer, and succeed
assertEquals(200, get(baseUrl + "admin/collections?action=CREATE&name=mycoll&numShards=2", jwtTestToken).second().intValue());
-
+
// Now update two documents
Pair result = post(baseUrl + "mycoll/update?commit=true", "[{\"id\" : \"1\"}, {\"id\": \"2\"}, {\"id\": \"3\"}]", jwtTestToken);
assertEquals(Integer.valueOf(200), result.second());
@@ -186,11 +186,11 @@ public void createCollectionAndUpdateDistributed() throws Exception {
// result = get(baseUrl + "mycoll/query?q=*:*");
// System.out.println(result.first());
// assertEquals(Integer.valueOf(200), result.second());
-
-
+
+
// String collectionName = "jwtAuthTestColl";
- // create collection
+ // create collection
// CollectionAdminRequest.Create create = CollectionAdminRequest.createCollection(collectionName, "conf1",
// NUM_SHARDS, REPLICATION_FACTOR);
// create.process(solrClient);
@@ -224,7 +224,7 @@ private Pair get(String url, String token) throws IOException {
createConn.connect();
BufferedReader br2 = new BufferedReader(new InputStreamReader((InputStream) createConn.getContent()));
String result = br2.lines().collect(Collectors.joining("\n"));
- int code = createConn.getResponseCode();
+ int code = createConn.getResponseCode();
createConn.disconnect();
return new Pair<>(result, code);
}
@@ -242,15 +242,15 @@ private Pair post(String url, String json, String token) throws
os.write(json.getBytes());
os.flush();
os.close();
-
+
con.connect();
BufferedReader br2 = new BufferedReader(new InputStreamReader((InputStream) con.getContent()));
String result = br2.lines().collect(Collectors.joining("\n"));
- int code = con.getResponseCode();
+ int code = con.getResponseCode();
con.disconnect();
return new Pair<>(result, code);
}
-
+
// SolrHttpClientBuilder getHttpClientBuilder(SolrHttpClientBuilder builder, String token) {
// if (builder == null) {
// builder = SolrHttpClientBuilder.create();
@@ -268,17 +268,17 @@ private Pair post(String url, String json, String token) throws
// });
// return builder;
// }
-
+
private static class CountInterceptor implements HttpRequestInterceptor {
@Override
public void process(HttpRequest request, HttpContext context) throws HttpException, IOException {
Header ah = request.getFirstHeader(HttpHeaders.AUTHORIZATION);
if (ah != null && ah.getValue().startsWith("Bearer"))
jwtInterceptCount.addAndGet(1);
-
+
Header ph = request.getFirstHeader(PKIAuthenticationPlugin.HEADER);
if (ph != null)
pkiInterceptCount.addAndGet(1);
- }
+ }
}
}
diff --git a/solr/solr-ref-guide/src/jwt-authentication-plugin.adoc b/solr/solr-ref-guide/src/jwt-authentication-plugin.adoc
index 6214b3b9dc6f..6331676d118b 100644
--- a/solr/solr-ref-guide/src/jwt-authentication-plugin.adoc
+++ b/solr/solr-ref-guide/src/jwt-authentication-plugin.adoc
@@ -58,7 +58,7 @@ The next example shows configuring using https://openid.net/specs/openid-connect
"authentication": {
"class": "solr.JWTAuthPlugin",
"block_unknown": true,
- "well_known_url": "https://my.key.server/.well-known/openid-configuration"
+ "well_known_url": "https://idp.example.com/.well-known/openid-configuration"
}
}
----
@@ -121,7 +121,7 @@ require_exp ; Makes `exp` (expiry time) mandatory ; `tr
alg_whitelist ; JSON array with algorithms to accept: `HS256`, `HS384`, `HS512`, `RS256`, `RS384`, `RS512`, `ES256`, `ES384`, `ES512`, `PS256`, `PS384`, `PS512`, `none ;
jwk_cache_dur ; Duration of JWK cache in seconds ; `3600` (1 hour)
principal_claim ; What claim id to pull principal from ; `sub`
-claims_match ; JSON object of claims (key) that must match a regular expression (value). Example: `{ "roles" : "A|B" }` will require the `roles` claim to be either "A" or "B". ; (none)
+claims_match ; JSON object of claims (key) that must match a regular expression (value). Example: `{ "foo" : "A|B" }` will require the `foo` claim to be either "A" or "B". ; (none)
|===
From f24ada64b14e2277ea175ff938fc50b62b02c6a4 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20H=C3=B8ydahl?=
Date: Wed, 5 Sep 2018 16:18:58 +0200
Subject: [PATCH 15/88] Fix javadoc formatting
---
.../solr/security/JWTPrincipalWithUserRoles.java | 10 +++++-----
1 file changed, 5 insertions(+), 5 deletions(-)
diff --git a/solr/core/src/java/org/apache/solr/security/JWTPrincipalWithUserRoles.java b/solr/core/src/java/org/apache/solr/security/JWTPrincipalWithUserRoles.java
index 473f2f4f6e51..ecad497929c9 100644
--- a/solr/core/src/java/org/apache/solr/security/JWTPrincipalWithUserRoles.java
+++ b/solr/core/src/java/org/apache/solr/security/JWTPrincipalWithUserRoles.java
@@ -27,9 +27,9 @@
* JWT principal that contains username, token, claims and a list of roles the user has,
* so one can keep track of user-role mappings in an Identity Server external to Solr and
* pass the information to Solr in a signed JWT token. The role information can then be used to authorize
- * requests without the need to maintain or lookup what roles each user belongs to.
- */
- public class JWTPrincipalWithUserRoles extends JWTPrincipal implements VerifiedUserRoles {
+ * requests without the need to maintain or lookup what roles each user belongs to.
+ */
+public class JWTPrincipalWithUserRoles extends JWTPrincipal implements VerifiedUserRoles {
private final Set roles;
public JWTPrincipalWithUserRoles(final String username, String token, Map claims, Set roles) {
@@ -37,7 +37,7 @@ public JWTPrincipalWithUserRoles(final String username, String token, Map
Date: Wed, 5 Sep 2018 19:44:32 +0200
Subject: [PATCH 16/88] More getters with tests
---
.../apache/solr/security/JWTAuthPlugin.java | 34 ++++++++++++++-----
.../solr/security/JWTAuthPluginTest.java | 6 ++--
2 files changed, 29 insertions(+), 11 deletions(-)
diff --git a/solr/core/src/java/org/apache/solr/security/JWTAuthPlugin.java b/solr/core/src/java/org/apache/solr/security/JWTAuthPlugin.java
index 38f6fde9fdcb..0371e9ab1be8 100644
--- a/solr/core/src/java/org/apache/solr/security/JWTAuthPlugin.java
+++ b/solr/core/src/java/org/apache/solr/security/JWTAuthPlugin.java
@@ -112,7 +112,7 @@ public class JWTAuthPlugin extends AuthenticationPlugin implements HttpClientBui
private HashSet requiredScopes = new HashSet<>();
private String clientId;
private long jwkCacheDuration;
- private OidcDiscoveryConfig oidcDiscoveryConfig;
+ private WellKnownDiscoveryConfig oidcDiscoveryConfig;
private String confIdpConfigUrl;
private Map pluginConfig;
private Instant lastInitTime = Instant.now();
@@ -137,7 +137,7 @@ public void init(Map pluginConfig) {
if (confIdpConfigUrl != null) {
log.debug("Initializing well-known oidc config from {}", confIdpConfigUrl);
- oidcDiscoveryConfig = OidcDiscoveryConfig.parse(confIdpConfigUrl);
+ oidcDiscoveryConfig = WellKnownDiscoveryConfig.parse(confIdpConfigUrl);
iss = oidcDiscoveryConfig.getIssuer();
}
@@ -549,14 +549,14 @@ AuthCode getAuthCode() {
* Config object for a OpenId Connect well-known config
* Typically exposed through /.well-known/openid-configuration endpoint
*/
- public static class OidcDiscoveryConfig {
+ public static class WellKnownDiscoveryConfig {
private static Map securityConf;
- OidcDiscoveryConfig(Map securityConf) {
- OidcDiscoveryConfig.securityConf = securityConf;
+ WellKnownDiscoveryConfig(Map securityConf) {
+ WellKnownDiscoveryConfig.securityConf = securityConf;
}
- public static OidcDiscoveryConfig parse(String urlString) {
+ public static WellKnownDiscoveryConfig parse(String urlString) {
try {
URL url = new URL(urlString);
if (!Arrays.asList("https", "file").contains(url.getProtocol())) {
@@ -570,13 +570,13 @@ public static OidcDiscoveryConfig parse(String urlString) {
}
}
- public static OidcDiscoveryConfig parse(String json, Charset charset) {
+ public static WellKnownDiscoveryConfig parse(String json, Charset charset) {
return parse(new ByteArrayInputStream(json.getBytes(charset)));
}
- public static OidcDiscoveryConfig parse(InputStream configStream) {
+ public static WellKnownDiscoveryConfig parse(InputStream configStream) {
securityConf = (Map) Utils.fromJSON(configStream);
- return new OidcDiscoveryConfig(securityConf);
+ return new WellKnownDiscoveryConfig(securityConf);
}
@@ -591,6 +591,22 @@ public String getIssuer() {
public String getAuthorizationEndpoint() {
return (String) securityConf.get("authorization_endpoint");
}
+
+ public String getUserInfoEndpoint() {
+ return (String) securityConf.get("userinfo_endpoint");
+ }
+
+ public String getTokenEndpoint() {
+ return (String) securityConf.get("token_endpoint");
+ }
+
+ public List getScopesSupported() {
+ return (List) securityConf.get("scopes_supported");
+ }
+
+ public List getResponseTypesSupported() {
+ return (List) securityConf.get("response_types_supported");
+ }
}
/**
diff --git a/solr/core/src/test/org/apache/solr/security/JWTAuthPluginTest.java b/solr/core/src/test/org/apache/solr/security/JWTAuthPluginTest.java
index 8e15cd36e466..f8287531fc2f 100644
--- a/solr/core/src/test/org/apache/solr/security/JWTAuthPluginTest.java
+++ b/solr/core/src/test/org/apache/solr/security/JWTAuthPluginTest.java
@@ -372,7 +372,7 @@ public void wellKnownConfigNotReachable() {
@Test
public void wellKnownConfigFromInputstream() throws IOException {
Path configJson = TEST_PATH().resolve("security").resolve("jwt_well-known-config.json");
- JWTAuthPlugin.OidcDiscoveryConfig config = JWTAuthPlugin.OidcDiscoveryConfig.parse(Files.newInputStream(configJson));
+ JWTAuthPlugin.WellKnownDiscoveryConfig config = JWTAuthPlugin.WellKnownDiscoveryConfig.parse(Files.newInputStream(configJson));
assertEquals("https://acmepaymentscorp/oauth/jwks", config.getJwksUrl());
}
@@ -380,9 +380,11 @@ public void wellKnownConfigFromInputstream() throws IOException {
public void wellKnownConfigFromString() throws IOException {
Path configJson = TEST_PATH().resolve("security").resolve("jwt_well-known-config.json");
String configString = StringUtils.join(Files.readAllLines(configJson), "\n");
- JWTAuthPlugin.OidcDiscoveryConfig config = JWTAuthPlugin.OidcDiscoveryConfig.parse(configString, StandardCharsets.UTF_8);
+ JWTAuthPlugin.WellKnownDiscoveryConfig config = JWTAuthPlugin.WellKnownDiscoveryConfig.parse(configString, StandardCharsets.UTF_8);
assertEquals("https://acmepaymentscorp/oauth/jwks", config.getJwksUrl());
assertEquals("http://acmepaymentscorp", config.getIssuer());
assertEquals("http://acmepaymentscorp/oauth/auz/authorize", config.getAuthorizationEndpoint());
+ assertEquals(Arrays.asList("READ", "WRITE", "DELETE", "openid", "scope", "profile", "email", "address", "phone"), config.getScopesSupported());
+ assertEquals(Arrays.asList("code", "code id_token", "code token", "code id_token token", "token", "id_token", "id_token token"), config.getResponseTypesSupported());
}
}
\ No newline at end of file
From 2ed173f0ffc5971db249be269f0989c12ebfbd98 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20H=C3=B8ydahl?=
Date: Wed, 5 Sep 2018 20:43:33 +0200
Subject: [PATCH 17/88] Cleanup integration test
---
.../JWTAuthPluginIntegrationTest.java | 122 +++---------------
1 file changed, 18 insertions(+), 104 deletions(-)
diff --git a/solr/core/src/test/org/apache/solr/security/JWTAuthPluginIntegrationTest.java b/solr/core/src/test/org/apache/solr/security/JWTAuthPluginIntegrationTest.java
index 5b106ead669a..d2f35babc138 100644
--- a/solr/core/src/test/org/apache/solr/security/JWTAuthPluginIntegrationTest.java
+++ b/solr/core/src/test/org/apache/solr/security/JWTAuthPluginIntegrationTest.java
@@ -36,7 +36,6 @@
import org.apache.solr.client.solrj.impl.HttpClientUtil;
import org.apache.solr.cloud.SolrCloudTestCase;
import org.apache.solr.common.util.Pair;
-import org.apache.solr.util.LogLevel;
import org.jose4j.jwk.PublicJsonWebKey;
import org.jose4j.jwk.RsaJsonWebKey;
import org.jose4j.jws.AlgorithmIdentifiers;
@@ -47,6 +46,10 @@
import org.junit.BeforeClass;
import org.junit.Test;
+/**
+ * Validate that JWT token authentication works in a real cluster.
+ * TODO: Test also using SolrJ as client. But that requires a way to set Authorization header on request
+ */
public class JWTAuthPluginIntegrationTest extends SolrCloudTestCase {
protected static final int NUM_SERVERS = 2;
protected static final int NUM_SHARDS = 2;
@@ -88,6 +91,8 @@ public static void setupClass() throws Exception {
HttpClientUtil.removeRequestInterceptor(interceptor);
HttpClientUtil.addRequestInterceptor(interceptor);
+
+ cluster.waitForAllNodes(10);
}
@AfterClass
@@ -97,7 +102,7 @@ public static void tearDownClass() throws Exception {
}
@Before
- public void before() {
+ public void before() throws IOException, InterruptedException {
jwtInterceptCount.set(0);
pkiInterceptCount.set(0);
}
@@ -115,102 +120,30 @@ public void infoRequestWithToken() throws IOException {
}
@Test
- @LogLevel("org.apache.solr.security=DEBUG")
- public void createCollectionAndQueryDistributed() throws Exception {
+ public void createCollectionUpdateAndQueryDistributed() throws Exception {
// Admin request will use PKI inter-node auth from Overseer, and succeed
assertEquals(200, get(baseUrl + "admin/collections?action=CREATE&name=mycoll&numShards=2", jwtTestToken).second().intValue());
- verifyInterRequestHeaderCounts(0,0);
-
- // First a non distributed query
- Pair result = get(baseUrl + "mycoll/query?q=*:*&distrib=false", jwtTestToken);
- assertEquals(Integer.valueOf(200), result.second());
- verifyInterRequestHeaderCounts(0,0);
- // Now do a distributed query, using JWTAUth for inter-node
- result = get(baseUrl + "mycoll/query?q=*:*", jwtTestToken);
+ // Now update two documents
+ Pair result = post(baseUrl + "mycoll/update?commit=true", "[{\"id\" : \"1\"}, {\"id\": \"2\"}, {\"id\": \"3\"}]", jwtTestToken);
assertEquals(Integer.valueOf(200), result.second());
verifyInterRequestHeaderCounts(2,2);
- // Delete
- assertEquals(200, get(baseUrl + "admin/collections?action=DELETE&name=mycoll", jwtTestToken).second().intValue());
+ // First a non distributed query
+ result = get(baseUrl + "mycoll/query?q=*:*&distrib=false", jwtTestToken);
+ assertEquals(Integer.valueOf(200), result.second());
verifyInterRequestHeaderCounts(2,2);
- }
-
- @Test
- public void createCollectionAndUpdateDistributed() throws Exception {
- // Admin request will use PKI inter-node auth from Overseer, and succeed
- assertEquals(200, get(baseUrl + "admin/collections?action=CREATE&name=mycoll&numShards=2", jwtTestToken).second().intValue());
- // Now update two documents
- Pair result = post(baseUrl + "mycoll/update?commit=true", "[{\"id\" : \"1\"}, {\"id\": \"2\"}, {\"id\": \"3\"}]", jwtTestToken);
+ // Now do a distributed query, using JWTAUth for inter-node
+ result = get(baseUrl + "mycoll/query?q=*:*", jwtTestToken);
assertEquals(Integer.valueOf(200), result.second());
-
+ verifyInterRequestHeaderCounts(6,6);
+
// Delete
assertEquals(200, get(baseUrl + "admin/collections?action=DELETE&name=mycoll", jwtTestToken).second().intValue());
- verifyInterRequestHeaderCounts(2,2);
+ verifyInterRequestHeaderCounts(6,6);
}
-
-
-// NOCOMMIT: Test using SolrJ as client
-// private void testCollectionCreateSearchDelete(boolean enableJwt) throws Exception {
-// if (enableJwt) {
-// HttpClientUtil.setHttpClientBuilder(getHttpClientBuilder(HttpClientUtil.getHttpClientBuilder(), testJwt));
-// } else {
-// HttpClientUtil.resetHttpClientBuilder();
-// }
-
-// ClusterStateProvider csProv = cluster.getSolrClient().getClusterStateProvider();
-// CloudSolrClientBuilder builder = new CloudSolrClientBuilder(csProv);
-// CloudSolrClient solrClient = builder.build();
-
-// CloudSolrClient solrClient = cluster.getSolrClient();
-
-// URL solrurl = new URL(baseUrl + "admin/info/system");
-// HttpURLConnection conn = (HttpURLConnection) solrurl.openConnection();
-// conn.setRequestProperty("Authorization", "Bearer " + testJwt);
-// conn.connect();
-// BufferedReader br = new BufferedReader(new InputStreamReader((InputStream) conn.getContent()));
-// br.lines().forEach(l -> {
-// System.out.println(l);
-// });
-// conn.disconnect();
-
-
-// Pair result;
-//
-// result = get(baseUrl + "admin/collections?action=CREATE&name=mycoll&numShards=2");
-// System.out.println(result.first());
-// assertEquals(Integer.valueOf(200), result.second());
-//
-// result = get(baseUrl + "mycoll/query?q=*:*");
-// System.out.println(result.first());
-// assertEquals(Integer.valueOf(200), result.second());
-
-
-// String collectionName = "jwtAuthTestColl";
-
- // create collection
-// CollectionAdminRequest.Create create = CollectionAdminRequest.createCollection(collectionName, "conf1",
-// NUM_SHARDS, REPLICATION_FACTOR);
-// create.process(solrClient);
-//
-// solrClient.add(collectionName, new SolrInputDocument("id", "1"));
-// solrClient.add(collectionName, new SolrInputDocument("id", "2"));
-// solrClient.commit(collectionName);
-//
-// SolrQuery query = new SolrQuery();
-// query.setQuery("*:*");
-// QueryResponse rsp = solrClient.query(collectionName, query);
-// assertEquals(2, rsp.getResults().getNumFound());
-//
-// CollectionAdminRequest.Delete deleteReq = CollectionAdminRequest.deleteCollection(collectionName);
-// deleteReq.process(solrClient);
-// AbstractDistribZkTestBase.waitForCollectionToDisappear(collectionName,
-// solrClient.getZkStateReader(), true, true, 330);
-// solrClient.close();
-// }
-
private void verifyInterRequestHeaderCounts(int jwt, int pki) {
assertEquals(jwt, jwtInterceptCount.get());
assertEquals(pki, jwtInterceptCount.get());
@@ -221,7 +154,6 @@ private Pair get(String url, String token) throws IOException {
HttpURLConnection createConn = (HttpURLConnection) createUrl.openConnection();
if (token != null)
createConn.setRequestProperty("Authorization", "Bearer " + token);
- createConn.connect();
BufferedReader br2 = new BufferedReader(new InputStreamReader((InputStream) createConn.getContent()));
String result = br2.lines().collect(Collectors.joining("\n"));
int code = createConn.getResponseCode();
@@ -251,24 +183,6 @@ private Pair post(String url, String json, String token) throws
return new Pair<>(result, code);
}
-// SolrHttpClientBuilder getHttpClientBuilder(SolrHttpClientBuilder builder, String token) {
-// if (builder == null) {
-// builder = SolrHttpClientBuilder.create();
-// }
-// builder.setAuthSchemeRegistryProvider(() -> {
-// Lookup authProviders = RegistryBuilder.create()
-// .register("Bearer", new JWTAuthPlugin.JwtBearerAuthschemeProvider())
-// .build();
-// return authProviders;
-// });
-// builder.setDefaultCredentialsProvider(() -> {
-// CredentialsProvider credentialsProvider = new BasicCredentialsProvider();
-// credentialsProvider.setCredentials(AuthScope.ANY, new UsernamePasswordCredentials(token, null));
-// return credentialsProvider;
-// });
-// return builder;
-// }
-
private static class CountInterceptor implements HttpRequestInterceptor {
@Override
public void process(HttpRequest request, HttpContext context) throws HttpException, IOException {
From 47f591ff5176db97dbe2d2e2e1c59946bcc8127a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20H=C3=B8ydahl?=
Date: Wed, 5 Sep 2018 23:01:55 +0200
Subject: [PATCH 18/88] Convert to camel case config keys Fail if unknown
config keys are present Enable editing through API Update refguide Document
introspection API
---
.../apache/solr/security/JWTAuthPlugin.java | 81 ++++++++++++++-----
.../security/jwt_plugin_jwk_security.json | 2 +-
.../security/jwt_plugin_jwk_url_security.json | 2 +-
.../solr/security/JWTAuthPluginTest.java | 32 ++++----
.../src/jwt-authentication-plugin.adoc | 81 ++++++++++++-------
.../cluster.security.JwtAuth.Commands.json | 18 +++++
6 files changed, 152 insertions(+), 64 deletions(-)
create mode 100644 solr/solrj/src/resources/apispec/cluster.security.JwtAuth.Commands.json
diff --git a/solr/core/src/java/org/apache/solr/security/JWTAuthPlugin.java b/solr/core/src/java/org/apache/solr/security/JWTAuthPlugin.java
index 0371e9ab1be8..0c277aec9b79 100644
--- a/solr/core/src/java/org/apache/solr/security/JWTAuthPlugin.java
+++ b/solr/core/src/java/org/apache/solr/security/JWTAuthPlugin.java
@@ -41,7 +41,9 @@
import java.util.Set;
import java.util.StringTokenizer;
import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+import com.google.common.collect.ImmutableSet;
import org.apache.http.HttpException;
import org.apache.http.HttpHeaders;
import org.apache.http.HttpRequest;
@@ -53,6 +55,7 @@
import org.apache.solr.common.SolrException;
import org.apache.solr.common.SpecProvider;
import org.apache.solr.common.StringUtils;
+import org.apache.solr.common.util.CommandOperation;
import org.apache.solr.common.util.Utils;
import org.apache.solr.common.util.ValidatingJsonMap;
import org.apache.solr.core.CoreContainer;
@@ -76,27 +79,32 @@
/**
* Authenticaion plugin that finds logged in user by validating the signature of a JWT token
*/
-public class JWTAuthPlugin extends AuthenticationPlugin implements HttpClientBuilderPlugin, SpecProvider {
+public class JWTAuthPlugin extends AuthenticationPlugin implements HttpClientBuilderPlugin, SpecProvider, ConfigEditablePlugin {
private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
- private static final String PARAM_BLOCK_UNKNOWN = "block_unknown";
- private static final String PARAM_JWK_URL = "jwk_url";
+ private static final String PARAM_BLOCK_UNKNOWN = "blockUnknown";
+ private static final String PARAM_JWK_URL = "jwkUrl";
private static final String PARAM_JWK = "jwk";
private static final String PARAM_ISSUER = "iss";
private static final String PARAM_AUDIENCE = "aud";
- private static final String PARAM_REQUIRE_SUBJECT = "require_sub";
- private static final String PARAM_PRINCIPAL_CLAIM = "principal_claim";
- private static final String PARAM_REQUIRE_EXPIRATIONTIME = "require_exp";
- private static final String PARAM_ALG_WHITELIST = "alg_whitelist";
- private static final String PARAM_JWK_CACHE_DURATION = "jwk_cache_dur";
- private static final String PARAM_CLAIMS_MATCH = "claims_match";
+ private static final String PARAM_REQUIRE_SUBJECT = "requireSub";
+ private static final String PARAM_PRINCIPAL_CLAIM = "principalClaim";
+ private static final String PARAM_REQUIRE_EXPIRATIONTIME = "requireExp";
+ private static final String PARAM_ALG_WHITELIST = "algWhitelist";
+ private static final String PARAM_JWK_CACHE_DURATION = "jwkCacheDur";
+ private static final String PARAM_CLAIMS_MATCH = "claimsMatch";
private static final String PARAM_SCOPE = "scope";
- private static final String PARAM_ADMIN_SCOPE = "admin_scope";
- private static final String PARAM_CLIENT_ID = "client_id";
- private static final String PARAM_WELL_KNOWN_URL = "well_known_url";
+ private static final String PARAM_ADMIN_SCOPE = "adminScope";
+ private static final String PARAM_CLIENT_ID = "clientId";
+ private static final String PARAM_WELL_KNOWN_URL = "wellKnownUrl";
+
private static final String AUTH_REALM = "solr-jwt";
private static final String CLAIM_SCOPE = "scope";
private final JwtPkiDelegationInterceptor interceptor = new JwtPkiDelegationInterceptor();
+ static final Set supported_ops = ImmutableSet.of("set-user", "delete-user");
+ private static final Set PROPS = ImmutableSet.of(PARAM_BLOCK_UNKNOWN, PARAM_JWK_URL, PARAM_JWK, PARAM_ISSUER,
+ PARAM_AUDIENCE, PARAM_REQUIRE_SUBJECT, PARAM_PRINCIPAL_CLAIM, PARAM_REQUIRE_EXPIRATIONTIME, PARAM_ALG_WHITELIST,
+ PARAM_JWK_CACHE_DURATION, PARAM_CLAIMS_MATCH, PARAM_SCOPE, PARAM_ADMIN_SCOPE, PARAM_CLIENT_ID, PARAM_WELL_KNOWN_URL);
private JwtConsumer jwtConsumer;
private String iss;
@@ -118,6 +126,7 @@ public class JWTAuthPlugin extends AuthenticationPlugin implements HttpClientBui
private Instant lastInitTime = Instant.now();
private CoreContainer coreContainer;
+
/**
* Initialize plugin with core container, this method is chosen by reflection at create time
* @param coreContainer instance of core container
@@ -128,6 +137,12 @@ public JWTAuthPlugin(CoreContainer coreContainer) {
@Override
public void init(Map pluginConfig) {
+ List unknownKeys = pluginConfig.keySet().stream().filter(k -> !PROPS.contains(k)).collect(Collectors.toList());
+ unknownKeys.remove("class");
+ if (!unknownKeys.isEmpty()) {
+ throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "Invalid JwtAuth configuration parameter " + unknownKeys);
+ }
+
blockUnknown = Boolean.parseBoolean(String.valueOf(pluginConfig.getOrDefault(PARAM_BLOCK_UNKNOWN, false)));
clientId = (String) pluginConfig.get(PARAM_CLIENT_ID);
requireSubject = Boolean.parseBoolean(String.valueOf(pluginConfig.getOrDefault(PARAM_REQUIRE_SUBJECT, "true")));
@@ -204,7 +219,7 @@ private void initJwk(Map pluginConfig) {
JsonWebKeySet jwks = parseJwkSet(confJwk);
verificationKeyResolver = new JwksVerificationKeyResolver(jwks.getJsonWebKeys());
} catch (JoseException e) {
- throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "Invalid JWTAuthPlugin configuration, jwk field parse error", e);
+ throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "Invalid JWTAuthPlugin configuration, " + PARAM_JWK + " parse error", e);
}
}
initConsumer();
@@ -216,10 +231,10 @@ private void setupJwkUrl(String url) {
try {
URL jwkUrl = new URL(url);
if (!"https".equalsIgnoreCase(jwkUrl.getProtocol())) {
- throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "jwk_url must be an HTTPS url");
+ throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, PARAM_JWK_URL + " must be an HTTPS url");
}
} catch (MalformedURLException e) {
- throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "jwk_url must be a valid URL");
+ throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, PARAM_JWK_URL + " must be a valid URL");
}
HttpsJwks httpsJkws = new HttpsJwks(url);
httpsJkws.setDefaultCacheDuration(jwkCacheDuration);
@@ -251,7 +266,7 @@ public boolean doAuthenticate(ServletRequest servletRequest, ServletResponse ser
if (jwtConsumer == null) {
if (header == null && !blockUnknown) {
- log.info("JWTAuth not configured, but allowing anonymous access since blockUnknown==false");
+ log.info("JWTAuth not configured, but allowing anonymous access since {}==false", PARAM_BLOCK_UNKNOWN);
filterChain.doFilter(request, response);
return true;
}
@@ -285,7 +300,7 @@ public Principal getUserPrincipal() {
case PASS_THROUGH:
if (log.isDebugEnabled())
- log.debug("Unknown user, but allow due to block_unknown=false");
+ log.debug("Unknown user, but allow due to {}=false", PARAM_BLOCK_UNKNOWN);
filterChain.doFilter(request, response);
return true;
@@ -368,7 +383,7 @@ protected JWTAuthenticationResponse authenticate(String authorizationHeader) {
// Validate that at least one of the required scopes are present in the scope claim
if (!requiredScopes.isEmpty()) {
if (scopes.stream().noneMatch(requiredScopes::contains)) {
- return new JWTAuthenticationResponse(AuthCode.SCOPE_MISSING, "'scope' claim does not contain any of the required scopes: " + requiredScopes);
+ return new JWTAuthenticationResponse(AuthCode.SCOPE_MISSING, "Claim " + CLAIM_SCOPE + " does not contain any of the required scopes: " + requiredScopes);
}
}
final Set finalScopes = new HashSet<>(scopes);
@@ -399,7 +414,7 @@ protected JWTAuthenticationResponse authenticate(String authorizationHeader) {
if (blockUnknown) {
return new JWTAuthenticationResponse(AuthCode.NO_AUTZ_HEADER, "Missing Authorization header");
} else {
- log.debug("No user authenticated, but block_unknown=false, so letting request through");
+ log.debug("No user authenticated, but blockUnknown=false, so letting request through");
return new JWTAuthenticationResponse(AuthCode.PASS_THROUGH);
}
}
@@ -455,6 +470,34 @@ private void authenticationFailure(HttpServletResponse response, String message)
authenticationFailure(response, message, HttpServletResponse.SC_UNAUTHORIZED, null);
}
+ /**
+ * Operate the commands on the latest conf and return a new conf object
+ * If there are errors in the commands , throw a SolrException. return a null
+ * if no changes are to be made as a result of this edit. It is the responsibility
+ * of the implementation to ensure that the returned config is valid . The framework
+ * does no validation of the data
+ *
+ * @param latestConf latest version of config
+ * @param commands the list of command operations to perform
+ */
+ @Override
+ public Map edit(Map latestConf, List commands) {
+ for (CommandOperation command : commands) {
+ if (command.name.equals("set-property")) {
+ for (Map.Entry e : command.getDataMap().entrySet()) {
+ if (PROPS.contains(e.getKey())) {
+ latestConf.put(e.getKey(), e.getValue());
+ return latestConf;
+ } else {
+ command.addError("Unknown property " + e.getKey());
+ }
+ }
+ }
+ }
+ if (!CommandOperation.captureErrors(commands).isEmpty()) return null;
+ return latestConf;
+ }
+
private enum BearerWwwAuthErrorCode { invalid_request, invalid_token, insufficient_scope};
private void authenticationFailure(HttpServletResponse response, String message, int httpCode, BearerWwwAuthErrorCode responseError) throws IOException {
diff --git a/solr/core/src/test-files/solr/security/jwt_plugin_jwk_security.json b/solr/core/src/test-files/solr/security/jwt_plugin_jwk_security.json
index 3c249e64e9c2..7daab7ac9cbd 100644
--- a/solr/core/src/test-files/solr/security/jwt_plugin_jwk_security.json
+++ b/solr/core/src/test-files/solr/security/jwt_plugin_jwk_security.json
@@ -1,7 +1,7 @@
{
"authentication": {
"class": "solr.JWTAuthPlugin",
- "block_unknown": true,
+ "blockUnknown": true,
"jwk": {
"kty": "RSA",
"e": "AQAB",
diff --git a/solr/core/src/test-files/solr/security/jwt_plugin_jwk_url_security.json b/solr/core/src/test-files/solr/security/jwt_plugin_jwk_url_security.json
index 0431ab86335b..74b86ef03c14 100644
--- a/solr/core/src/test-files/solr/security/jwt_plugin_jwk_url_security.json
+++ b/solr/core/src/test-files/solr/security/jwt_plugin_jwk_url_security.json
@@ -1,6 +1,6 @@
{
"authentication" : {
"class": "solr.JWTAuthPlugin",
- "jwk_url": "https://127.0.0.1:8999/this-will-fail.wks"
+ "jwkUrl": "https://127.0.0.1:8999/this-will-fail.wks"
}
}
\ No newline at end of file
diff --git a/solr/core/src/test/org/apache/solr/security/JWTAuthPluginTest.java b/solr/core/src/test/org/apache/solr/security/JWTAuthPluginTest.java
index f8287531fc2f..99f07687f144 100644
--- a/solr/core/src/test/org/apache/solr/security/JWTAuthPluginTest.java
+++ b/solr/core/src/test/org/apache/solr/security/JWTAuthPluginTest.java
@@ -172,7 +172,7 @@ public void initWithJwk() {
@Test
public void initWithJwkUrl() {
HashMap authConf = new HashMap<>();
- authConf.put("jwk_url", "https://127.0.0.1:9999/foo.jwk");
+ authConf.put("jwkUrl", "https://127.0.0.1:9999/foo.jwk");
plugin = new JWTAuthPlugin(null);
plugin.init(authConf);
}
@@ -225,12 +225,12 @@ public void authFailedMissingAudience() {
@Test
public void authFailedMissingPrincipal() {
- testConfig.put("principal_claim", "customPrincipal");
+ testConfig.put("principalClaim", "customPrincipal");
plugin.init(testConfig);
JWTAuthPlugin.JWTAuthenticationResponse resp = plugin.authenticate(testHeader);
assertTrue(resp.isAuthenticated());
- testConfig.put("principal_claim", "NA");
+ testConfig.put("principalClaim", "NA");
plugin.init(testConfig);
resp = plugin.authenticate(testHeader);
assertFalse(resp.isAuthenticated());
@@ -244,7 +244,7 @@ public void claimMatch() {
shouldMatch.put("claim1", "foo");
shouldMatch.put("claim2", "foo|bar");
shouldMatch.put("claim3", "f\\w{2}$");
- testConfig.put("claims_match", shouldMatch);
+ testConfig.put("claimsMatch", shouldMatch);
plugin.init(testConfig);
JWTAuthPlugin.JWTAuthenticationResponse resp = plugin.authenticate(testHeader);
assertTrue(resp.isAuthenticated());
@@ -265,20 +265,20 @@ public void claimMatch() {
@Test
public void missingIssAudExp() {
- testConfig.put("require_exp", "false");
- testConfig.put("require_sub", "false");
+ testConfig.put("requireExp", "false");
+ testConfig.put("requireSub", "false");
plugin.init(testConfig);
JWTAuthPlugin.JWTAuthenticationResponse resp = plugin.authenticate(slimHeader);
assertTrue(resp.isAuthenticated());
// Missing exp header
- testConfig.put("require_exp", true);
+ testConfig.put("requireExp", true);
plugin.init(testConfig);
resp = plugin.authenticate(slimHeader);
assertEquals(JWTAuthPlugin.JWTAuthenticationResponse.AuthCode.JWT_VALIDATION_EXCEPTION, resp.getAuthCode());
// Missing sub header
- testConfig.put("require_sub", true);
+ testConfig.put("requireSub", true);
plugin.init(testConfig);
resp = plugin.authenticate(slimHeader);
assertEquals(JWTAuthPlugin.JWTAuthenticationResponse.AuthCode.JWT_VALIDATION_EXCEPTION, resp.getAuthCode());
@@ -286,7 +286,7 @@ public void missingIssAudExp() {
@Test
public void algWhitelist() {
- testConfig.put("alg_whitelist", Arrays.asList("PS384", "PS512"));
+ testConfig.put("algWhitelist", Arrays.asList("PS384", "PS512"));
plugin.init(testConfig);
JWTAuthPlugin.JWTAuthenticationResponse resp = plugin.authenticate(testHeader);
assertEquals(JWTAuthPlugin.JWTAuthenticationResponse.AuthCode.JWT_VALIDATION_EXCEPTION, resp.getAuthCode());
@@ -319,7 +319,7 @@ public void wrongScope() {
@Test
public void noHeaderBlockUnknown() {
- testConfig.put("block_unknown", true);
+ testConfig.put("blockUnknown", true);
plugin.init(testConfig);
JWTAuthPlugin.JWTAuthenticationResponse resp = plugin.authenticate(null);
assertEquals(NO_AUTZ_HEADER, resp.getAuthCode());
@@ -327,7 +327,7 @@ public void noHeaderBlockUnknown() {
@Test
public void noHeaderNotBlockUnknown() {
- testConfig.put("block_unknown", false);
+ testConfig.put("blockUnknown", false);
plugin.init(testConfig);
JWTAuthPlugin.JWTAuthenticationResponse resp = plugin.authenticate(null);
assertEquals(JWTAuthPlugin.JWTAuthenticationResponse.AuthCode.PASS_THROUGH, resp.getAuthCode());
@@ -335,7 +335,7 @@ public void noHeaderNotBlockUnknown() {
@Test
public void minimalConfigPassThrough() {
- testConfig.put("block_unknown", false);
+ testConfig.put("blockUnknown", false);
plugin.init(testConfig);
JWTAuthPlugin.JWTAuthenticationResponse resp = plugin.authenticate(null);
assertEquals(JWTAuthPlugin.JWTAuthenticationResponse.AuthCode.PASS_THROUGH, resp.getAuthCode());
@@ -344,7 +344,7 @@ public void minimalConfigPassThrough() {
@Test
public void wellKnownConfig() throws IOException {
String wellKnownUrl = TEST_PATH().resolve("security").resolve("jwt_well-known-config.json").toAbsolutePath().toUri().toString();
- testConfig.put("well_known_url", wellKnownUrl);
+ testConfig.put("wellKnownUrl", wellKnownUrl);
testConfig.remove("jwk");
plugin.init(testConfig);
JWTAuthPlugin.JWTAuthenticationResponse resp = plugin.authenticate(null);
@@ -353,19 +353,19 @@ public void wellKnownConfig() throws IOException {
@Test(expected = SolrException.class)
public void onlyOneJwkConfig() throws IOException {
- testConfig.put("jwk_url", "http://127.0.0.1:45678/.well-known/config");
+ testConfig.put("jwkUrl", "http://127.0.0.1:45678/.well-known/config");
plugin.init(testConfig);
}
@Test(expected = SolrException.class)
public void wellKnownConfigNotHttps() throws IOException {
- testConfig.put("well_known_url", "http://127.0.0.1:45678/.well-known/config");
+ testConfig.put("wellKnownUrl", "http://127.0.0.1:45678/.well-known/config");
plugin.init(testConfig);
}
@Test(expected = SolrException.class)
public void wellKnownConfigNotReachable() {
- testConfig.put("well_known_url", "https://127.0.0.1:45678/.well-known/config");
+ testConfig.put("wellKnownUrl", "https://127.0.0.1:45678/.well-known/config");
plugin.init(testConfig);
}
diff --git a/solr/solr-ref-guide/src/jwt-authentication-plugin.adoc b/solr/solr-ref-guide/src/jwt-authentication-plugin.adoc
index 39c13f87884c..041f8c4b9d7c 100644
--- a/solr/solr-ref-guide/src/jwt-authentication-plugin.adoc
+++ b/solr/solr-ref-guide/src/jwt-authentication-plugin.adoc
@@ -44,8 +44,8 @@ To start enforcing authentication for all users, requiring a valid JWT token in
{
"authentication": {
"class": "solr.JWTAuthPlugin",
- "block_unknown": true,
- "jwk_url": "https://my.key.server/jwk.json"
+ "blockUnknown": true,
+ "jwkUrl": "https://my.key.server/jwk.json"
}
}
----
@@ -57,8 +57,8 @@ The next example shows configuring using https://openid.net/specs/openid-connect
{
"authentication": {
"class": "solr.JWTAuthPlugin",
- "block_unknown": true,
- "well_known_url": "https://idp.example.com/.well-known/openid-configuration"
+ "blockUnknown": true,
+ "wellKnownUrl": "https://idp.example.com/.well-known/openid-configuration"
}
}
----
@@ -72,20 +72,20 @@ A more complex configuration with static JWK:
{
"authentication": {
"class": "solr.JWTAuthPlugin", <1>
- "block_unknown": true, <2>
+ "blockUnknown": true, <2>
"jwk": { <3>
"e": "AQAB",
"kid": "k1",
"kty": "RSA",
"n": "3ZF6wBGPMsLzsS1KLghxaVpZtXD3nTLzDm0c974i9-KNU_1rhhBeiVfS64VfEQmP8SA470jEy7yWcvnz9GvG-YAlm9iOwVF7jLl2awdws0ocFjdSPT3SjPQKzOeMO7G9XqNTkrvoFCn1YAi26fbhhcqkwZDoeTcHQdRN32frzccuPhZrwImApIedroKLlKWv2IvPDnz2Bpe2WWVc2HdoWYqEVD3p_BEy8f-RTSHK3_8kDDF9yAwI9jx7CK1_C-eYxXltm-6rpS5NGyFm0UNTZMxVU28Tl7LX8Vb6CikyCQ9YRCtk_CvpKWmEuKEp9I28KHQNmGkDYT90nt3vjbCXxw"
},
- "client_id": "solr-client-12345", <4>
+ "clientId": "solr-client-12345", <4>
"iss": "https://example.com/idp", <5>
"aud": "https://example.com/solr", <6>
- "principal_claim": "solruid", <7>
- "claims_match": { "roles" : "admin|search", "dept" : "IT" }, <8>
+ "principalClaim": "solruid", <7>
+ "claimsMatch": { "foo" : "A|B", "dept" : "IT" }, <8>
"scope": "solr:read solr:write solr:admin", <9>
- "alg_whitelist" : [ "RS256", "RS384", "RS512" ] <10>
+ "algWhitelist" : [ "RS256", "RS384", "RS512" ] <10>
}
}
----
@@ -94,12 +94,12 @@ Let's comment on this config:
<1> Plugin class
<2> Make sure to block anyone without a valid token
-<3> Here we pass the JWK inline instead of referring to a URL with `jwk_url`
+<3> Here we pass the JWK inline instead of referring to a URL with `jwkUrl`
<4> Set the client id registered with Identiy Provider
<5> The issuer claim must match "https://example.com/idp"
<6> The audience claim must match "https://example.com/solr"
<7> Fetch the user id from another claim than the default `sub`
-<8> Require that the `roles` claim is one of "admin" or "search" and that the `dept` claim is "IT"
+<8> Require that the `roles` claim is one of "A" or "B" and that the `dept` claim is "IT"
<9> Require one of the scopes `solr:read`, `solr:write` or `solr:amin`
<10> Only accept RSA algorithms for signatures
@@ -108,33 +108,60 @@ Let's comment on this config:
[%header,format=csv,separator=;]
|===
Key ; Description ; Default
-block_unknown ; Set to `false` in order to pass requests from unknown users without a token ; `true`
-well_known_url ; URL to an https://openid.net/specs/openid-connect-discovery-1_0.html[OpenID Connect Discovery] endpoint ;
-client_id ; Client identifier for use with OpenID Connect ;
+blockUnknown ; Set to `false` in order to pass requests from unknown users without a token ; `true`
+wellKnownUrl ; URL to an https://openid.net/specs/openid-connect-discovery-1_0.html[OpenID Connect Discovery] endpoint ;
+clientId ; Client identifier for use with OpenID Connect ;
scope ; Whitespace separate list of valid scopes. If configured, the JWT access token MUST contain a `scope` claim with at least one of the listed scopes. Example: `solr:read solr:admin` ;
-jwk_url ; An https URL to a https://tools.ietf.org/html/rfc7517[JWK] keys file. Not required if `well_known_url` is provided ; Auto configured if `well_known_url` is provided
+jwkUrl ; An https URL to a https://tools.ietf.org/html/rfc7517[JWK] keys file. Not required if `well_known_url` is provided ; Auto configured if `well_known_url` is provided
jwk ; As an alternative to `jwk_url` you may provide a JSON object here containing the public key(s) of the issuer. ; Auto configured if `well_known_url` is provided
iss ; Validates that `iss` (issuer) equals this string ; If `well_known_url` is provided, use `issuer` from there.
aud ; Validates that `aud` (audience) equals this string ; If `client_id` is configured, require `aud` to match it
-require_sub ; Makes `sub` (subject) mandatory ; `true`
-require_exp ; Makes `exp` (expiry time) mandatory ; `true`
-alg_whitelist ; JSON array with algorithms to accept: `HS256`, `HS384`, `HS512`, `RS256`, `RS384`, `RS512`, `ES256`, `ES384`, `ES512`, `PS256`, `PS384`, `PS512`, `none ;
-jwk_cache_dur ; Duration of JWK cache in seconds ; `3600` (1 hour)
-principal_claim ; What claim id to pull principal from ; `sub`
-claims_match ; JSON object of claims (key) that must match a regular expression (value). Example: `{ "foo" : "A|B" }` will require the `foo` claim to be either "A" or "B". ; (none)
+requireSub ; Makes `sub` (subject) mandatory ; `true`
+requireExp ; Makes `exp` (expiry time) mandatory ; `true`
+algWhitelist ; JSON array with algorithms to accept: `HS256`, `HS384`, `HS512`, `RS256`, `RS384`, `RS512`, `ES256`, `ES384`, `ES512`, `PS256`, `PS384`, `PS512`, `none ;
+jwkCacheDur ; Duration of JWK cache in seconds ; `3600` (1 hour)
+principalClaim ; What claim id to pull principal from ; `sub`
+claimsMatch ; JSON object of claims (key) that must match a regular expression (value). Example: `{ "foo" : "A|B" }` will require the `foo` claim to be either "A" or "B". ; (none)
|===
== Editing Authentication Plugin Configuration
-**TODO: This plugin currently does not support edit API.**
+All properties mentioned above can be set or changed using the <>. You can thus start with a simple configuration with only `class` configured and then configure the rest using the API.
-=== Using JWT Auth with SolrJ
+=== Set a Property
-To use JWT Auth with SolrJ you must configure a `SolrHttpClientBuilder` that sets the `Authorization: Bearer` header. How to obtain a valid JWT token is up to the client application to choose.
+Set properties for the authentication plugin. Each of the configuration keys in the table above can be used as parameter keys for the `set-property` command.
-**TODO: Describe this **
+Example:
-=== Using the Solr Control Script with Basic Auth
+[.dynamic-tabs]
+--
+[example.tab-pane#v1set-property]
+====
+[.tab-label]*V1 API*
-The control scripts (`bin/solr`) do not currently support JWT Auth.
\ No newline at end of file
+[source,bash]
+----
+curl --user solr:SolrRocks http://localhost:8983/solr/admin/authentication -H 'Content-type:application/json' -d '{"set-property": {"blockUnknown":true, "wellKnownUrl": "https://example.com/.well-knwon/openid-configuration", "scope": "solr:read solr:write"}}'
+----
+====
+
+[example.tab-pane#v2set-property]
+====
+[.tab-label]*V2 API*
+
+[source,bash]
+----
+curl --user solr:SolrRocks http://localhost:8983/api/cluster/security/authentication -H 'Content-type:application/json' -d '{"set-property": {"blockUnknown":true, "wellKnownUrl": "https://example.com/.well-knwon/openid-configuration", "scope": "solr:read solr:write"}}'
+----
+====
+--
+
+== Using JWT Auth with SolrJ
+
+SolrJ does not currently support setting JWT token per request. This is planned for a future release.
+
+== Using the Solr Control Script with Basic Auth
+
+The control script (`bin/solr`) does not currently support JWT Auth.
\ No newline at end of file
diff --git a/solr/solrj/src/resources/apispec/cluster.security.JwtAuth.Commands.json b/solr/solrj/src/resources/apispec/cluster.security.JwtAuth.Commands.json
new file mode 100644
index 000000000000..e940d2f34f7a
--- /dev/null
+++ b/solr/solrj/src/resources/apispec/cluster.security.JwtAuth.Commands.json
@@ -0,0 +1,18 @@
+{
+ "documentation": "https://lucene.apache.org/solr/guide/jwt-authentication-plugin.html",
+ "description": "Modifies the configuration of JWT token authentication.",
+ "methods": [
+ "POST"
+ ],
+ "url": {
+ "paths": [
+ "/cluster/security/authentication"
+ ]
+ },
+ "commands": {
+ "set-property": {
+ "type":"object",
+ "description": "The set-property command lets you set any of the configuration parameters supported by this plugin"
+ }
+ }
+}
From 54b80cf9e2a227fbd8aed112178620119fcd5e17 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20H=C3=B8ydahl?=
Date: Tue, 11 Sep 2018 23:07:40 +0200
Subject: [PATCH 19/88] Refguide wiip
---
.../src/jwt-authentication-plugin.adoc | 14 ++++++++++++--
1 file changed, 12 insertions(+), 2 deletions(-)
diff --git a/solr/solr-ref-guide/src/jwt-authentication-plugin.adoc b/solr/solr-ref-guide/src/jwt-authentication-plugin.adoc
index 041f8c4b9d7c..f501126830ae 100644
--- a/solr/solr-ref-guide/src/jwt-authentication-plugin.adoc
+++ b/solr/solr-ref-guide/src/jwt-authentication-plugin.adoc
@@ -158,9 +158,19 @@ curl --user solr:SolrRocks http://localhost:8983/api/cluster/security/authentica
====
--
-== Using JWT Auth with SolrJ
+== Using clients with JWT Auth
-SolrJ does not currently support setting JWT token per request. This is planned for a future release.
+=== SolrJ
+
+SolrJ does not currently support setting JWT tokens per request. This is planned for a future release.
+
+=== cURL
+
+To authenticate with Solr with cURL
+
+=== Admin UI
+
+The Admin UI runs as a SPA (single page application) in your browser, and talks with the Solr APIs just like any other client. Because of this the Admin UI will stop working after enabling this plugin. To circumvent this you can install a browser plugin that adds HTTP headers to requests, and add a valid JWT token to the `Authorization` header by hand.
== Using the Solr Control Script with Basic Auth
From e4d1d50918889ade08255c1ed29ce901a73e36bd Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20H=C3=B8ydahl?=
Date: Thu, 20 Sep 2018 10:27:30 +0200
Subject: [PATCH 20/88] Make unique headers
---
.../src/jwt-authentication-plugin.adoc | 12 +++++++-----
1 file changed, 7 insertions(+), 5 deletions(-)
diff --git a/solr/solr-ref-guide/src/jwt-authentication-plugin.adoc b/solr/solr-ref-guide/src/jwt-authentication-plugin.adoc
index f501126830ae..a9942792c10b 100644
--- a/solr/solr-ref-guide/src/jwt-authentication-plugin.adoc
+++ b/solr/solr-ref-guide/src/jwt-authentication-plugin.adoc
@@ -125,11 +125,11 @@ claimsMatch ; JSON object of claims (key) that must match a regular expressi
|===
-== Editing Authentication Plugin Configuration
+== Editing JWT Authentication Plugin Configuration
All properties mentioned above can be set or changed using the <>. You can thus start with a simple configuration with only `class` configured and then configure the rest using the API.
-=== Set a Property
+=== Set a config Property
Set properties for the authentication plugin. Each of the configuration keys in the table above can be used as parameter keys for the `set-property` command.
@@ -137,7 +137,7 @@ Example:
[.dynamic-tabs]
--
-[example.tab-pane#v1set-property]
+[example.tab-pane#jwt-v1set-property]
====
[.tab-label]*V1 API*
@@ -147,7 +147,7 @@ curl --user solr:SolrRocks http://localhost:8983/solr/admin/authentication -H 'C
----
====
-[example.tab-pane#v2set-property]
+[example.tab-pane#jwt-v2set-property]
====
[.tab-label]*V2 API*
@@ -160,10 +160,12 @@ curl --user solr:SolrRocks http://localhost:8983/api/cluster/security/authentica
== Using clients with JWT Auth
+[#jwt-soljr]
=== SolrJ
SolrJ does not currently support setting JWT tokens per request. This is planned for a future release.
+[#jwt-curl]
=== cURL
To authenticate with Solr with cURL
@@ -172,6 +174,6 @@ To authenticate with Solr with cURL
The Admin UI runs as a SPA (single page application) in your browser, and talks with the Solr APIs just like any other client. Because of this the Admin UI will stop working after enabling this plugin. To circumvent this you can install a browser plugin that adds HTTP headers to requests, and add a valid JWT token to the `Authorization` header by hand.
-== Using the Solr Control Script with Basic Auth
+== Using the Solr Control Script with JWT Auth
The control script (`bin/solr`) does not currently support JWT Auth.
\ No newline at end of file
From 6014b855ffe5ab9c74a7e16a4ea65d4a80668121 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20H=C3=B8ydahl?=
Date: Thu, 20 Sep 2018 14:45:46 +0200
Subject: [PATCH 21/88] SOLR-12786: HTML reference guide has invalid links
---
solr/solr-ref-guide/src/basic-authentication-plugin.adoc | 2 +-
solr/solr-ref-guide/src/common-query-parameters.adoc | 2 +-
solr/solr-ref-guide/src/enabling-ssl.adoc | 2 +-
solr/solr-ref-guide/src/kerberos-authentication-plugin.adoc | 2 +-
solr/solr-ref-guide/src/major-changes-in-solr-7.adoc | 2 +-
solr/solr-ref-guide/src/resource-and-plugin-loading.adoc | 2 +-
solr/solr-ref-guide/src/solr-control-script-reference.adoc | 4 ++--
solr/solr-ref-guide/src/solr-tutorial.adoc | 2 +-
solr/solr-ref-guide/src/solrcloud-autoscaling-triggers.adoc | 2 +-
.../src/using-zookeeper-to-manage-configuration-files.adoc | 2 +-
10 files changed, 11 insertions(+), 11 deletions(-)
diff --git a/solr/solr-ref-guide/src/basic-authentication-plugin.adoc b/solr/solr-ref-guide/src/basic-authentication-plugin.adoc
index 88e8a0c8d24c..971f59eb3bbb 100644
--- a/solr/solr-ref-guide/src/basic-authentication-plugin.adoc
+++ b/solr/solr-ref-guide/src/basic-authentication-plugin.adoc
@@ -65,7 +65,7 @@ If you are using SolrCloud, you must upload `security.json` to ZooKeeper. You ca
bin/solr zk cp file:path_to_local_security.json zk:/security.json -z localhost:9983
----
-NOTE: If you have defined `ZK_HOST` in `solr.in.sh`/`solr.in.cmd` (see <>) you can omit `-z ` from the above command.
+NOTE: If you have defined `ZK_HOST` in `solr.in.sh`/`solr.in.cmd` (see <>) you can omit `-z ` from the above command.
=== Caveats
diff --git a/solr/solr-ref-guide/src/common-query-parameters.adoc b/solr/solr-ref-guide/src/common-query-parameters.adoc
index 499401525c0e..5df3ca3e3c25 100644
--- a/solr/solr-ref-guide/src/common-query-parameters.adoc
+++ b/solr/solr-ref-guide/src/common-query-parameters.adoc
@@ -102,7 +102,7 @@ fq=+popularity:[10 TO *] +section:0
----
* The document sets from each filter query are cached independently. Thus, concerning the previous examples: use a single `fq` containing two mandatory clauses if those clauses appear together often, and use two separate `fq` parameters if they are relatively independent. (To learn about tuning cache sizes and making sure a filter cache actually exists, see <>.)
-* It is also possible to use <> inside the `fq` to cache clauses individually and - among other things - to achieve union of cached filter queries.
+* It is also possible to use <> inside the `fq` to cache clauses individually and - among other things - to achieve union of cached filter queries.
* As with all parameters: special characters in an URL need to be properly escaped and encoded as hex values. Online tools are available to help you with URL-encoding. For example: http://meyerweb.com/eric/tools/dencoder/.
diff --git a/solr/solr-ref-guide/src/enabling-ssl.adoc b/solr/solr-ref-guide/src/enabling-ssl.adoc
index 96262bd4e3ec..77e66440d983 100644
--- a/solr/solr-ref-guide/src/enabling-ssl.adoc
+++ b/solr/solr-ref-guide/src/enabling-ssl.adoc
@@ -175,7 +175,7 @@ If you have set up your ZooKeeper cluster to use a <>) you can omit `-z ` from all of the `bin/solr`/`bin\solr.cmd` commands below.
+NOTE: If you have defined `ZK_HOST` in `solr.in.sh`/`solr.in.cmd` (see <>) you can omit `-z ` from all of the `bin/solr`/`bin\solr.cmd` commands below.
==== Create Solr Home Directories for Two Nodes
diff --git a/solr/solr-ref-guide/src/kerberos-authentication-plugin.adoc b/solr/solr-ref-guide/src/kerberos-authentication-plugin.adoc
index ed0b3cb900ad..ee1337b7caee 100644
--- a/solr/solr-ref-guide/src/kerberos-authentication-plugin.adoc
+++ b/solr/solr-ref-guide/src/kerberos-authentication-plugin.adoc
@@ -298,7 +298,7 @@ Once the configuration is complete, you can start Solr with the `bin/solr` scrip
bin/solr -c -z server1:2181,server2:2181,server3:2181/solr
----
-NOTE: If you have defined `ZK_HOST` in `solr.in.sh`/`solr.in.cmd` (see <>) you can omit `-z ` from the above command.
+NOTE: If you have defined `ZK_HOST` in `solr.in.sh`/`solr.in.cmd` (see <>) you can omit `-z ` from the above command.
=== Test the Configuration
diff --git a/solr/solr-ref-guide/src/major-changes-in-solr-7.adoc b/solr/solr-ref-guide/src/major-changes-in-solr-7.adoc
index 9689d276d019..a6b639c8f526 100644
--- a/solr/solr-ref-guide/src/major-changes-in-solr-7.adoc
+++ b/solr/solr-ref-guide/src/major-changes-in-solr-7.adoc
@@ -157,7 +157,7 @@ Limited back-compatibility is offered by automatically adding a default instance
+
This behavior can be also disabled by specifying a `SolrJmxReporter` configuration with a boolean init argument `enabled` set to `false`. For a more fine-grained control users should explicitly specify at least one `SolrJmxReporter` configuration.
+
-See also the section < Element>>, which describes how to set up Metrics Reporters in `solr.xml`. Note that back-compatibility support may be removed in Solr 8.
+See also the section < Element>>, which describes how to set up Metrics Reporters in `solr.xml`. Note that back-compatibility support may be removed in Solr 8.
* MBean names and attributes now follow the hierarchical names used in metrics. This is reflected also in `/admin/mbeans` and `/admin/plugins` output, and can be observed in the UI Plugins tab, because now all these APIs get their data from the metrics API. The old (mostly flat) JMX view has been removed.
diff --git a/solr/solr-ref-guide/src/resource-and-plugin-loading.adoc b/solr/solr-ref-guide/src/resource-and-plugin-loading.adoc
index 04dc84078df6..6efd13532903 100644
--- a/solr/solr-ref-guide/src/resource-and-plugin-loading.adoc
+++ b/solr/solr-ref-guide/src/resource-and-plugin-loading.adoc
@@ -46,7 +46,7 @@ CAUTION: By default, ZooKeeper's file size limit is 1MB. If your files are large
Under standalone Solr, when looking up a plugin or resource to be loaded, Solr's resource loader will first look under the `/conf/` directory. If the plugin or resource is not found, the configured plugin and resource file paths are searched - see the section <> below.
-On core load, Solr's resource loader constructs a list of paths (subdirectories and jars), first under <>, and then under directories pointed to by <` directives in SolrConfig>>.
+On core load, Solr's resource loader constructs a list of paths (subdirectories and jars), first under <>, and then under directories pointed to by <` directives in SolrConfig>>.
When looking up a resource or plugin to be loaded, the paths on the list are searched in the order they were added.
diff --git a/solr/solr-ref-guide/src/solr-control-script-reference.adoc b/solr/solr-ref-guide/src/solr-control-script-reference.adoc
index 5d5808e49d93..b4ebd34964f3 100644
--- a/solr/solr-ref-guide/src/solr-control-script-reference.adoc
+++ b/solr/solr-ref-guide/src/solr-control-script-reference.adoc
@@ -60,7 +60,7 @@ Start Solr in SolrCloud mode, which will also launch the embedded ZooKeeper inst
+
This option can be shortened to simply `-c`.
+
-If you are already running a ZooKeeper ensemble that you want to use instead of the embedded (single-node) ZooKeeper, you should also either specify `ZK_HOST` in `solr.in.sh`/`solr.in.cmd` (see <>) or pass the -z parameter.
+If you are already running a ZooKeeper ensemble that you want to use instead of the embedded (single-node) ZooKeeper, you should also either specify `ZK_HOST` in `solr.in.sh`/`solr.in.cmd` (see <>) or pass the -z parameter.
+
For more details, see the section <> below.
+
@@ -172,7 +172,7 @@ The `-c` and `-cloud` options are equivalent:
If you specify a ZooKeeper connection string, such as `-z 192.168.1.4:2181`, then Solr will connect to ZooKeeper and join the cluster.
-NOTE: If you have defined `ZK_HOST` in `solr.in.sh`/`solr.in.cmd` (see <>) you can omit `-z ` from all `bin/solr` commands.
+NOTE: If you have defined `ZK_HOST` in `solr.in.sh`/`solr.in.cmd` (see <>) you can omit `-z ` from all `bin/solr` commands.
When starting Solr in cloud mode, if you neither define `ZK_HOST` in `solr.in.sh`/`solr.in.cmd` nor specify the `-z` option, then Solr will launch an embedded ZooKeeper server listening on the Solr port + 1000, i.e., if Solr is running on port 8983, then the embedded ZooKeeper will be listening on port 9983.
diff --git a/solr/solr-ref-guide/src/solr-tutorial.adoc b/solr/solr-ref-guide/src/solr-tutorial.adoc
index 577fc5cdd2f3..34c1f9cce15c 100644
--- a/solr/solr-ref-guide/src/solr-tutorial.adoc
+++ b/solr/solr-ref-guide/src/solr-tutorial.adoc
@@ -497,7 +497,7 @@ This starts the first node. When it's done start the second node, and tell it ho
`./bin/solr start -c -p 7574 -s example/cloud/node2/solr -z localhost:9983`
-NOTE: If you have defined `ZK_HOST` in `solr.in.sh`/`solr.in.cmd` (see <>) you can omit `-z ` from the above command.
+NOTE: If you have defined `ZK_HOST` in `solr.in.sh`/`solr.in.cmd` (see <>) you can omit `-z ` from the above command.
=== Create a New Collection
diff --git a/solr/solr-ref-guide/src/solrcloud-autoscaling-triggers.adoc b/solr/solr-ref-guide/src/solrcloud-autoscaling-triggers.adoc
index 0b41e21914d1..ddb2002cd394 100644
--- a/solr/solr-ref-guide/src/solrcloud-autoscaling-triggers.adoc
+++ b/solr/solr-ref-guide/src/solrcloud-autoscaling-triggers.adoc
@@ -55,7 +55,7 @@ generated, which may significantly differ due to the rate limits set by `waitFor
indicates the nodes that were lost or added.
== Trigger Configuration
-Trigger configurations are managed using the <> with the commands `<>`, `<>`,
+Trigger configurations are managed using the <> with the commands `<>`, `<>`,
`suspend-trigger`, and `resume-trigger`.
=== Trigger Properties
diff --git a/solr/solr-ref-guide/src/using-zookeeper-to-manage-configuration-files.adoc b/solr/solr-ref-guide/src/using-zookeeper-to-manage-configuration-files.adoc
index b1b3b5a77e1b..091011eebc6b 100644
--- a/solr/solr-ref-guide/src/using-zookeeper-to-manage-configuration-files.adoc
+++ b/solr/solr-ref-guide/src/using-zookeeper-to-manage-configuration-files.adoc
@@ -85,4 +85,4 @@ If you for example would like to keep your `solr.xml` in ZooKeeper to avoid havi
bin/solr zk cp file:local/file/path/to/solr.xml zk:/solr.xml -z localhost:2181
----
-NOTE: If you have defined `ZK_HOST` in `solr.in.sh`/`solr.in.cmd` (see <>) you can omit `-z ` from the above command.
\ No newline at end of file
+NOTE: If you have defined `ZK_HOST` in `solr.in.sh`/`solr.in.cmd` (see <>) you can omit `-z ` from the above command.
\ No newline at end of file
From 91699c26eab63d22e04e527af4db675cadb1ea2c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20H=C3=B8ydahl?=
Date: Thu, 20 Sep 2018 16:28:20 +0200
Subject: [PATCH 22/88] Revert "SOLR-12786: HTML reference guide has invalid
links"
This reverts commit 6014b85
---
solr/solr-ref-guide/src/basic-authentication-plugin.adoc | 2 +-
solr/solr-ref-guide/src/common-query-parameters.adoc | 2 +-
solr/solr-ref-guide/src/enabling-ssl.adoc | 2 +-
solr/solr-ref-guide/src/kerberos-authentication-plugin.adoc | 2 +-
solr/solr-ref-guide/src/major-changes-in-solr-7.adoc | 2 +-
solr/solr-ref-guide/src/resource-and-plugin-loading.adoc | 2 +-
solr/solr-ref-guide/src/solr-control-script-reference.adoc | 4 ++--
solr/solr-ref-guide/src/solr-tutorial.adoc | 2 +-
solr/solr-ref-guide/src/solrcloud-autoscaling-triggers.adoc | 2 +-
.../src/using-zookeeper-to-manage-configuration-files.adoc | 2 +-
10 files changed, 11 insertions(+), 11 deletions(-)
diff --git a/solr/solr-ref-guide/src/basic-authentication-plugin.adoc b/solr/solr-ref-guide/src/basic-authentication-plugin.adoc
index 971f59eb3bbb..88e8a0c8d24c 100644
--- a/solr/solr-ref-guide/src/basic-authentication-plugin.adoc
+++ b/solr/solr-ref-guide/src/basic-authentication-plugin.adoc
@@ -65,7 +65,7 @@ If you are using SolrCloud, you must upload `security.json` to ZooKeeper. You ca
bin/solr zk cp file:path_to_local_security.json zk:/security.json -z localhost:9983
----
-NOTE: If you have defined `ZK_HOST` in `solr.in.sh`/`solr.in.cmd` (see <>) you can omit `-z ` from the above command.
+NOTE: If you have defined `ZK_HOST` in `solr.in.sh`/`solr.in.cmd` (see <>) you can omit `-z ` from the above command.
=== Caveats
diff --git a/solr/solr-ref-guide/src/common-query-parameters.adoc b/solr/solr-ref-guide/src/common-query-parameters.adoc
index 5df3ca3e3c25..499401525c0e 100644
--- a/solr/solr-ref-guide/src/common-query-parameters.adoc
+++ b/solr/solr-ref-guide/src/common-query-parameters.adoc
@@ -102,7 +102,7 @@ fq=+popularity:[10 TO *] +section:0
----
* The document sets from each filter query are cached independently. Thus, concerning the previous examples: use a single `fq` containing two mandatory clauses if those clauses appear together often, and use two separate `fq` parameters if they are relatively independent. (To learn about tuning cache sizes and making sure a filter cache actually exists, see <>.)
-* It is also possible to use <> inside the `fq` to cache clauses individually and - among other things - to achieve union of cached filter queries.
+* It is also possible to use <> inside the `fq` to cache clauses individually and - among other things - to achieve union of cached filter queries.
* As with all parameters: special characters in an URL need to be properly escaped and encoded as hex values. Online tools are available to help you with URL-encoding. For example: http://meyerweb.com/eric/tools/dencoder/.
diff --git a/solr/solr-ref-guide/src/enabling-ssl.adoc b/solr/solr-ref-guide/src/enabling-ssl.adoc
index 77e66440d983..96262bd4e3ec 100644
--- a/solr/solr-ref-guide/src/enabling-ssl.adoc
+++ b/solr/solr-ref-guide/src/enabling-ssl.adoc
@@ -175,7 +175,7 @@ If you have set up your ZooKeeper cluster to use a <>) you can omit `-z ` from all of the `bin/solr`/`bin\solr.cmd` commands below.
+NOTE: If you have defined `ZK_HOST` in `solr.in.sh`/`solr.in.cmd` (see <>) you can omit `-z ` from all of the `bin/solr`/`bin\solr.cmd` commands below.
==== Create Solr Home Directories for Two Nodes
diff --git a/solr/solr-ref-guide/src/kerberos-authentication-plugin.adoc b/solr/solr-ref-guide/src/kerberos-authentication-plugin.adoc
index ee1337b7caee..ed0b3cb900ad 100644
--- a/solr/solr-ref-guide/src/kerberos-authentication-plugin.adoc
+++ b/solr/solr-ref-guide/src/kerberos-authentication-plugin.adoc
@@ -298,7 +298,7 @@ Once the configuration is complete, you can start Solr with the `bin/solr` scrip
bin/solr -c -z server1:2181,server2:2181,server3:2181/solr
----
-NOTE: If you have defined `ZK_HOST` in `solr.in.sh`/`solr.in.cmd` (see <>) you can omit `-z ` from the above command.
+NOTE: If you have defined `ZK_HOST` in `solr.in.sh`/`solr.in.cmd` (see <>) you can omit `-z ` from the above command.
=== Test the Configuration
diff --git a/solr/solr-ref-guide/src/major-changes-in-solr-7.adoc b/solr/solr-ref-guide/src/major-changes-in-solr-7.adoc
index a6b639c8f526..9689d276d019 100644
--- a/solr/solr-ref-guide/src/major-changes-in-solr-7.adoc
+++ b/solr/solr-ref-guide/src/major-changes-in-solr-7.adoc
@@ -157,7 +157,7 @@ Limited back-compatibility is offered by automatically adding a default instance
+
This behavior can be also disabled by specifying a `SolrJmxReporter` configuration with a boolean init argument `enabled` set to `false`. For a more fine-grained control users should explicitly specify at least one `SolrJmxReporter` configuration.
+
-See also the section < Element>>, which describes how to set up Metrics Reporters in `solr.xml`. Note that back-compatibility support may be removed in Solr 8.
+See also the section < Element>>, which describes how to set up Metrics Reporters in `solr.xml`. Note that back-compatibility support may be removed in Solr 8.
* MBean names and attributes now follow the hierarchical names used in metrics. This is reflected also in `/admin/mbeans` and `/admin/plugins` output, and can be observed in the UI Plugins tab, because now all these APIs get their data from the metrics API. The old (mostly flat) JMX view has been removed.
diff --git a/solr/solr-ref-guide/src/resource-and-plugin-loading.adoc b/solr/solr-ref-guide/src/resource-and-plugin-loading.adoc
index 6efd13532903..04dc84078df6 100644
--- a/solr/solr-ref-guide/src/resource-and-plugin-loading.adoc
+++ b/solr/solr-ref-guide/src/resource-and-plugin-loading.adoc
@@ -46,7 +46,7 @@ CAUTION: By default, ZooKeeper's file size limit is 1MB. If your files are large
Under standalone Solr, when looking up a plugin or resource to be loaded, Solr's resource loader will first look under the `/conf/` directory. If the plugin or resource is not found, the configured plugin and resource file paths are searched - see the section <> below.
-On core load, Solr's resource loader constructs a list of paths (subdirectories and jars), first under <>, and then under directories pointed to by <` directives in SolrConfig>>.
+On core load, Solr's resource loader constructs a list of paths (subdirectories and jars), first under <>, and then under directories pointed to by <` directives in SolrConfig>>.
When looking up a resource or plugin to be loaded, the paths on the list are searched in the order they were added.
diff --git a/solr/solr-ref-guide/src/solr-control-script-reference.adoc b/solr/solr-ref-guide/src/solr-control-script-reference.adoc
index b4ebd34964f3..5d5808e49d93 100644
--- a/solr/solr-ref-guide/src/solr-control-script-reference.adoc
+++ b/solr/solr-ref-guide/src/solr-control-script-reference.adoc
@@ -60,7 +60,7 @@ Start Solr in SolrCloud mode, which will also launch the embedded ZooKeeper inst
+
This option can be shortened to simply `-c`.
+
-If you are already running a ZooKeeper ensemble that you want to use instead of the embedded (single-node) ZooKeeper, you should also either specify `ZK_HOST` in `solr.in.sh`/`solr.in.cmd` (see <>) or pass the -z parameter.
+If you are already running a ZooKeeper ensemble that you want to use instead of the embedded (single-node) ZooKeeper, you should also either specify `ZK_HOST` in `solr.in.sh`/`solr.in.cmd` (see <>) or pass the -z parameter.
+
For more details, see the section <> below.
+
@@ -172,7 +172,7 @@ The `-c` and `-cloud` options are equivalent:
If you specify a ZooKeeper connection string, such as `-z 192.168.1.4:2181`, then Solr will connect to ZooKeeper and join the cluster.
-NOTE: If you have defined `ZK_HOST` in `solr.in.sh`/`solr.in.cmd` (see <>) you can omit `-z ` from all `bin/solr` commands.
+NOTE: If you have defined `ZK_HOST` in `solr.in.sh`/`solr.in.cmd` (see <>) you can omit `-z ` from all `bin/solr` commands.
When starting Solr in cloud mode, if you neither define `ZK_HOST` in `solr.in.sh`/`solr.in.cmd` nor specify the `-z` option, then Solr will launch an embedded ZooKeeper server listening on the Solr port + 1000, i.e., if Solr is running on port 8983, then the embedded ZooKeeper will be listening on port 9983.
diff --git a/solr/solr-ref-guide/src/solr-tutorial.adoc b/solr/solr-ref-guide/src/solr-tutorial.adoc
index 34c1f9cce15c..577fc5cdd2f3 100644
--- a/solr/solr-ref-guide/src/solr-tutorial.adoc
+++ b/solr/solr-ref-guide/src/solr-tutorial.adoc
@@ -497,7 +497,7 @@ This starts the first node. When it's done start the second node, and tell it ho
`./bin/solr start -c -p 7574 -s example/cloud/node2/solr -z localhost:9983`
-NOTE: If you have defined `ZK_HOST` in `solr.in.sh`/`solr.in.cmd` (see <>) you can omit `-z ` from the above command.
+NOTE: If you have defined `ZK_HOST` in `solr.in.sh`/`solr.in.cmd` (see <>) you can omit `-z ` from the above command.
=== Create a New Collection
diff --git a/solr/solr-ref-guide/src/solrcloud-autoscaling-triggers.adoc b/solr/solr-ref-guide/src/solrcloud-autoscaling-triggers.adoc
index ddb2002cd394..0b41e21914d1 100644
--- a/solr/solr-ref-guide/src/solrcloud-autoscaling-triggers.adoc
+++ b/solr/solr-ref-guide/src/solrcloud-autoscaling-triggers.adoc
@@ -55,7 +55,7 @@ generated, which may significantly differ due to the rate limits set by `waitFor
indicates the nodes that were lost or added.
== Trigger Configuration
-Trigger configurations are managed using the <> with the commands `<>`, `<>`,
+Trigger configurations are managed using the <> with the commands `<>`, `<>`,
`suspend-trigger`, and `resume-trigger`.
=== Trigger Properties
diff --git a/solr/solr-ref-guide/src/using-zookeeper-to-manage-configuration-files.adoc b/solr/solr-ref-guide/src/using-zookeeper-to-manage-configuration-files.adoc
index 091011eebc6b..b1b3b5a77e1b 100644
--- a/solr/solr-ref-guide/src/using-zookeeper-to-manage-configuration-files.adoc
+++ b/solr/solr-ref-guide/src/using-zookeeper-to-manage-configuration-files.adoc
@@ -85,4 +85,4 @@ If you for example would like to keep your `solr.xml` in ZooKeeper to avoid havi
bin/solr zk cp file:local/file/path/to/solr.xml zk:/solr.xml -z localhost:2181
----
-NOTE: If you have defined `ZK_HOST` in `solr.in.sh`/`solr.in.cmd` (see <>) you can omit `-z ` from the above command.
\ No newline at end of file
+NOTE: If you have defined `ZK_HOST` in `solr.in.sh`/`solr.in.cmd` (see <>) you can omit `-z ` from the above command.
\ No newline at end of file
From 9cc9eab3db405c57b92d9aaa664aa88bdf0b238a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20H=C3=B8ydahl?=
Date: Mon, 24 Sep 2018 13:34:14 +0200
Subject: [PATCH 23/88] Remove unused adminScope and supported_ops
---
.../src/java/org/apache/solr/security/JWTAuthPlugin.java | 6 +-----
1 file changed, 1 insertion(+), 5 deletions(-)
diff --git a/solr/core/src/java/org/apache/solr/security/JWTAuthPlugin.java b/solr/core/src/java/org/apache/solr/security/JWTAuthPlugin.java
index 0c277aec9b79..6aa05bf1635e 100644
--- a/solr/core/src/java/org/apache/solr/security/JWTAuthPlugin.java
+++ b/solr/core/src/java/org/apache/solr/security/JWTAuthPlugin.java
@@ -93,7 +93,6 @@ public class JWTAuthPlugin extends AuthenticationPlugin implements HttpClientBui
private static final String PARAM_JWK_CACHE_DURATION = "jwkCacheDur";
private static final String PARAM_CLAIMS_MATCH = "claimsMatch";
private static final String PARAM_SCOPE = "scope";
- private static final String PARAM_ADMIN_SCOPE = "adminScope";
private static final String PARAM_CLIENT_ID = "clientId";
private static final String PARAM_WELL_KNOWN_URL = "wellKnownUrl";
@@ -101,10 +100,9 @@ public class JWTAuthPlugin extends AuthenticationPlugin implements HttpClientBui
private static final String CLAIM_SCOPE = "scope";
private final JwtPkiDelegationInterceptor interceptor = new JwtPkiDelegationInterceptor();
- static final Set supported_ops = ImmutableSet.of("set-user", "delete-user");
private static final Set PROPS = ImmutableSet.of(PARAM_BLOCK_UNKNOWN, PARAM_JWK_URL, PARAM_JWK, PARAM_ISSUER,
PARAM_AUDIENCE, PARAM_REQUIRE_SUBJECT, PARAM_PRINCIPAL_CLAIM, PARAM_REQUIRE_EXPIRATIONTIME, PARAM_ALG_WHITELIST,
- PARAM_JWK_CACHE_DURATION, PARAM_CLAIMS_MATCH, PARAM_SCOPE, PARAM_ADMIN_SCOPE, PARAM_CLIENT_ID, PARAM_WELL_KNOWN_URL);
+ PARAM_JWK_CACHE_DURATION, PARAM_CLAIMS_MATCH, PARAM_SCOPE, PARAM_CLIENT_ID, PARAM_WELL_KNOWN_URL);
private JwtConsumer jwtConsumer;
private String iss;
@@ -116,7 +114,6 @@ public class JWTAuthPlugin extends AuthenticationPlugin implements HttpClientBui
private String principalClaim;
private HashMap claimsMatchCompiled;
private boolean blockUnknown;
- private String adminScope;
private HashSet requiredScopes = new HashSet<>();
private String clientId;
private long jwkCacheDuration;
@@ -178,7 +175,6 @@ public void init(Map pluginConfig) {
if (!StringUtils.isEmpty(requiredScopesStr)) {
requiredScopes = new HashSet<>(Arrays.asList(requiredScopesStr.split("\\s+")));
}
- adminScope = (String) pluginConfig.get(PARAM_ADMIN_SCOPE);
Map claimsMatch = (Map) pluginConfig.get(PARAM_CLAIMS_MATCH);
claimsMatchCompiled = new HashMap<>();
if (claimsMatch != null) {
From 404a2671e4ea7bf21e4b5738758f36e0d92afaea Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20H=C3=B8ydahl?=
Date: Mon, 24 Sep 2018 13:50:59 +0200
Subject: [PATCH 24/88] Prettify docs table
---
.../src/jwt-authentication-plugin.adoc | 14 +++++++-------
1 file changed, 7 insertions(+), 7 deletions(-)
diff --git a/solr/solr-ref-guide/src/jwt-authentication-plugin.adoc b/solr/solr-ref-guide/src/jwt-authentication-plugin.adoc
index a9942792c10b..426f55af58fc 100644
--- a/solr/solr-ref-guide/src/jwt-authentication-plugin.adoc
+++ b/solr/solr-ref-guide/src/jwt-authentication-plugin.adoc
@@ -107,19 +107,19 @@ Let's comment on this config:
[%header,format=csv,separator=;]
|===
-Key ; Description ; Default
+Key ; Description ; Default
blockUnknown ; Set to `false` in order to pass requests from unknown users without a token ; `true`
-wellKnownUrl ; URL to an https://openid.net/specs/openid-connect-discovery-1_0.html[OpenID Connect Discovery] endpoint ;
+wellKnownUrl ; URL to an https://openid.net/specs/openid-connect-discovery-1_0.html[OpenID Connect Discovery] endpoint ;
clientId ; Client identifier for use with OpenID Connect ;
-scope ; Whitespace separate list of valid scopes. If configured, the JWT access token MUST contain a `scope` claim with at least one of the listed scopes. Example: `solr:read solr:admin` ;
+scope ; Whitespace separate list of valid scopes. If configured, the JWT access token MUST contain a `scope` claim with at least one of the listed scopes. Example: `solr:read solr:admin` ;
jwkUrl ; An https URL to a https://tools.ietf.org/html/rfc7517[JWK] keys file. Not required if `well_known_url` is provided ; Auto configured if `well_known_url` is provided
-jwk ; As an alternative to `jwk_url` you may provide a JSON object here containing the public key(s) of the issuer. ; Auto configured if `well_known_url` is provided
-iss ; Validates that `iss` (issuer) equals this string ; If `well_known_url` is provided, use `issuer` from there.
-aud ; Validates that `aud` (audience) equals this string ; If `client_id` is configured, require `aud` to match it
+jwk ; As an alternative to `jwk_url` you may provide a JSON object here containing the public key(s) of the issuer. ; Auto configured if `well_known_url` is provided
+iss ; Validates that `iss` (issuer) equals this string ; If `well_known_url` is provided, use `issuer` from there.
+aud ; Validates that `aud` (audience) equals this string ; If `client_id` is configured, require `aud` to match it
requireSub ; Makes `sub` (subject) mandatory ; `true`
requireExp ; Makes `exp` (expiry time) mandatory ; `true`
algWhitelist ; JSON array with algorithms to accept: `HS256`, `HS384`, `HS512`, `RS256`, `RS384`, `RS512`, `ES256`, `ES384`, `ES512`, `PS256`, `PS384`, `PS512`, `none ;
-jwkCacheDur ; Duration of JWK cache in seconds ; `3600` (1 hour)
+jwkCacheDur ; Duration of JWK cache in seconds ; `3600` (1 hour)
principalClaim ; What claim id to pull principal from ; `sub`
claimsMatch ; JSON object of claims (key) that must match a regular expression (value). Example: `{ "foo" : "A|B" }` will require the `foo` claim to be either "A" or "B". ; (none)
|===
From 78b1341f2f902a195d4983041258ff62a30d4bcd Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20H=C3=B8ydahl?=
Date: Mon, 24 Sep 2018 13:58:59 +0200
Subject: [PATCH 25/88] Finalise clients chapter
---
.../solr-ref-guide/src/jwt-authentication-plugin.adoc | 11 ++++++++---
1 file changed, 8 insertions(+), 3 deletions(-)
diff --git a/solr/solr-ref-guide/src/jwt-authentication-plugin.adoc b/solr/solr-ref-guide/src/jwt-authentication-plugin.adoc
index 426f55af58fc..86d38486c4d2 100644
--- a/solr/solr-ref-guide/src/jwt-authentication-plugin.adoc
+++ b/solr/solr-ref-guide/src/jwt-authentication-plugin.adoc
@@ -163,16 +163,21 @@ curl --user solr:SolrRocks http://localhost:8983/api/cluster/security/authentica
[#jwt-soljr]
=== SolrJ
-SolrJ does not currently support setting JWT tokens per request. This is planned for a future release.
+SolrJ does not currently support supplying JWT tokens per request.
[#jwt-curl]
=== cURL
-To authenticate with Solr with cURL
+To authenticate with Solr when using the cURL utility, supply a valid JWT access token in an `Authorization` header, as follows (replace xxxxxx.xxxxxx.xxxxxx with your JWT compact token):
+
+[source,bash]
+----
+curl -H "Authorization: Bearer xxxxxx.xxxxxx.xxxxxx" http://localhost:8983/solr/admin/info/system
+----
=== Admin UI
-The Admin UI runs as a SPA (single page application) in your browser, and talks with the Solr APIs just like any other client. Because of this the Admin UI will stop working after enabling this plugin. To circumvent this you can install a browser plugin that adds HTTP headers to requests, and add a valid JWT token to the `Authorization` header by hand.
+The Admin UI runs as a SPA (single page application) in your browser, and talks with the Solr APIs just like any other client. Because of this the Admin UI will stop working after enabling this plugin. To circumvent this you may be able to install a browser plugin that adds HTTP headers to requests, and add manually a valid JWT token to the `Authorization` header.
== Using the Solr Control Script with JWT Auth
From 06adf2ecc93a1451fb0dca703d64aa13084aacd2 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20H=C3=B8ydahl?=
Date: Wed, 12 Dec 2018 15:07:14 +0100
Subject: [PATCH 26/88] Pull out parts that belong to other JIRAs Changes entry
moved to 8.0.0 section
---
lucene/ivy-versions.properties | 2 +-
solr/CHANGES.txt | 4 ++--
.../solr/handler/component/HttpShardHandler.java | 9 +--------
.../solr/security/PKIAuthenticationPlugin.java | 2 +-
.../org/apache/solr/update/SolrCmdDistributor.java | 3 ---
.../org/apache/solr/client/solrj/SolrRequest.java | 11 -----------
.../solr/client/solrj/impl/HttpSolrClient.java | 13 +++----------
7 files changed, 8 insertions(+), 36 deletions(-)
diff --git a/lucene/ivy-versions.properties b/lucene/ivy-versions.properties
index d0140c94e56c..52d965dcbe4b 100644
--- a/lucene/ivy-versions.properties
+++ b/lucene/ivy-versions.properties
@@ -308,4 +308,4 @@ org.slf4j.version = 1.7.24
ua.net.nlp.morfologik-ukrainian-search.version = 3.9.0
/ua.net.nlp/morfologik-ukrainian-search = ${ua.net.nlp.morfologik-ukrainian-search.version}
-/xerces/xercesImpl = 2.9.1
\ No newline at end of file
+/xerces/xercesImpl = 2.9.1
diff --git a/solr/CHANGES.txt b/solr/CHANGES.txt
index b39719cd7265..1502c38da56c 100644
--- a/solr/CHANGES.txt
+++ b/solr/CHANGES.txt
@@ -79,6 +79,8 @@ New Features
* SOLR-12593: The default configSet now includes an "ignored_*" dynamic field. (David Smiley)
+* SOLR-12121: JWT Token authentication plugin (janhoy)
+
* SOLR-12791: Add Metrics reporting for AuthenticationPlugin (janhoy)
Bug Fixes
@@ -845,8 +847,6 @@ New Features
* SOLR-11913: SolrJ SolrParams now implements Iterable> and also has a stream() method
using it for convenience. (David Smiley, Tapan Vaishnav)
-* SOLR-12121: JWT Token authentication plugin (janhoy)
-
* SOLR-11924: Added the ability to listen to changes in the set of active collections in a cloud
in the ZkStateReader, through the CloudCollectionsListener. (Houston Putman, Dennis Gove)
diff --git a/solr/core/src/java/org/apache/solr/handler/component/HttpShardHandler.java b/solr/core/src/java/org/apache/solr/handler/component/HttpShardHandler.java
index 05988eb9c8f6..a548031f7bd2 100644
--- a/solr/core/src/java/org/apache/solr/handler/component/HttpShardHandler.java
+++ b/solr/core/src/java/org/apache/solr/handler/component/HttpShardHandler.java
@@ -18,7 +18,6 @@
import java.lang.invoke.MethodHandles;
import java.net.ConnectException;
-import java.security.Principal;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
@@ -57,7 +56,6 @@
import org.apache.solr.common.util.StrUtils;
import org.apache.solr.core.CoreDescriptor;
import org.apache.solr.request.SolrQueryRequest;
-import org.apache.solr.request.SolrRequestInfo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
@@ -137,11 +135,7 @@ private List getURLs(String shard) {
public void submit(final ShardRequest sreq, final String shard, final ModifiableSolrParams params) {
// do this outside of the callable for thread safety reasons
final List urls = getURLs(shard);
-
- // If request has a Principal (authenticated user), extract it for passing on to the new shard request
- SolrRequestInfo requestInfo = SolrRequestInfo.getRequestInfo();
- final Principal userPrincipal = requestInfo == null ? null : requestInfo.getReq().getUserPrincipal();
-
+
Callable task = () -> {
ShardResponse srsp = new ShardResponse();
@@ -160,7 +154,6 @@ public void submit(final ShardRequest sreq, final String shard, final Modifiable
QueryRequest req = makeQueryRequest(sreq, params, shard);
req.setMethod(SolrRequest.METHOD.POST);
- req.setUserPrincipal(userPrincipal);
// no need to set the response parser as binary is the default
// req.setResponseParser(new BinaryResponseParser());
diff --git a/solr/core/src/java/org/apache/solr/security/PKIAuthenticationPlugin.java b/solr/core/src/java/org/apache/solr/security/PKIAuthenticationPlugin.java
index a1ef420a9e49..fe1ae7bdaca0 100644
--- a/solr/core/src/java/org/apache/solr/security/PKIAuthenticationPlugin.java
+++ b/solr/core/src/java/org/apache/solr/security/PKIAuthenticationPlugin.java
@@ -250,7 +250,7 @@ public void process(HttpRequest httpRequest, HttpContext httpContext) throws Htt
}
@SuppressForbidden(reason = "Needs currentTimeMillis to set current time in header")
- protected void setHeader(HttpRequest httpRequest) {
+ void setHeader(HttpRequest httpRequest) {
SolrRequestInfo reqInfo = getRequestInfo();
String usr;
if (reqInfo != null) {
diff --git a/solr/core/src/java/org/apache/solr/update/SolrCmdDistributor.java b/solr/core/src/java/org/apache/solr/update/SolrCmdDistributor.java
index 48632f98abf9..9536f9dd5108 100644
--- a/solr/core/src/java/org/apache/solr/update/SolrCmdDistributor.java
+++ b/solr/core/src/java/org/apache/solr/update/SolrCmdDistributor.java
@@ -47,7 +47,6 @@
import org.apache.solr.common.params.ModifiableSolrParams;
import org.apache.solr.common.util.NamedList;
import org.apache.solr.core.Diagnostics;
-import org.apache.solr.request.SolrRequestInfo;
import org.apache.solr.update.processor.DistributedUpdateProcessor;
import org.apache.solr.update.processor.DistributedUpdateProcessor.LeaderRequestReplicationTracker;
import org.apache.solr.update.processor.DistributedUpdateProcessor.RollupRequestReplicationTracker;
@@ -283,8 +282,6 @@ void addCommit(UpdateRequest ureq, CommitUpdateCommand cmd) {
}
private void submit(final Req req, boolean isCommit) {
- // Copy user principal from the original request to the new update request, for later authentication interceptor use
- req.uReq.setUserPrincipal(SolrRequestInfo.getRequestInfo().getReq().getUserPrincipal());
if (req.synchronous) {
blockAndDoRetries();
diff --git a/solr/solrj/src/java/org/apache/solr/client/solrj/SolrRequest.java b/solr/solrj/src/java/org/apache/solr/client/solrj/SolrRequest.java
index 04b94f7aae84..7dbaab90915c 100644
--- a/solr/solrj/src/java/org/apache/solr/client/solrj/SolrRequest.java
+++ b/solr/solrj/src/java/org/apache/solr/client/solrj/SolrRequest.java
@@ -18,7 +18,6 @@
import java.io.IOException;
import java.io.Serializable;
-import java.security.Principal;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
@@ -37,16 +36,6 @@
* @since solr 1.3
*/
public abstract class SolrRequest implements Serializable {
- // This user principal is typically used by Auth plugins during distributed/sharded search
- private Principal userPrincipal;
-
- public void setUserPrincipal(Principal userPrincipal) {
- this.userPrincipal = userPrincipal;
- }
-
- public Principal getUserPrincipal() {
- return userPrincipal;
- }
public enum METHOD {
GET,
diff --git a/solr/solrj/src/java/org/apache/solr/client/solrj/impl/HttpSolrClient.java b/solr/solrj/src/java/org/apache/solr/client/solrj/impl/HttpSolrClient.java
index 8831448579ef..ad845e828fe4 100644
--- a/solr/solrj/src/java/org/apache/solr/client/solrj/impl/HttpSolrClient.java
+++ b/solr/solrj/src/java/org/apache/solr/client/solrj/impl/HttpSolrClient.java
@@ -24,7 +24,6 @@
import java.net.ConnectException;
import java.net.SocketTimeoutException;
import java.nio.charset.StandardCharsets;
-import java.security.Principal;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
@@ -253,7 +252,7 @@ public NamedList request(final SolrRequest request, final ResponseParser
throws SolrServerException, IOException {
HttpRequestBase method = createMethod(request, collection);
setBasicAuthHeader(request, method);
- return executeMethod(method, request.getUserPrincipal(), processor, isV2ApiRequest(request));
+ return executeMethod(method, processor, isV2ApiRequest(request));
}
private boolean isV2ApiRequest(final SolrRequest request) {
@@ -297,7 +296,7 @@ public HttpUriRequestResponse httpUriRequest(final SolrRequest request, final Re
ExecutorService pool = ExecutorUtil.newMDCAwareFixedThreadPool(1, new SolrjNamedThreadFactory("httpUriRequest"));
try {
MDC.put("HttpSolrClient.url", baseUrl);
- mrr.future = pool.submit(() -> executeMethod(method, request.getUserPrincipal(), processor, isV2ApiRequest(request)));
+ mrr.future = pool.submit(() -> executeMethod(method, processor, isV2ApiRequest(request)));
} finally {
pool.shutdown();
@@ -518,7 +517,7 @@ private HttpEntityEnclosingRequestBase fillContentStream(SolrRequest request, Co
private static final List errPath = Arrays.asList("metadata", "error-class");//Utils.getObjectByPath(err, false,"metadata/error-class")
- protected NamedList executeMethod(HttpRequestBase method, Principal userPrincipal, final ResponseParser processor, final boolean isV2Api) throws SolrServerException {
+ protected NamedList executeMethod(HttpRequestBase method, final ResponseParser processor, final boolean isV2Api) throws SolrServerException {
method.addHeader("User-Agent", AGENT);
org.apache.http.client.config.RequestConfig.Builder requestConfigBuilder = HttpClientUtil.createDefaultRequestConfigBuilder();
@@ -540,12 +539,6 @@ protected NamedList executeMethod(HttpRequestBase method, Principal user
try {
// Execute the method.
HttpClientContext httpClientRequestContext = HttpClientUtil.createNewHttpClientRequestContext();
- if (userPrincipal != null) {
- // Normally the context contains a static userToken to enable reuse resources.
- // However, if a personal Principal object exists, we use that instead, also as a means
- // to transfer authentication information to Auth plugins that wish to intercept the request later
- httpClientRequestContext.setUserToken(userPrincipal);
- }
final HttpResponse response = httpClient.execute(method, httpClientRequestContext);
int httpStatus = response.getStatusLine().getStatusCode();
From d35d157b648f89ddd343e03b772e45eaa00dfe14 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20H=C3=B8ydahl?=
Date: Wed, 12 Dec 2018 15:22:50 +0100
Subject: [PATCH 27/88] New sha1 sum
---
solr/licenses/jose4j-0.6.3.jar.sha1 | 1 -
1 file changed, 1 deletion(-)
delete mode 100644 solr/licenses/jose4j-0.6.3.jar.sha1
diff --git a/solr/licenses/jose4j-0.6.3.jar.sha1 b/solr/licenses/jose4j-0.6.3.jar.sha1
deleted file mode 100644
index 968ae6dd27e2..000000000000
--- a/solr/licenses/jose4j-0.6.3.jar.sha1
+++ /dev/null
@@ -1 +0,0 @@
-05bf092aa7be6fe8894d11f8d2040a2b3b401a14
From f08bb76081869430cd631677948048ac623fcf2c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20H=C3=B8ydahl?=
Date: Wed, 12 Dec 2018 15:32:27 +0100
Subject: [PATCH 28/88] Fix precommit by specifying charset
---
.../apache/solr/security/JWTAuthPluginIntegrationTest.java | 7 ++++---
1 file changed, 4 insertions(+), 3 deletions(-)
diff --git a/solr/core/src/test/org/apache/solr/security/JWTAuthPluginIntegrationTest.java b/solr/core/src/test/org/apache/solr/security/JWTAuthPluginIntegrationTest.java
index d2f35babc138..03ae5416c30c 100644
--- a/solr/core/src/test/org/apache/solr/security/JWTAuthPluginIntegrationTest.java
+++ b/solr/core/src/test/org/apache/solr/security/JWTAuthPluginIntegrationTest.java
@@ -23,6 +23,7 @@
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
+import java.nio.charset.StandardCharsets;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
@@ -154,7 +155,7 @@ private Pair